【问题标题】:Efficient data/string reading and copy from file (CSV) c从文件 (CSV) 中高效读取和复制数据/字符串 c
【发布时间】:2017-12-11 06:36:48
【问题描述】:

我已阅读其他有关从文件复制数据的帖子。让我告诉你为什么我的情况不同。 使用 C,我必须从 csv 文件中读取 4300 万行输入。条目没有错误,格式如下:

int, int , int , variable length string with only alphanumerics and spaces , int \n

问题是我将所有数据以数组和列表的形式复制到内存中,以对其进行一些非常简单的平均,然后将所有处理过的数据输出到文件中,没什么特别的。 我在三个主要方面需要帮助:

  1. 关于字符串,(我最大的问题在这里)首先从文件中读取,然后复制到数组中,然后传递给另一个函数,只有在满足条件时才会将其复制到动态内存中。例如:

    fileAnalizer(){
      while ( ! EOF ){
        char * s = function_to_read_string_from_file();
        data_to_array(s);
      }
      ....
      ....
      processData(arrays);
      dataToFiles(arrays);
    
    }
    
    void data_to_structures(char * s){
      if ( condition is met)
        char * aux = malloc((strlen(s)+1 )* sizeof(char));
        strcpy(aux,s);
      ....
      ...
    }
    

    如您所见,字符串经过了 3 次。我需要一种方法来更有效地执行此过程,减少字符串的传输次数。我曾尝试逐个字符地读取字符并计算字符串长度,但整个过程变慢了。

  2. 高效读取输入:您是否建议先将所有数据复制到缓冲区中?如果是这样:什么类型的缓冲区,在许多块中还是只有一个? 这是我目前的阅读计划:

    void
    csvReader(FILE* f){
        T_structCDT c;
        c.string = malloc(MAX_STRING_LENGHT*sizeof(char));
        while (fscanf(f,"%d,%d,%d,%[^,],%d\n",&c.a, &c.b, &c.vivienda, c.c, &c.d)==5 ){
            data_to_structures(c);
        }
    }
    
  3. 然后,我有近 50000 条 csv 行的已处理数据可以转储到其他文件中。你会如何推荐倾销?逐行或再次将数据发送到缓冲区然后进行转储?我的代码现在看起来像这样。

    void dataToFiles(arrayOfStructs immenseAr1, arrayOfStructs immenseAr2){
      for (iteration over immenseAr1) {
          fprintf(f1, "%d,%d,%s\n", immenseAr1[i].a, immenseAr1[i].b, inmenseAr1[i].string);
      }
      for (iteration over immenseAr2) {
          fprintf(f2, "%d,%d,%s\n", inmenseAr2[i].a, inmenseAr2[i].b, inmenseAr2[i].string);
      }
    }
    

我必须在转储之前读取所有数据。除了将所有数据存储到内存中然后分析它然后转储所有分析的数据之外,您是否会推荐一种不同的方法? 该程序有 2000 万行,目前耗时超过 40 秒。 我真的需要缩短那个时间。

【问题讨论】:

  • 第一。如果您有像 gcc 这样的优化编译器,则指定 -O2 或 -O3 命令行选项。如果您发布了一个 MCV 示例,那么我可能看不到有多少指令可以在执行过程中被优化。 第二。使用分析器。如果贡献者的来源对响应时间有很大的偏差,那么您可能会找到一个金块。但是,我确实看到了对 calloc 的调用,然后是对 strcpy 的调用。也许 strdup 会是一个很好的替代品?但我不知道这是否会成为响应时间的重要因素。使用分析器,您肯定会知道。
  • 好吧,没有任何关于处理时间与磁盘时间之类的数字,很难提出太多建议。如果处理需要付出很多努力,您可能会分块读取它,在读取下一个块时处理一个块。也许您可以将块内存池化以避免一些 malloc。没有更多细节,很难看出经济可能在哪里产生。好像有很多抄袭?我假设您的 CPU 和 SSD 硬件已经相当快了?
  • 4300 万行中有多少行必须存储在内存中?你说有 500 行要输出——它们是来自 4300 万行,还是以某种方式制造的?代码的哪些部分实际上用完了时间?你是怎么测量的?如果dataToFiles 处理是瓶颈,我确实会感到惊讶。要满足的条件有多复杂? data_to_array()data_to_structures() 有什么关系? function_to_read_string_from_file()csvReader() 有什么关系?您的伪代码并不清楚。你需要非常清楚。

标签: c string performance csv processing-efficiency


【解决方案1】:

尝试扫描您的大文件而不将其全部存储到内存中,一次只在局部变量中保存一条记录:

void csvReader(FILE *f) {
    T_structCDT c;
    int count = 0;
    c.string = malloc(1000);
    while (fscanf(f, "%d,%d,%d,%999[^,],%d\n", &c.a, &c.b, &c.vivienda, c.c, &c.d) == 5) {
        // nothing for now
        count++;
    }
    printf("%d records parsed\n");
}

测量这个简单的解析器所花费的时间:

  • 如果速度足够快,请执行选择测试并在解析阶段找到少数匹配记录时一次输出一条。这些步骤的额外时间应该相当少,因为只有少数记录匹配。

  • 时间太长了,你需要一个更花哨的 CSV 解析器,这是很多工作,但可以快速完成,特别是如果你可以假设你的输入文件对所有人都使用这种简单的格式记录。这里的主题过于宽泛,无法详细说明,但可达到的速度应该接近 cat csvfile > /dev/nullgrep a_short_string_not_present csvfile

在我的系统上(普通硬盘的普通linux服务器),从冷启动开始解析4000万行总计2GB的时间不到20秒,第二次不到4秒:磁盘I/O似乎是瓶颈。

如果您需要经常执行此选择,您可能应该使用不同的数据格式,可能是数据库系统。如果偶尔对格式固定的数据执行扫描,则使用 SSD 等更快的存储会有所帮助,但不要指望奇迹。

编辑为了将文字付诸实践,我编写了一个简单的生成器和提取器:

这是一个生成 CSV 数据的简单程序:

#include <stdio.h>
#include <stdlib.h>

const char *dict[] = {
    "Lorem", "ipsum", "dolor", "sit", "amet;", "consectetur", "adipiscing", "elit;",
    "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et",
    "dolore", "magna", "aliqua.", "Ut", "enim", "ad", "minim", "veniam;",
    "quis", "nostrud", "exercitation", "ullamco", "laboris", "nisi", "ut", "aliquip",
    "ex", "ea", "commodo", "consequat.", "Duis", "aute", "irure", "dolor",
    "in", "reprehenderit", "in", "voluptate", "velit", "esse", "cillum", "dolore",
    "eu", "fugiat", "nulla", "pariatur.", "Excepteur", "sint", "occaecat", "cupidatat",
    "non", "proident;", "sunt", "in", "culpa", "qui", "officia", "deserunt",
    "mollit", "anim", "id", "est", "laborum.",
};

int csvgen(const char *fmt, long lines) {
    char buf[1024];

    if (*fmt == '\0')
        return 1;

    while (lines > 0) {
        size_t pos = 0;
        int count = 0;
        for (const char *p = fmt; *p && pos < sizeof(buf); p++) {
            switch (*p) {
            case '0': case '1': case '2': case '3': case '4':
            case '5': case '6': case '7': case '8': case '9':
                count = count * 10 + *p - '0';
                continue;
            case 'd':
                if (!count) count = 101;
                pos += snprintf(buf + pos, sizeof(buf) - pos, "%d",
                                rand() % (2 + count - 1) - count + 1);
                count = 0;
                continue;
            case 'u':
                if (!count) count = 101;
                pos += snprintf(buf + pos, sizeof(buf) - pos, "%u",
                                rand() % count);
                count = 0;
                continue;
            case 's':
                if (!count) count = 4;
                count = rand() % count + 1;
                while (count-- > 0 && pos < sizeof(buf)) {
                    pos += snprintf(buf + pos, sizeof(buf) - pos, "%s ",
                                    dict[rand() % (sizeof(dict) / sizeof(*dict))]);
                }
                if (pos < sizeof(buf)) {
                    pos--;
                }
                count = 0;
                continue;
            default:
                buf[pos++] = *p;
                count = 0;
                continue;
            }
        }
        if (pos < sizeof(buf)) {
            buf[pos++] = '\n';
            fwrite(buf, 1, pos, stdout);
            lines--;
        }
    }
    return 0;
}

int main(int argc, char *argv[]) {
    if (argc < 3) {
        fprintf(stderr, "usage: csvgen format number\n");
        return 2;
    }
    return csvgen(argv[1], strtol(argv[2], NULL, 0));
}

这是一个具有 3 种不同解析方法的提取器:

#include <stdio.h>
#include <stdlib.h>

static inline unsigned int getuint(const char *p, const char **pp) {
    unsigned int d, n = 0;
    while ((d = *p - '0') <= 9) {
        n = n * 10 + d;
        p++;
    }
    *pp = p;
    return n;
}

int csvgrep(FILE *f, int method) {
    struct {
        int a, b, c, d;
        int spos, slen;
        char s[1000];
    } c;
    int count = 0, line = 0;

    // select 500 out of 43M
#define select(c)  ((c).a == 100 && (c).b == 100 && (c).c > 74 && (c).d > 50)

    if (method == 0) {
        // default method: fscanf
        while (fscanf(f, "%d,%d,%d,%999[^,],%d\n", &c.a, &c.b, &c.c, c.s, &c.d) == 5) {
            line++;
            if (select(c)) {
                count++;
                printf("%d,%d,%d,%s,%d\n", c.a, c.b, c.c, c.s, c.d);
            }
        }
    } else
    if (method == 1) {
        // use fgets and simple parser
        char buf[1024];
        while (fgets(buf, sizeof(buf), f)) {
            char *p = buf;
            int i;
            line++;
            c.a = strtol(p, &p, 10);
            p += (*p == ',');
            c.b = strtol(p, &p, 10);
            p += (*p == ',');
            c.c = strtol(p, &p, 10);
            p += (*p == ',');
            for (i = 0; *p && *p != ','; p++) {
                c.s[i++] = *p;
            }
            c.s[i] = '\0';
            p += (*p == ',');
            c.d = strtol(p, &p, 10);
            if (*p != '\n') {
                fprintf(stderr, "csvgrep: invalid format at line %d\n", line);
                continue;
            }
            if (select(c)) {
                count++;
                printf("%d,%d,%d,%s,%d\n", c.a, c.b, c.c, c.s, c.d);
            }
        }
    } else
    if (method == 2) {
        // use fgets and hand coded parser, positive numbers only, no string copy
        char buf[1024];
        while (fgets(buf, sizeof(buf), f)) {
            const char *p = buf;
            line++;
            c.a = getuint(p, &p);
            p += (*p == ',');
            c.b = getuint(p, &p);
            p += (*p == ',');
            c.c = getuint(p, &p);
            p += (*p == ',');
            c.spos = p - buf;
            while (*p && *p != ',') p++;
            c.slen = p - buf - c.spos;
            p += (*p == ',');
            c.d = getuint(p, &p);
            if (*p != '\n') {
                fprintf(stderr, "csvgrep: invalid format at line %d\n", line);
                continue;
            }
            if (select(c)) {
                count++;
                printf("%d,%d,%d,%.*s,%d\n", c.a, c.b, c.c, c.slen, buf + c.spos, c.d);
            }
        }
    } else {
        fprintf(stderr, "csvgrep: unknown method: %d\n", method);
        return 1;
    }
    fprintf(stderr, "csvgrep: %d records selected from %d lines\n", count, line);
    return 0;
}

int main(int argc, char *argv[]) {
    if (argc > 2 && strtol(argv[2], NULL, 0)) {
        // non zero second argument -> set a 1M I/O buffer
        setvbuf(stdin, NULL, _IOFBF, 1024 * 1024);
    }
    return csvgrep(stdin, argc > 1 ? strtol(argv[1], NULL, 0) : 0);
}

以下是一些比较基准数据:

$ time ./csvgen "u,u,u,s,u" 43000000 > 43m
real    0m34.428s    user    0m32.911s    sys     0m1.358s
$ time grep zz 43m
real    0m10.338s    user    0m10.069s    sys     0m0.211s
$ time wc -lc 43m
 43000000 1195458701 43m
real    0m1.043s     user    0m0.839s     sys     0m0.196s
$ time cat 43m > /dev/null
real    0m0.201s     user    0m0.004s     sys     0m0.195s
$ time ./csvgrep 0 < 43m > x0
csvgrep: 508 records selected from 43000000 lines
real    0m14.271s    user    0m13.856s    sys     0m0.341s
$ time ./csvgrep 1 < 43m > x1
csvgrep: 508 records selected from 43000000 lines
real    0m8.235s     user    0m7.856s     sys     0m0.331s
$ time ./csvgrep 2 < 43m > x2
csvgrep: 508 records selected from 43000000 lines
real    0m3.892s     user    0m3.555s     sys     0m0.312s
$ time ./csvgrep 2 1 < 43m > x3
csvgrep: 508 records selected from 43000000 lines
real    0m3.706s     user    0m3.488s     sys     0m0.203s
$ cmp x0 x1
$ cmp x0 x2
$ cmp x0 x3

如您所见,专门解析方法提供了近 50% 的增益,而手动编码整数转换和字符串扫描又获得了 50%。使用 1 MB 缓冲区而不是默认大小只能提供 0.2 秒的边际增益。

为了进一步提高速度,可以使用mmap()绕过I/O流接口,对文件内容做更强的假设。在上面的代码中,仍然可以优雅地处理无效格式,但是您可以删除一些测试并以可靠性为代价将执行时间额外减少 5%。

上述基准测试是在具有 SSD 驱动器的系统上执行的,并且文件 43m 适合 RAM,因此计时不包括太多磁盘 I/O 延迟。 grep 速度出奇的慢,增加搜索字符串长度会使它变得更糟……wc -lc 为扫描性能设定了一个目标,为 4 倍,但 cat 似乎遥不可及。

【讨论】:

  • 我喜欢这个答案。是的,我尝试了多种解决方案。我认为使用getc() 循环会更快,但不知何故fscanf() 是最快的。目前,我需要 30 秒才能阅读所有 40 Mill。我不能做任何硬件更改,但你说you need a more fancy CSV parser, which is a lot of work but can be done and made fast你能给我一个想法吗?
  • @PiloBasualdo:好的,我为早餐编写了一个基准代码,以便您了解。 4 倍似乎很容易获得,但如果 I/O 延迟是您系统的瓶颈,您可能会观察到小得多的改进。
【解决方案2】:
  1. 使用 aux=strdup(s); 代替 calloc()、strlen() 和 strcpy()。

  2. 您的操作系统(文件系统)通常在缓冲数据流方面非常有效。人们可能会找到一种更有效的缓冲数据流的方法,但这种尝试通常最终会冗余缓冲操作系统已经缓冲的内容。您的操作系统很可能提供特定功能,允许您绕过通常由操作系统/文件系统完成的缓冲。通常,这意味着不使用 fscanf() 等“stdio.h”函数。

  3. 同样,请注意不要不必要地双重缓冲您的数据。请记住,操作系统会缓冲您的数据,并在通常最有效的时候将其实际写入。 (这就是为什么有一个 fflush() 函数......向操作系统建议您将等到它写入所有数据后再继续。)而且,就像通常有特定的操作系统函数来绕过操作系统读取缓冲区一样,通常有操作系统特定的功能来绕过操作系统写入缓冲区。但是,这些功能可能超出了您(也可能是这些受众)的需求范围。

我的总结答案(如上所述)是,试图超越操作系统以及它缓冲数据流的方式通常会导致代码效率降低。

【讨论】:

    【解决方案3】:

    选择合适的工具

    有这么多数据(你说 来自 csv 文件的 4300 万行输入),硬盘 I/O 将成为瓶颈,而且由于您每次都在处理纯文本文件您需要进行不同的计算(如果您改变主意并希望对其进行稍微不同的非常非常简单的平均值,然后将所有处理过的数据输出到文件中),您将需要遍历所有每次都是这个过程。

    更好的策略是使用数据库管理系统,它是存储和处理大量数据的合适工具,并且可以让您灵活地进行所需的任何处理,包括索引数据、高效的内存处理和缓存等,使用简单的 SQL 命令。

    如果不想搭建SQL服务器(如MySQL或PostgreSQL),可以使用不需要服务器的数据库管理系统,如SQLite:http://www.sqlite.org/,另外,使用 sqlite3 shell 程序从命令行驱动,或者如果您愿意,也可以从 C 程序(SQLite 实际上是一个 C 库),或者使用 GUI 界面,例如 http://sqlitestudio.pl/

    SQLite 将允许您创建数据库、创建表、将 CSV 文件导入其中、进行计算并以各种格式转储结果,...

    使用 SQLite 的演练示例

    这是一个帮助您入门的示例,说明 sqlite3 shell 程序和 C 代码的使用。

    假设您在data1.csv 中有您描述的格式的数据,其中包含:

    1,2,3,variable length string with only alphanumerics and spaces,5
    11,22,33,other variable length string with only alphanumerics and spaces,55
    111,222,333,yet another variable length string with only alphanumerics and spaces,555
    

    data2.csv 中包含:

    2,3,4,from second batch variable length string with only alphanumerics and spaces,6
    12,23,34,from second batch other variable length string with only alphanumerics and spaces,56
    112,223,334,from second batch yet another variable length string with only alphanumerics and spaces,556
    

    您使用sqlite3 命令行实用程序创建数据库,创建具有正确格式的表,导入 CSV 文件,然后发出如下 SQL 命令:

    $ sqlite3 bigdatabase.sqlite3
    SQLite version 3.8.7.1 2014-10-29 13:59:56
    Enter ".help" for usage hints.
    sqlite> create table alldata(col1 int, col2 int, col3 int, col4 varchar(255), col5 int);
    sqlite> .mode csv
    sqlite> .import data1.csv alldata
    sqlite> .import data2.csv alldata
    sqlite> select * from alldata;
    1,2,3,"variable length string with only alphanumerics and spaces",5
    11,22,33,"other variable length string with only alphanumerics and spaces",55
    111,222,333,"yet another variable length string with only alphanumerics and spaces",555
    2,3,4,"from second batch variable length string with only alphanumerics and spaces",6
    12,23,34,"from second batch other variable length string with only alphanumerics and spaces",56
    112,223,334,"from second batch yet another variable length string with only alphanumerics and spaces",556
    sqlite> select avg(col2) from alldata;
    82.5
    sqlite> 
    

    (按 Ctrl-D 退出 SQLite shell)

    上面,我们创建了一个bigdatabase.sqlite3文件,其中包含由SQLite处理的创建的数据库,一个表alldata,我们将CSV数据导入其中,显示其中包含的数据(不要在4300万行),并(计算并)显示我们命名为col2 的列中包含的整数的平均值,该列恰好是第二列。

    您可以将创建的 SQLite 数据库与 C 和 SQLite 库一起使用,以实现相同的目的。

    创建一个sqlite-average.c 文件(改编自SQLite's Quickstart Page 中的示例),如下所示:

    #include <stdio.h>
    #include <sqlite3.h>
    
    static int callback(void *NotUsed, int argc, char **argv, char **azColName) {
        int i;
        for(i=0; i<argc; i++){
            printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
        }
        printf("\n");
        return 0;
    }
    
    int main(void) {
        sqlite3 *db;
        char *zErrMsg = 0;
        int rc;
    
        rc = sqlite3_open("bigdatabase.sqlite3", &db);                                                      
        if (rc) {
            fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
            sqlite3_close(db);
            return 1;
        }
        rc = sqlite3_exec(db, "select avg(col2) from alldata;", callback, 0, &zErrMsg);
        if (rc!=SQLITE_OK){
            fprintf(stderr, "SQL error: %s\n", zErrMsg);
            sqlite3_free(zErrMsg);
        }
        sqlite3_close(db);
    
        return 0;
    }
    

    编译它,链接到已安装的 SQLite 库,使用 gcc 你可以这样做:

    $ gcc -Wall sqlite-average.c -lsqlite3
    

    运行编译后的可执行文件:

    $ ./a.out
    avg(col2) = 82.5
    
    $
    

    您可能希望为查找数据的列创建索引,例如此表中的第 2 列和第 5 列,以便在那里更快地获取信息:

    sqlite> create index alldata_idx ON alldata(col2,col5);
    

    如果适用,决定哪一列将包含表的主键等。

    更多信息,请查看:

    【讨论】:

    • 这是一个非常棒的 sqlite 教程,但我完全不相信 sqlite 或 sql 非常适合这个。我认为您不会在 40 秒内将 4300 万行的任何内容放入 sqlite,更不用说过程了。大数据推理开始适用于该规模
    • 如果你真的只有一个计算,并且你知道你需要全面扫描,那么自定义代码可能是正确的答案。
    • @SamHartman,我的观点是,扫描平面文本文件所涉及的所有解析和磁盘 I/O 是主要耗时的任务,而使用纯 C 程序方法,他需要如果需求、他想要计算的内容……正在发生变化,则使用文本平面文件重复此过程。很明显,OP不需要一次性操作,否则,他不会担心40秒他在做什么太多。 DMBS 将一劳永逸地为他处理 CSV 数据的导入、内存管理问题等,他可以专注于他的查询。
    • @SamHartman,你指的是“大数据推理”,如果你指的是 NoSQL,请参见:sqlite.org/whentouse.htmlstackoverflow.com/questions/1033309/sqlite-for-large-data-sets——另外,OP 的用例是一致的结构化数据,适用于 SQL ,他似乎需要只读操作,没有连接,...无论如何,我给了他一个简单的操作方法来尝试一下。 SQLite 是其他 DMBS 中的一个例子,我仍然认为它是适合这项工作的工具。我选择展示如何使用它是因为它很容易尝试,而不一定是因为他应该选择这个而不是其他的。
    猜你喜欢
    • 1970-01-01
    • 2017-05-29
    • 2014-12-04
    • 2015-12-03
    • 1970-01-01
    • 2023-04-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多