如何加快逐行读取ASCII文件的速度? (C ++)

时间:2011-03-02 07:21:50

标签: c++ optimization file-io ifstream

以下是一些代码,在进行一些测量后是一个相当大的瓶颈:

//-----------------------------------------------------------------------------
//  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行终止的文本字符串。

编辑:后续问题,了解建议的替代方案: 任何替代方案(减去增加流缓冲区大小)是否意味着我编写了一个解析每个字符的解析器?我有点像流的简单语法,但如果我需要速度,我可以重新写一些更细节的东西。将整个文件读入内存是一个可行的选择,它只有2mb左右。

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

9 个答案:

答案 0 :(得分:8)

在我的系统上快速分析(linux-2.6.37,gcc-4.5.2,用-O3编译)表明I / O不是瓶颈。无论是使用fscanf进入char数组,然后使用dict.insert()还是operator>>,如同您的确切代码一样,它需要大约相同的时间(读取240k字文件需要155 - 160 ms)。 / p>

在代码中用std::unordered_set替换gcc的std::vector<std::string>会将执行时间减少到45毫秒(fscanf) - 55毫秒(operator>>)。尝试分析IO并单独设置插入。

答案 1 :(得分:4)

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

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

char LocalBuffer[4096]; // buffer

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

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

注意:如果rdbuf的构造成功

,则ifstream的结果保证不为空

根据可用内存的不同,强烈建议您尽可能扩大缓冲区,以限制与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;
}

答案 2 :(得分:2)

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

0.25秒实际上是个问题吗?如果你不打算加载更大的文件是否需要让它更快,如果它使它更不易读?

答案 3 :(得分:2)

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

最大的区别在于C ++流基于语言环境进行大量操作。角色转换/标点符号等等。

因此,C库会更快。

替换死链接

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


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

对于溪流:

区域设置。

很少有人真正费心去学习如何正确设置和/或操纵流的区域设置。

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

示例:

  • 您是否知道语言环境会改变'。'自动以十进制数字表示任何其他字符。
  • 您是否知道语言环境会添加','每三位数字以便于阅读。
  • 您是否知道locales可用于操作文本(即从UTF-16转换为UTF-8(写入文件时)。

示例:

答案 4 :(得分:2)

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

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

从时间复杂的角度来看:

  • 最糟糕的情况是你的程序复杂性:O(n * n)= O(n [读取数据] * n [插入到unordered_set])
  • 您的程序复杂性的平均情况:O(n)= O(n [读取数据] * 1 [插入到unordered_set])

实用技巧:

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

我的测量结果:

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

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

此外,我没有省略那些包含大量上下文切换等的测量。因此,有足够的空间来做更好的测量。

我的代码:


14-16 ms 从文件中读取&amp;将数据插入 2D字符数组(读取和插入)n次

65-75 ms 使用二进制搜索搜索所有字词(搜索n次):

总计= 79-91 ms


61-78 ms 从文件中读取&amp;将数据插入 unordered_set字符数组(读取和插入)n次

7-9 ms 按哈希搜索n次

总计= 68-87 ms


如果您搜索的次数比插入的次数多,则选择哈希表(unordered_set),否则使用二进制搜索(使用简单数组)。


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

使用-O2编译: 157-182 ms

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

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


您可以做的最简单,但使用当前代码更好?

  1. [更好地使用高级API]使用“getline(wordListFile,word)”而不是“wordListFile&gt;&gt; word”。另外我认为“getline”比“&gt;&gt;”更具可读性操作者。
  2. 使用-O2编译: 142-170 ms。与原始代码相比,速度提升~12-15 ms

    使用-O0编译(如果省略-O标志,默认情况下“-O”级别也为0): 213-235 ms。与原始代码相比,速度提升~10-13 ms

    1. [更好地使用高级API]避免与“dict.reserve(XXXXXX);”,@ DavidRodríguez - dribeas也提到它有关。如果您的字典是静态的,或者您可以猜测字典大小(例如,文件大小除以平均字长)。首先运行没有“保留”并输出bucket_count(cout&lt;&lt;“bucket_count =”&lt;&lt; dict.bucket_count()&lt;&lt;“\ n”;;),在我的情况下它是299951.然后我添加了“ dict.reserve(299951);”
    2. 使用-O2编译: 99-121- [137] ms。 〜33-43- [49] ms速度提升与原始代码相比。

      你可以做些什么来提高速度呢?

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


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


      您是否可以提供有关您计划优化的系统(一个或多个系列)的更多详细信息?

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

      我的回答是否与任何内容相关?请添加评论或投票,因为我看到没有PM。

答案 5 :(得分:1)

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

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

答案 6 :(得分:0)

信不信由你,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”以消除两种情况之间的差异。时间结果没有改变。

编辑:我认为这个答案中的兴趣量(和downvotes)显示了与现实相反的流行观点。人们无法相信在C和流中读取输入的简单情况可能会如此不同。在你投票之前:去自己测量一下。关键不是设置大量的状态(没有人通常设置),而只是人们最常写的代码。意见在绩效上没有任何意义:衡量,衡量,衡量是最重要的。

答案 7 :(得分:0)

不幸的是,在使用fstream时,你无法提高性能。

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

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

答案 8 :(得分:0)

如果你真的想快,ditch istream和string,并在const char*附近创建一个简单的类Read_Only_Text&amp; size,然后内存映射文件并插入unordered_set<Read_Only_Text>并引用嵌入的字符串。它意味着你不必要地保留2mb文件,即使你的唯一键的数量可能要少得多,但填充它会非常非常快。我知道这是一种痛苦,但我已经多次完成各种任务,结果非常好。