C 多线程 HPC 计算中,CSV 数据应一次载入内存还是逐行读取?
优先使用带缓冲的顺序读取,再根据内存容量和计算方式决定是否保存全部数据。逐行处理时,每行不一定都会触发一次 read() 系统调用,没必要仅为减少系统调用而把整个 CSV 文件载入内存。
现有信息不足以判断程序的实际瓶颈。总耗时可能主要来自存储读取、CSV 解析、数值转换、计算或线程之间的同步,需要分别测量。
逐行读取为什么不一定慢?
read() 是底层读取接口;fgets()、fread() 等属于 C 标准 I/O 接口,读取普通文件时通常会使用 FILE 的缓冲区。POSIX 的 getline() 也可以用于逐行读取流。
启用缓冲后,库会先读取一批字节,再从缓冲区提供数据。多次按行读取的函数调用可能只对应一次底层读取,不能直接把函数调用次数等同于系统调用次数。
如果当前代码自行反复调用 read(fd, ..., 1),可以尝试改用缓冲读取。如果已经使用带缓冲的流读取,则应先检查 CSV 解析和数值转换的成本。
全量加载和分批处理如何选择?
| 处理方式 | 适用条件 | 需要考虑的成本 |
|---|---|---|
| 全量加载所需数值,再并行计算 | 解析后的数据能轻松放入内存,或需要多次遍历、随机访问 | 加载阶段耗时、内存占用,以及可能出现的换页 |
| 读取一批、计算一批 | 数据量大,主要进行一次顺序遍历 | 批次大小、任务调度和缓冲区管理 |
| 读取与计算形成流水线 | 各批次能独立计算,希望尝试重叠读取与计算 | 队列同步、结果顺序和内存所有权 |
如果各行之间没有计算依赖,可以先让单线程顺序读取和解析,再由工作线程按批计算。批次中保存计算需要的数值,避免为每行单独创建任务。
全量加载时,也不必长期保存全部 CSV 原文。可以边读边解析,只保留需要的列。估算内存占用时,应计入数值数组、结果、临时缓冲区以及其他数据结构。
带缓冲的逐行读取示例
下面的代码演示读取部分,使用 POSIX getline(),因此不属于纯 ISO C。consume_row() 由调用方实现,负责解析一行并处理数据;返回非零值表示处理失败。
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
int scan_csv(const char *path,
int (*consume_row)(const char *, size_t))
{
FILE *fp = fopen(path, "r");
if (fp == NULL)
return -1;
/* 示例缓冲区大小,实际效果需要测量。 */
char io_buffer[64 * 1024];
if (setvbuf(fp, io_buffer, _IOFBF,
sizeof io_buffer) != 0) {
fclose(fp);
return -1;
}
char *line = NULL;
size_t capacity = 0;
ssize_t length;
int status = 0;
while ((length = getline(&line, &capacity, fp)) != -1) {
if (consume_row(line, (size_t)length) != 0) {
status = -1;
break;
}
}
if (!feof(fp))
status = -1;
free(line);
if (fclose(fp) != 0)
status = -1;
return status;
}
setvbuf() 必须在该流进行其他 I/O 操作之前调用。这里的 io_buffer 在 fclose() 前始终有效;更大的缓冲区是否有收益,需要通过实际测量判断。
这个示例按物理行读取。如果 CSV 允许引号字段中包含换行,一次 getline() 不一定能得到一条完整记录,需要由 CSV 解析器继续拼接并识别记录边界。
如何接入多线程计算?
读取线程先将字段解析为数值,填入当前批次。批次填满后,交给工作线程处理,读取线程切换到另一个可用批次。如果所有批次都被占用,读取线程就等待,避免待处理数据无限增长。
接入时还需注意:
getline()会复用或重新分配line,不能把这个指针直接交给异步线程后继续读取。应复制数据,或先解析到具有独立生命周期的批次数组中。- 工作线程应处理已经分配好的记录范围。多个线程同时从同一个
FILE *读取会增加协调难度,也不保证提高吞吐量。 - 使用多个独立文件区间并行解析时,需要正确处理 CSV 记录边界,不能随意按字节切分。
- 如果结果必须保持原始行顺序,应为批次或记录保留顺序信息。
怎样判断优化是否有效?
先分别测量读取、解析、计算以及完整流程的耗时,再比较全量加载与分批处理。比较时保持输入、计算逻辑和线程数一致,并计入首次加载时间。
如果 CSV 解析占据大部分时间,继续增大读取缓冲区可能收益有限。如果计算占据大部分时间,则应关注任务粒度、线程分工和内存访问。如果读取占据大部分时间,可以尝试让读取与计算重叠,但不能保证流水线一定更快。
重复运行还可能受操作系统文件缓存影响。应区分首次读取与缓存中的重复读取,避免把缓存带来的提速误判为代码优化效果。
备注:内容仅供参考。
