PAYATHON 2026

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 解析占据大部分时间,继续增大读取缓冲区可能收益有限。如果计算占据大部分时间,则应关注任务粒度、线程分工和内存访问。如果读取占据大部分时间,可以尝试让读取与计算重叠,但不能保证流水线一定更快。

重复运行还可能受操作系统文件缓存影响。应区分首次读取与缓存中的重复读取,避免把缓存带来的提速误判为代码优化效果。

备注:内容仅供参考。