【问题标题】:How can I speed up line by line reading of an ASCII file? (C++)如何加快逐行读取 ASCII 文件的速度? (C++)
【发布时间】:2011-07-07 01:52:32
【问题描述】:

在进行一些测量后,这里有一段代码是一个相当大的瓶颈:

//-----------------------------------------------------------------------------
//  Construct dictionary hash set from dictionary file
//-----------------------------------------------------------------------------
void constructDictionary(unordered_set<string> &dict)
{
    ifstream wordListFile;
    wordListFile.open("dictionary.txt");

    std::string word;
    while( wordListFile >> word )
    {
        if( !word.empty() )
        {
            dict.insert(word);
        }
    }

    wordListFile.close();
}

我正在阅读大约 200,000 个单词,这在我的机器上大约需要 240 毫秒。这里使用ifstream高效吗?我能做得更好吗?我正在阅读有关 mmap() 实现的信息,但我并没有 100% 理解它们。输入文件只是带有 *nix 行终止的文本字符串。

编辑:后续问题建议的替代方案: any 替代方案(减去增加流缓冲区大小)是否意味着我编写了一个解析器来检查每个字符的换行符?我有点喜欢流的简单语法,但如果我必须为了速度,我可以重写一些更具体的东西。将整个文件读入内存是一个可行的选择,它只有大约 2mb。

编辑#2:我发现我的速度变慢是由于设置插入,但是对于那些仍然对逐行加速文件 IO 感兴趣的人,请阅读在这里回答并查看Matthieu M.'s continuation on the topic.

【问题讨论】:

  • 关于读取的最大收获是使用大缓冲区,这样您就不必为每一行(或小缓冲区)遍历所有 O/S 层,也不会重复支付性能开销罚款。不知道增加 ifstream 的读取缓冲区大小的 API 是什么,这就是为什么这是评论,而不是答案。但我确信有一种方法可以调整缓冲区的大小(或分配你自己的,指定它的大小)。
  • 当性能出现问题时,您应该做的第一件事是profile。看看你的代码大部分时间都花在了哪里。据我们所知,这可能是在将值添加到您的字典中。不太可能,我知道,但如果你需要性能,你真的需要知道。
  • @sbi:这不太可能。在高性能统计应用程序中,我发现 unordered_map 的 Boost 版本比 Google 的 sparsehash 慢一个数量级,并且比 GNU std::map 快不了多少。
  • @sbi 和@larsmans:你们是对的,请参阅已接受的答案。

标签: c++ optimization file-io ifstream


【解决方案1】:

在我的系统(linux-2.6.37,gcc-4.5.2,使用 -O3 编译)上的快速分析表明 I/O 不是瓶颈。无论是在 char 数组中使用 fscanf 后跟 dict.insert() 还是 operator&gt;&gt; 就像在您的确切代码中一样,都需要大约相同的时间(读取 240k 字文件需要 155 - 160 毫秒)。

在代码中将 gcc 的 std::unordered_set 替换为 std::vector&lt;std::string&gt; 会将执行时间降低到 45 ms (fscanf) - 55 ms (operator&gt;&gt;)。尝试分别分析 IO 和设置插入。

【讨论】:

  • 我正在使用 unordered_set 在我的代码的另一部分加快查找字典的速度。但是,是的,这确实是瓶颈。我现在惊呼小溪是瓶颈还为时过早......
【解决方案2】:

通常,您可以通过增加缓冲区大小来获得更好的性能。

在构建ifstream 之后,您可以使用以下方法设置其内部缓冲区:

char LocalBuffer[4096]; // buffer

std::ifstream wordListFile("dictionary.txt");

wordListFile.rdbuf()->pubsetbuf(LocalBuffer, 4096);

注意:如果ifstream 的构造成功,rdbuf 的结果保证不为空

根据可用内存,强烈建议您尽可能增加缓冲区,以限制与 HDD 的交互和系统调用的数量。

我使用自己的一个小基准进行了一些简单的测量,您可以在下面找到代码(我对批评者很感兴趣):

SLES 10 (sp 3) 上的 gcc 3.4.2
C:9.52725e+06
C++:1.11238e+07
差异:1.59655e+06

这会导致 17% 的减速。

这考虑到:

  • 自动内存管理(无缓冲区溢出)
  • 自动资源管理(没有忘记关闭文件的风险)
  • 处理locale

因此,我们可以争辩说流很慢...但是请不要乱扔代码并抱怨它很慢,优化是一项艰巨的工作。


相应的代码,其中benchmark 是我自己的一个小实用程序,它使用gettimeofday 测量重复执行(这里启动50 次迭代)的时间。

#include <fstream>
#include <iostream>
#include <iomanip>

#include <cmath>
#include <cstdio>

#include "benchmark.h"

struct CRead
{
  CRead(char const* filename): _filename(filename) {}

  void operator()()
  {
    FILE* file = fopen(_filename, "r");

    int count = 0;
    while ( fscanf(file,"%s", _buffer) == 1 ) { ++count; }

    fclose(file);
  }

  char const* _filename;
  char _buffer[1024];
};

struct CppRead
{
  CppRead(char const* filename): _filename(filename), _buffer() {}

  enum { BufferSize = 16184 };

  void operator()()
  {
    std::ifstream file(_filename);
    file.rdbuf()->pubsetbuf(_buffer, BufferSize);

    int count = 0;
    std::string s;
    while ( file >> s ) { ++count; }
  }

  char const* _filename;
  char _buffer[BufferSize];
};


int main(int argc, char* argv[])
{
  size_t iterations = 1;
  if (argc > 1) { iterations = atoi(argv[1]); }

  char const* filename = "largefile.txt";

  CRead cread(filename);
  CppRead cppread(filename);

  double ctime = benchmark(cread, iterations);
  double cpptime = benchmark(cppread, iterations);

  std::cout << "C  : " << ctime << "\n"
               "C++: " << cpptime << "\n";

  return 0;
}

【讨论】:

  • “所以,我们可以争辩说流很慢……但是请不要乱扔代码并抱怨它很慢,优化是一项艰苦的工作。” Matthieu 的观点是,您必须通过“艰苦的工作”才能使 C++ 版本合理地执行,单行读取循环的比较最终表现出如此巨大的不同是多么令人惊讶。不过,很高兴了解缓冲。
  • 有趣的是,在我的 100 万个“howdythere”文件测试用例中,添加缓冲几乎没有什么区别,而 C 版本的速度仍然快 5 倍左右。
  • 通过增加缓冲区大小,您可以减少读取文件所需的用户/内核交互次数。这也可以在 C 中通过 setbuffer 调用 afaik 来完成。
  • @sylvainulg:好吧,如果你要读取一个大文件,增加缓冲区大小似乎是合理的。感谢 C 提示,我不知道。
  • "强烈建议您增加缓冲区以限制与 HDD 的交互。4096 对应于大多数 HDD 扇区的大小。" -- 从技术上讲,HDD 扇区是 512 字节,只有操作系统倾向于将它们分组为 4K 块,以便更容易匹配一个虚拟内存页面。您的整个轨道,而不仅仅是一个扇区,有可能在操作系统级别被乐观地缓存。直到轨道的大小(先验未知),您所观察到的只是通过减少系统调用而不是与磁盘的交互来改进。所以是的,你的建议对要做的事情和取得的效果是明智的,但不准确
【解决方案3】:

将整个文件一次性读取到内存中,然后对其进行操作可能会更快,因为它避免了重复返回磁盘读取另一个块。

0.25s 真的有问题吗?如果您不打算加载更大的文件,如果它降低了可读性,是否需要使其更快?

【讨论】:

  • 提前读取整个文件并不总是一个可行的选择。使用纯 C 文件 I/O 进行逐行输入比使用 c++ 流快得多,至少在我上次测量时是这样。与直觉相反,但测量为王。
  • 同意,但 OP 没有提到任何需要担心的内存限制。
  • 这是典型的内存与速度权衡。因此,一种选择是尽量减少 I/O 操作的数量,只需将内容一次读入一个字符串,然后将该字符串转换为一组无序的单词。
【解决方案4】:

C++ 和 C 库以同样快的速度从磁盘读取内容,并且已经缓冲以补偿磁盘 I/O 延迟。您不会通过添加更多缓冲来使其更快。

最大的不同是 C++ 流会根据语言环境进行大量操作。字符转换/标点符号等/等等。

因此,C 库会更快。

替换死链接

由于某种原因,链接的问题已被删除。 所以我把相关信息移到这里。链接的问题是关于 C++ 中的隐藏功能。


虽然在技术上不是 STL 的一部分。
流库是标准 C++ 库的一部分。

对于流:

语言环境。

很少有人真正费心去学习如何正确设置和/或操作流的语言环境。

第二个最酷的东西是迭代器模板。
对我来说最特别的是流迭代器,它基本上将流转换为非常基本的容器,然后可以与标准算法结合使用。

例子:

  • 您是否知道语言环境会更改“.”以十进制数字自动转换为任何其他字符。
  • 您知道吗?语言环境会每三个数字添加一个“,”以方便阅读。
  • 您知道吗?语言环境可用于在传递过程中操作文本(即从 UTF-16 转换为 UTF-8(写入文件时)。

等等

例子:

【讨论】:

  • wrt locale --> 是否也以二进制模式激活?在我的基准测试中,我没有看到二进制和非二进制之间的性能差异。
  • @Matthieu M:好问题。我刚刚浏览了 DevStudio 中的代码,是的。二进制指定不影响语言环境的使用。它唯一要做的就是改变行尾的处理方式(这不是语言环境的一部分)。
  • 谢谢 :) 这与我的观察一致。你知道是否有办法“禁用”这个语言环境?语言环境的问题是所有这些方面的操作等......我可以看到一种注入新语言环境的方法,但我看不到任何方法可以完全删除它,而且我不确定放置一个没有任何方面的语言环境实际上会改善性能很多。
  • @Matthieu M:不,我不知道如何禁用它们。但是 codecvt facet 有一个方法(always_noconv),它指示是否需要调用其他方法。我认为其他方面类型具有在什么都不做时限制虚拟调用数量的技术。
【解决方案5】:

我的系统(3.2.0-52-generic,g++-4.7 (Ubuntu/Linaro 4.7.3-2ubuntu1~12.04) 4.7.3,用-O2编译如果没有指定,CPU: i3-2125)

在我的测试用例中,我使用了 295068 个单词的字典(因此,比你的字典多了 100k 个单词):http://dl.dropboxusercontent.com/u/4076606/words.txt

从时间复杂度的角度来看:

  • 程序复杂度最差的情况:O(n*n)=O(n[read data]*n[insert into unordered_set])
  • 程序复杂度的平均情况:O(n)=O(n[read data]*1[insert into unordered_set])

实用技巧:

  • 大多数简单的数据结构具有较少的时间开销。简单数组比向量快。 char 数组比字符串快。网络上有很多关于它的信息。

我的测量结果:

注意:我没有刷新我的操作系统缓存和硬盘缓存。最后一个我无法控制,但第一个可以控制:

sync; sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'

我也没有忽略那些包含大量上下文切换等的测量。因此,还有空间进行更好的测量。

我的代码(从文件中读取并插入数据;搜索所有单词):


14–16 ms 从文件中读取并将数据插入到 2D 字符数组(读取和插入)n 次

65-75 ms二分搜索搜索所有单词(搜索n次):

总计=79-91 毫秒


61-78 ms 从文件中读取并将数据插入到 unordered_set char 数组(读取和插入)n 次

7-9 ms按哈希 n 次搜索

总计=68-87 毫秒


如果搜索次数多于插入次数,请选择哈希表 (unordered_set),否则选择二分搜索(使用简单数组)。


您的原始代码(搜索和插入):

使用 -O2 编译:157-182 毫秒

使用-O0编译(如果省略-O标志,“-O”级别默认也是0):223-248 ms

因此,编译器选项也很重要,在这种情况下,它意味着 66 毫秒的速度提升。您没有指定其中任何一个。所以,我最好的猜测是你没有使用它。当我试图回答你的主要问题时。


你可以用你当前的代码做最简单但更好的事情?

  1. [更好地使用高级 API] 使用“getline(wordListFile, word)”而不是“wordListFile >> word”。另外我认为“getline”比“>>”操作符更具可读性。

使用 -O2 编译:142-170 毫秒。 ~ 12-15 毫秒的速度提升与您的原始代码相比。

使用 -O0 编译(如果省略 -O 标志,“-O”级别默认也是 0):213-235 ms。与您的原始代码相比,速度提高了 ~ 10-13 毫秒

  1. [更好地使用高级 API] 避免使用“dict.reserve(XXXXXX);”重新散列,@David Rodríguez - dribeas 也提到了它。如果您的字典是静态的,或者您可以猜测字典大小(例如文件大小除以平均字长)。第一次运行没有“reserve”并输出bucket_count(cout

使用 -O2 编译:99-121-[137] 毫秒。 ~ 33-43-[49] ms 速度提升与您的原始代码相比。

您可以采取哪些更高级的措施来加快速度?

为您的特定数据输入实现您自己的哈希函数。使用 char 数组而不是 STL 字符串。完成之后,才可以使用直接 OS I/O 编写代码。正如您所注意到的(从我的测量中也可以看出)数据结构是瓶颈。如果媒体速度很慢,但 CPU 速度很快,请在程序中解压缩文件。


我的代码并不完美,但仍然比上面可以看到的任何东西都要好:http://pastebin.com/gQdkmqt8(哈希函数来自网络,也可以做得更好)


您能否提供更多关于您计划优化哪个系统(一个或范围)的详细信息?

时间复杂度信息: 应该是链接......但我没有那么多的声誉,因为我是stackoverflow的初学者。

我的回答仍然与任何事情相关吗?请添加评论或投票,因为我没有看到任何 PM。

【讨论】:

    【解决方案6】:

    IO 库的正确实现将为您缓存数据,避免过多的磁盘访问和系统调用。我建议您使用系统调用级别的工具(例如,如果您在 Linux 下使用 strace)来检查您的 IO 实际发生了什么。

    显然,dict.insert(xxx) 如果不允许 O(1) 插入,也可能是个麻烦事。

    【讨论】:

    • 'dict' 是一个 unordered_set(散列集),所以它应该是 O(1) 插入时间。
    • 我会调查您的 strace 建议。谢谢。
    • 重点不是“正确”实现会做什么,而是现有实现实际上会做什么。在 C++ 流中逐行读取显然比在 C i/o 中执行相同的操作要慢得多。
    • @Jon:摊销 O(1) 并不意味着它不能变得更快。特别是,在创建unordered_set 期间添加大小提示可以避免中间内存分配(这很昂贵)和复制(从旧分配的集到新分配的集)。虽然理论上无论有无提示,它的插入都是 O(1) 操作,但在现实生活中,性能可能会受到此影响。
    【解决方案7】:

    很遗憾,在使用 fstream 时,您无法提高性能。

    您可以通过读取较大的文件块然后解析单个单词来获得非常轻微的速度提升,但这取决于您的 fstream 实现如何进行缓冲。

    获得重大改进的唯一方法是使用操作系统的 I/O 功能。例如,在 Windows 上,使用 FILE_FLAG_SEQUENTIAL_SCAN 标志打开文件可能会加快读取速度,以及使用异步读取从磁盘抓取数据并并行解析。

    【讨论】:

      【解决方案8】:

      如果你真的想要快速,放弃 istream 和字符串,并在 const char*size 周围创建一个普通类 Read_Only_Text,然后内存映射文件并插入到 unordered_set&lt;Read_Only_Text&gt; 并引用嵌入的字符串。这将意味着您不必要地保留 2mb 文件,即使您的唯一密钥数量可能要少得多,但填充速度会非常非常快。我知道这很痛苦,但我已经为各种任务做了几次,结果非常好。

      【讨论】:

        【解决方案9】:

        信不信由你,stdlib 流在读取数据方面的性能远低于 C 库例程。如果您需要顶级 IO 读取性能,请不要使用 c++ 流。我在算法竞赛网站上发现了这一点——我的代码使用 c++ 流读取标准输入时会遇到测试超时,但使用纯 C FILE 操作会在很长一段时间内完成。

        编辑:只需在一些示例数据上尝试这两个程序。我在 Mac OS X 10.6.6 上使用 g++ i686-apple-darwin10-g++-4.2.1 (GCC) 4.2.1 (Apple Inc. build 5664) 在一个包含 100 万行“howdythere”的文件上运行它们,并且scanf 版本的运行速度始终比 cin 版本快 5 倍:

        #include <stdio.h>
        
        int main()
        {
            int count = 0;
            char buf[1024];
            while ( scanf("%s", buf) == 1 )
                ++ count;
        
            printf( "%d lines\n", count );
        }
        

        #include <iostream>
        
        int main()
        {
            char buf[1024];
            int count = 0;
        
            while ( ! std::cin.eof() )
            {
                std::cin.getline( buf, 1023 );
                if ( ! std::cin.eof() )
                    ++count;
            }
           std::cout << count << " lines" << std::endl;
        }
        

        编辑:将数据文件更改为“howdythere”以消除两种情况之间的差异。计时结果没有变化。

        编辑:我认为这个答案的兴趣(和反对票)的数量表明现实与流行的观点有多么相反。人们简直不敢相信在 C 和流中读取输入的简单情况会如此不同。在你投反对票之前:自己去衡量吧。关键不是设置大量的状态(通常没有人设置),而只是人们最常编写的代码。意见对绩效没有任何意义:衡量、衡量、衡量才是最重要的。

        【讨论】:

        • 这不一定适用于所有情况。例如,在我参加的算法比赛中,我必须读取大量整数,而 c++ 流实际上比 c i/o 运行得更快。它在很大程度上取决于编译器、系统和 i/o 的类型。
        • 你在问谁?如果你问我,那我已经回答过了。 (编译器、系统和 i/o 类型的差异)
        • 我对流有类似的经验,所以我至少会考虑重写这个函数。我几乎可以肯定,将整个文件读入缓冲区而不使用流将轻松胜过当前的 OP 解决方案。
        • @quasiverse 我发现这些结果在不同的操作系统和编译器中非常一致。我很想看看你的代码中流的性能优于 C i/o,因为我还没有发现这种情况。
        • @Bogatyr:您是否尝试增加流缓冲区?因为与 C 版本不同,您的 C++ 版本根本不保证(从磁盘)一次读取(从磁盘)1023 字节......即使您只看到那个大小的块。
        猜你喜欢
        • 2016-01-24
        • 1970-01-01
        • 2015-11-07
        • 1970-01-01
        • 2018-03-26
        • 2014-06-21
        • 2012-06-13
        • 1970-01-01
        • 2019-08-13
        相关资源
        最近更新 更多