读取文件时使用getline与读取整个文件后基于换行符进行分割的区别

13

我现在想处理硬盘上文件的每一行。是先将整个文件加载然后基于换行符(使用boost)进行分割,还是最好使用getline()?我的问题是当调用getline()时,它是否会读取单个行(导致多次硬盘访问)或读取整个文件并逐行给出?


如果您担心I/O时间,那么这就是Google的做法:他们分析的生成文件大小可达数GB或更大。在这种情况下,顺序读取如此大的文件需要不可接受的时间,因此文件被分成较小的部分(通常为64 MB),并且并行读取。 - SChepurin
6个回答

6
getline 会在 C 库的深处调用系统调用 read()。它被调用的次数以及如何调用取决于 C 库的设计。但很可能在一次读取一个文件行和一次性读取整个文件上没有明显区别,因为底层操作系统会每次读取(至少)一个磁盘块,很可能是一个“页面”(4KB)或更多。
此外,除非您读取字符串后几乎不对其进行任何操作(例如编写类似于“grep”的程序,因此大部分时间都在查找字符串),否则逐行阅读的开销不太可能占用您花费的大部分时间。
但“一次加载整个文件”有几个不同的问题:
  1. 直到读取整个文件才会开始处理。
  2. 您需要足够的内存将整个文件读入内存 - 如果文件大小为数百GB怎么办? 那时您的程序会失败吗?
不要尝试优化某些东西,除非您使用了分析工具证明这是导致代码运行缓慢的原因之一。否则你只会给自己带来更多问题。
编辑:因此,我编写了一个程序来测量这一点,因为我认为这非常有趣。
结果确实很有趣-为了使比较公平,我创建了三个大文件,每个文件大小为1297984192字节(通过复制目录中约十几个不同源文件的所有源文件,然后将此文件多次复制“乘以”到超过1.5秒才运行测试的程度,我认为这需要执行时间足够长,以确保计时不会太容易受到随机“网络数据包”或其他外部影响而耗时)。
我还决定按进程测量系统和用户时间。
$ ./bigfile
Lines=24812608
Wallclock time for mmap is 1.98 (user:1.83 system: 0.14)
Lines=24812608
Wallclock time for getline is 2.07 (user:1.68 system: 0.389)
Lines=24812608
Wallclock time for readwhole is 2.52 (user:1.79 system: 0.723)
$ ./bigfile
Lines=24812608
Wallclock time for mmap is 1.96 (user:1.83 system: 0.12)
Lines=24812608
Wallclock time for getline is 2.07 (user:1.67 system: 0.392)
Lines=24812608
Wallclock time for readwhole is 2.48 (user:1.76 system: 0.707)

这里有三种不同的函数用于读取文件(当然还有一些测量时间等方面的代码,但为了缩小本文的大小,我选择不全部发布 - 而且我还尝试过更改顺序以查看是否有任何差异,因此上面的结果与这里的函数顺序不同)

void func_readwhole(const char *name)
{
    string fullname = string("bigfile_") + name;
    ifstream f(fullname.c_str());

    if (!f) 
    {
        cerr << "could not open file for " << fullname << endl;
        exit(1);
    }

    f.seekg(0, ios::end);
    streampos size = f.tellg();

    f.seekg(0, ios::beg);

    char* buffer = new char[size];
    f.read(buffer, size);
    if (f.gcount() != size)
    {
        cerr << "Read failed ...\n";
        exit(1);
    }

    stringstream ss;
    ss.rdbuf()->pubsetbuf(buffer, size);

    int lines = 0;
    string str;
    while(getline(ss, str))
    {
        lines++;
    }

    f.close();


    cout << "Lines=" << lines << endl;

    delete [] buffer;
}

void func_getline(const char *name)
{
    string fullname = string("bigfile_") + name;
    ifstream f(fullname.c_str());

    if (!f) 
    {
        cerr << "could not open file for " << fullname << endl;
        exit(1);
    }

    string str;
    int lines = 0;

    while(getline(f, str))
    {
        lines++;
    }

    cout << "Lines=" << lines << endl;

    f.close();
}

void func_mmap(const char *name)
{
    char *buffer;

    string fullname = string("bigfile_") + name;
    int f = open(fullname.c_str(), O_RDONLY);

    off_t size = lseek(f, 0, SEEK_END);

    lseek(f, 0, SEEK_SET);

    buffer = (char *)mmap(NULL, size, PROT_READ, MAP_PRIVATE, f, 0);


    stringstream ss;
    ss.rdbuf()->pubsetbuf(buffer, size);

    int lines = 0;
    string str;
    while(getline(ss, str))
    {
        lines++;
    }

    munmap(buffer, size);
    cout << "Lines=" << lines << endl;
}

1
I/O读取是读取文件请求的数量。如果函数内部执行的时间比函数调用时间长得多,那么你肯定不会期望函数调用的次数会(太)影响实际时间。当然,这是在Linux上测试的,Windows可能有所不同 - 因为我只有Windows虚拟机,所以我认为进行比较不太公平。您是否实际测量了所需的时间? - Mats Petersson
除非你必须以某种方式“按I/O请求付费”,但你没有在租用具有这种愚蠢策略的机器上运行,对吗? - Mats Petersson
1
所以,你实际上是测量了时间差异,而不是I/O请求的数量。我相信如果我测量I/O请求的数量,我的最慢版本将是最低的I/O请求数量 - 这是因为在读取文件时没有重叠,而在读取小块文件时会有重叠。 - Mats Petersson
在您的readwhole方法中,程序可能会等待直到完成获取。而在readline中,它不会停止,而是生成I/O请求,直到达到末尾。在您的情况下,我更喜欢每次使用更多块的readline方法,因为第一个方法的读取时间比CPU切换时间长得多。 - Arpit
没错,这正是我关于“对应用程序进行性能分析”(或找到其他测量方法)的观点。这表明在一个系统上成立的规则不一定适用于另一个系统。请注意,如果你只是读取文件,那么读取整个文件会快得多 - 在我的测试中,stringstream和每行读入字符串所花费的时间最长。 - Mats Petersson
显示剩余6条评论

3
操作系统将一整个数据块读入内存缓冲区(根据磁盘格式而定,通常每次4-8k),并为您进行部分缓冲。让操作系统替你处理它,以你的程序能够理解的方式读取数据即可。

我想处理整个文件,我的问题是关于速度,基本上哪种技术更快。 - psyche
2
你无法感觉到速度上的差异。操作系统非常擅长缓存/缓冲。只要你不在两个不同的文件之间交替访问,就不会有问题。如果你不相信我,请自行尝试。 - Floris
实际上,硬盘访问次数有点让我担心。我已经按照getline的方式编写了代码,但如果读取次数非常高,硬盘会更快损坏。所以我的问题是,使用其他技术是否会对硬盘读取次数产生任何影响?或者在硬件层面上,这是同样的事情。 - psyche
3
每个数据块将被读取一次。除非同时访问另一个文件,否则差异很小 - 现代硬件有很多缓存来提高访问速度。通常它会读取整个磁盘的“旋转”以防您需要这些其他块。硬盘相当稳健。只需使用您拥有的即可。 - Floris
1
当任务管理器谈论“读取”时,它指的是您向操作系统发出的调用 - 因此,当您要求将整个文件作为一个块读取时,它将被计算为一个“读取”。这并不反映磁盘驱动器本身发生的情况。如果我正确理解了您所说的话,getline()方法最终会更快。这很有道理 - 请参见下面@Arne Mertz的评论。使用getline()时,不会发生所有这些复制 - 正如我所说,在磁盘驱动器级别上,无论您如何读取数据,事情都已经相当优化了。我们可以认为您已经得到了您想要的答案吗? - Floris
我认为我的基准测试确实表明了这一点——即使文件的I/O请求较少,读取整个文件的速度仍然较慢。 - Mats Petersson

2

fstreams已经有了合理的缓冲区。操作系统对硬盘的底层访问也有了合理的缓冲区。硬盘本身也有一个合理的缓冲区。如果您逐行读取文件,或者逐个字符地读取文件,那么您肯定不会触发更多的硬盘访问。

因此,没有必要将整个文件加载到一个大缓冲区中并在该缓冲区上进行操作,因为它已经在缓冲区中了。通常也没有必要一次缓冲一行。为什么要分配内存来缓冲已经在ifstream中缓冲的字符串呢?如果可以的话,直接在流上工作,不要把所有东西从一个缓冲区扔到另一个缓冲区。除非它支持可读性和/或您的分析器告诉您磁盘访问显著地减慢了程序速度。


1
+1 表示非常清楚已经有足够的缓冲处理,没有必要再添加更多。并且基本上赞同我的观点。 :-) - Floris
1
如果他使用mmap映射文件,然后使用基于迭代器对的自定义字符串类来表示字符串,而不移动任何数据,则速度可能会更快。然而,这需要付出很多努力,除非真的非常必要,否则我不会费心去做。 - James Kanze
是的,我的基准测试显示mmap比其他替代方案更快。 - Mats Petersson

1
我认为C++的惯用法是逐行读取文件,并在读取文件时创建基于行的容器。最有可能的是iostreams(getline)将被缓冲,以至于您不会注意到显着的差异。
然而,对于非常大的文件,通过读取文件的较大块(而不是一次性读取整个文件)并在发现换行符时进行拆分,您可能会获得更好的性能。
如果您想具体知道哪种方法更快,以及速度如何,您将不得不对您的代码进行性能分析。

1

如果可以容纳在内存中,最好获取所有数据,因为每当您请求I/O时,程序会失去处理并放入等待队列。

enter image description here

然而,如果文件大小很大,那么最好一次读取所需处理的尽可能多的数据。因为较大的读操作将比较小的读操作花费更多时间来完成。CPU进程切换时间远小于整个文件读取时间。


定义“更好”...虽然从技术上讲,减少了开销,但如果你需要读取一个8GB的文本文件到缓冲区中,然后进行处理,总运行时间可能比读取几百字节的块并处理每个元素要长得多。但这也可能不是这种情况。这完全取决于您在处理中做什么以及操作系统如何缓存事物。但我认为我们不能断言哪种方法“更好”。 - Mats Petersson
实际上,我并没有考虑这些大文件。因为从问题中无法确定。 - Arpit
它最多只能达到2GB,不能超过这个大小。 - psyche
然后使用缓冲读取。每次读取4行或适合您的数量。 - Arpit
我没有尝试过“一次读取四行”,但请参见下文,了解以各种方式读取大文件的结果。请说明您的方法(“readwhole”)为什么更好?[或者如果我做错了,请说明您会如何不同地处理-我不是stringstream类型工作的专家,所以可能弄错了] - Mats Petersson

0
如果它是一个小文件,从磁盘读取整个文件并逐行解析可能比一次读取一行更有效率——因为那样会涉及到很多磁盘访问。

在这种情况下,显然你不想将整个文件读入内存(而且它很可能分布在磁盘上)。我会选择Floris的建议,选择最适合你代码的方法,让操作系统为你优化磁盘访问。 - Ray
1
@PragneshPatel 当然,这仅适用于您只访问一次的情况。如果您多次处理文件,则将其全部保存在内存中可能非常值得。 - Ray

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