如何在C++中快速而安全地读取文本文件中的极长行?

9

有一个6.53 GiB的大型文本文件。它的每一行都可以是数据行或注释行。注释行通常很短,不到80个字符,而数据行包含超过2百万个字符,长度可变。

考虑到每个数据行都需要作为一个单元进行处理,是否有一种简单的方法在C++中安全快速地读取行?

安全(针对可变长度的数据行):希望解决方案像std::getline()一样易于使用。由于长度会改变,因此希望避免额外的内存管理。

快速:解决方案可以实现与python 3.6.0readline()一样快,甚至可以与stdio.hfgets()一样快。

欢迎使用纯C解决方案。提供了用于进一步处理的接口,同时支持C和C++。


更新 1: 感谢Basile Starynkevitch提供的简短但宝贵的评论,完美的解决方案出现了:POSIX getline()。由于进一步处理仅涉及从字符转换为数字,并且不使用字符串类的许多功能,在此应用程序中,char数组就足够了。
更新 2:感谢ZulanGalik的评论,他们都报告了std::getline()fgets()POSIX getline()在性能上具有可比性。另一种可能的解决方案是使用更好的标准库实现,如libstdc++。此外,这里有一份报告声称Visual C++和libc++实现的std::getline没有进行良好的优化。
libc++libstdc++的转换大大改变了结果。在另一个平台上,使用libstdc++ 3.4.13 / Linux 2.6.32,POSIX getline()std::getline()fgets()显示相当的性能。最初,在Xcode 8.3.2 (8E2002)的clang默认设置下运行代码,因此使用了libc++
更多细节和一些努力(非常长): <string>getline() 可以处理任意长的行,但速度较慢。在 C++ 中是否有类似于 Python 中的 readline() 的替代方案?
// benchmark on Mac OS X with libc++ and SSD:
readline() of python                         ~550 MiB/s

fgets() of stdio.h, -O0 / -O2               ~1100 MiB/s

getline() of string, -O0                      ~27 MiB/s
getline() of string, -O2                     ~150 MiB/s
getline() of string + stack buffer, -O2      ~150 MiB/s

getline() of ifstream, -O0 / -O2             ~240 MiB/s
read() of ifstream, -O2                      ~340 MiB/s

wc -l                                        ~670 MiB/s

cat data.txt | ./read-cin-unsync              ~20 MiB/s

getline() of stdio.h (POSIX.1-2008), -O0    ~1300 MiB/s
  • 速度只是粗略地四舍五入,只是为了显示数量级,所有代码块都运行了多次以确保值具有代表性。

  • '-O0 / -O2' 表示两个优化级别的速度非常相似

  • 代码如下所示。


Python中的readline()

# readline.py

import time
import os

t_start = time.perf_counter()

fname = 'data.txt'
fin = open(fname, 'rt')

count = 0

while True:
    l = fin.readline()
    length = len(l)
    if length == 0:     # EOF
        break
    if length > 80:     # data line
        count += 1

fin.close()

t_end = time.perf_counter()
time = t_end - t_start

fsize = os.path.getsize(fname)/1024/1024   # file size in MiB
print("speed: %d MiB/s" %(fsize/time))
print("reads %d data lines" %count)

# run as `python readline.py` with python 3.6.0

stdio.hfgets()

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

int main(int argc, char* argv[]){
  clock_t t_start = clock();

  if(argc != 2) {
    fprintf(stderr, "needs one input argument\n");
    return EXIT_FAILURE;
  }

  FILE* fp = fopen(argv[1], "r");
  if(fp == NULL) {
    perror("Failed to open file");
    return EXIT_FAILURE;
  }

  // maximum length of lines, determined previously by python
  const int SIZE = 1024*1024*3;
  char line[SIZE];

  int count = 0;
  while(fgets(line, SIZE, fp) == line) {
    if(strlen(line) > 80) {
      count += 1;
    }
  }

  clock_t t_end = clock();

  const double fsize = 6685;  // file size in MiB

  double time = (t_end-t_start) / (double)CLOCKS_PER_SEC;

  fprintf(stdout, "takes %.2f s\n", time);
  fprintf(stdout, "speed: %d MiB/s\n", (int)(fsize/time));
  fprintf(stdout, "reads %d data lines\n", count);

  return EXIT_SUCCESS;
}

<string>中的getline()

// readline-string-getline.cpp
#include <string>
#include <fstream>
#include <iostream>
#include <ctime>
#include <cstdlib>

using namespace std;

int main(int argc, char* argv[]) {
  clock_t t_start = clock();

  if(argc != 2) {
    fprintf(stderr, "needs one input argument\n");
    return EXIT_FAILURE;
  }

  // manually set the buffer on stack
  const int BUFFERSIZE = 1024*1024*3;   // stack on my platform is 8 MiB
  char buffer[BUFFERSIZE];
  ifstream fin;
  fin.rdbuf()->pubsetbuf(buffer, BUFFERSIZE);
  fin.open(argv[1]);

  // default buffer setting
  // ifstream fin(argv[1]);

  if(!fin) {
    perror("Failed to open file");
    return EXIT_FAILURE;
  }

  // maximum length of lines, determined previously by python
  const int SIZE = 1024*1024*3;
  string line;
  line.reserve(SIZE);

  int count = 0;
  while(getline(fin, line)) {
    if(line.size() > 80) {
      count += 1;
    }
  }

  clock_t t_end = clock();

  const double fsize = 6685;  // file size in MiB

  double time = (t_end-t_start) / (double)CLOCKS_PER_SEC;

  fprintf(stdout, "takes %.2f s\n", time);
  fprintf(stdout, "speed: %d MiB/s\n", (int)(fsize/time));
  fprintf(stdout, "reads %d data lines\n", count);

  return EXIT_SUCCESS;
}

ifstreamgetline()

// readline-ifstream-getline.cpp
#include <fstream>
#include <iostream>
#include <ctime>
#include <cstdlib>

using namespace std;

int main(int argc, char* argv[]) {
  clock_t t_start = clock();

  if(argc != 2) {
    fprintf(stderr, "needs one input argument\n");
    return EXIT_FAILURE;
  }

  ifstream fin(argv[1]);
  if(!fin) {
    perror("Failed to open file");
    return EXIT_FAILURE;
  }

  // maximum length of lines, determined previously by python
  const int SIZE = 1024*1024*3;
  char line[SIZE];

  int count = 0;
  while(fin.getline(line, SIZE)) {
    if(strlen(line) > 80) {
      count += 1;
    }
  }

  clock_t t_end = clock();

  const double fsize = 6685;  // file size in MiB

  double time = (t_end-t_start) / (double)CLOCKS_PER_SEC;

  fprintf(stdout, "takes %.2f s\n", time);
  fprintf(stdout, "speed: %d MiB/s\n", (int)(fsize/time));
  fprintf(stdout, "reads %d data lines\n", count);

  return EXIT_SUCCESS;
}

ifstreamread()

// seq-read-bin.cpp
// sequentially read the file to see the speed upper bound of
// ifstream

#include <iostream>
#include <fstream>
#include <ctime>

using namespace std;


int main(int argc, char* argv[]) {
  clock_t t_start = clock();

  if(argc != 2) {
    fprintf(stderr, "needs one input argument\n");
    return EXIT_FAILURE;
  }

  ifstream fin(argv[1], ios::binary);

  const int SIZE = 1024*1024*3;
  char str[SIZE];

  while(fin) {
    fin.read(str,SIZE);
  }

  clock_t t_end = clock();
  double time = (t_end-t_start) / (double)CLOCKS_PER_SEC;

  const double fsize = 6685;  // file size in MiB

  fprintf(stdout, "takes %.2f s\n", time);
  fprintf(stdout, "speed: %d MiB/s\n", (int)(fsize/time));

  return EXIT_SUCCESS;
}

使用 cat 命令,然后使用 cin.sync_with_stdio(false)cin 读取输入

#include <iostream>
#include <ctime>
#include <cstdlib>

using namespace std;

int main(void) {
  clock_t t_start = clock();

  string input_line;

  cin.sync_with_stdio(false);

  while(cin) {
    getline(cin, input_line);
  }

  double time = (clock() - t_start) / (double)CLOCKS_PER_SEC;

  const double fsize = 6685;  // file size in MiB

  fprintf(stdout, "takes %.2f s\n", time);
  fprintf(stdout, "speed: %d MiB/s\n", (int)(fsize/time));

  return EXIT_SUCCESS;
}

POSIX getline()

// readline-c-getline.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(int argc, char *argv[]) {

  clock_t t_start = clock();

  char *line = NULL;
  size_t len = 0;
  ssize_t nread;

  if (argc != 2) {
    fprintf(stderr, "Usage: %s <file>\n", argv[1]);
    exit(EXIT_FAILURE);
  }

  FILE *stream = fopen(argv[1], "r");
  if (stream == NULL) {
    perror("fopen");
    exit(EXIT_FAILURE);
  }

  int length = -1;
  int count = 0;
  while ((nread = getline(&line, &len, stream)) != -1) {
    if (nread > 80) {
      count += 1;
    }
  }

  free(line);
  fclose(stream);

  double time = (clock() - t_start) / (double)CLOCKS_PER_SEC;
  const double fsize = 6685;  // file size in MiB
  fprintf(stdout, "takes %.2f s\n", time);
  fprintf(stdout, "speed: %d MiB/s\n", (int)(fsize/time));
  fprintf(stdout, "reads %d data lines.\n", count);
  // fprintf(stdout, "length of MSA: %d\n", length-1);

  exit(EXIT_SUCCESS);
}

5
你看过这个链接吗:https://dev59.com/_mox5IYBdhLWcg3wLhei?rq=1? - Rene
4
@Olaf,你移除的标签是非常相关的,尤其是 [performance] 标签。问题中明显包含了 C++、Python 和 (纯) C 的代码。请不要仅为满足你对 [C] 和 [C++] 标签的偏见而使问题更难被找到。请注意保持标签的准确性以帮助其他人更容易地找到该问题。 - Zulan
1
此外,这应该只被标记为 C++,因为无论你在 pythonC 上有多少专业知识,都不能告诉你一个 C++ 程序员应该做什么。 - Galik
2
Olaf 在这里完全正确地删除了其他语言标签。我们不会根据问题中偶然出现的代码标记问题,而是基于期望得到的答案类型标记问题。如果您不想用 Python 回答,那么 Python 标签就不合适。 这显然是一个 C++ 问题,而且您希望用 C++ 得到答案。 如果您包含了一些伪代码,您也不会将其标记为 [伪代码],对吧? - Cody Gray
2
我必须说,在我的系统上使用可比数据,std::fgetsstd::getline 所花费的时间完全相同。 - Galik
显示剩余30条评论
3个回答

6
好的,C标准库是C++标准库的子集。根据C++ 2014标准的n4296草案:

17.2 C标准库[library.c]

C++标准库还提供了C标准库的功能,经过适当调整以确保静态类型安全。

因此,只要您在注释中解释性能瓶颈需要它,就可以在C++程序中使用fgets - 只需仔细封装它在实用类中,以保留OO高级结构即可。

我本来就预料到这篇帖子会被踩,因为它建议在C++中使用C库函数,但我天真地希望能得到一条评论... - Serge Ballesta
1
这些库也是 C++ 标准的一部分,所以我不明白为什么会被踩。 - Galik
2
我唯一不同意的部分是说“你应该仔细将其封装在实用程序类中,以保留OO高级结构。” 我不明白为什么需要将'fgets'包装在一个类中。C++ 是一种多范式语言:并不是所有东西都必须适合 OOP 范式。它不是一个紧身衣。而一个"实用程序"类并没有任何特别面向对象的东西。只有在'fgets'确实可以从对象导向中受益时才会使用包装器,而我不确定如何做到这一点。 - Cody Gray
@CodyGray: 我更喜欢将低级别的优化解耦以获得清晰的高层结构,无论是在 C 还是 C++ 中。由于 OP 要求一个 C++ 程序,我假设高层结构是面向对象的。 - Serge Ballesta
@CodyGray RAII!引用Scott Meyers的Effective C++中的话:使用对象来管理资源——资源获取即初始化(RAII)。 - Zulan
这就是为什么应该被表示为一个对象,而不是为什么fgets应该被建模为一个对象。流不仅有相关联的资源,而且相应的文件实际上是一个对象。我想你会说fgets将成为流类的成员函数,但它也可以作为自由函数正常工作。@zulan - Cody Gray

1

是的,有一种更快的方法来读取行和创建字符串。

查询文件大小,然后将其加载到缓冲区中。然后迭代缓冲区,用空字符替换换行符并存储下一行的指针。

如果您的平台很可能有一个调用将文件加载到内存中的函数,则速度会更快。


2
不清楚哪种方法更快。我的朋友为我们的叠加管理代码(Pr1me软件的VAX移植版本 - 我们谈论的是早期80年代)进行了“优化”,使用内存映射。但实际上并没有提高很多速度。他改用QIOW从磁盘读取到固定位的内存中,这样速度就快多了。你的方法将涉及两次数据通行,这可能是不能接受的高昂代价。 - Martin Bonner supports Monica

1
正如我所评论的,在Linux和POSIX系统上,您可以考虑使用getline(3); 我猜以下代码可以编译为C和C++(假设您有一些有效的fopen-ed FILE*fil; ...)
char* linbuf = NULL; /// or nullptr in C++
size_t linsiz = 0;
ssize_t linlen = 0;

while((linlen=getline(&linbuf, &linsiz,fil))>=0) {
  // do something useful with linbuf; but no C++ exceptions
}
free(linbuf); linsiz=0;

我猜这可能适用于C++(或很容易适应)。但是,要注意C++异常,它们不应该通过while循环(或者你应该确保适当的析构函数或catch正在执行free(linbuf);)。
此外,getline可能会失败(例如,如果它调用了失败的malloc),你可能需要合理地处理这种失败。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接