【问题标题】:Optimize file reading C++优化文件读取 C++
【发布时间】:2014-01-25 10:11:23
【问题描述】:
string str1, str2;
vector<string> vec;

ifstream infile;

infile.open("myfile.txt");
while (! infile.eof() )
{
    getline(infile,str1);
    istringstream is;
        is >> str1;
    while (is >> str2)
    { 
        vec.push_back(str2);
    }
} 

代码所做的是从文件中读取字符串并将其存储到向量中。

性能 需要优先考虑。我该如何优化这段代码,以让阅读性能更快?

【问题讨论】:

标签: c++ file-io


【解决方案1】:

正如其他人已经指出的那样(例如参见herohuyongtao's answer),必须修复循环条件以及如何将str1 放入istringstream

然而,这里有一个到目前为止大家都忽略的重要问题:你根本不需要istringstream

  vec.reserve(the_number_of_words_you_exptect_at_least);

  while (infile >> str1) {

    vec.push_back(str1);
  }

它摆脱了您最初不需要的内部循环,并且不会在每次迭代中创建istringstream

如果您需要进一步解析每一行并且确实需要istringstream,请在循环之外创建它,并通过istringstream::str(const string&amp; s) 设置其字符串缓冲区。

我可以很容易地想象您的循环非常慢:Windows 上的堆分配非常慢(与 Linux 相比);我被咬过一次。

Andrei Alexandrescu 在他的演讲 Writing Quick Code in C++, Quickly 中提出(在某种意义上)一个类似的例子。令人惊讶的是,像上述那样在紧密循环中进行不必要的堆分配可能比实际的文件 IO 慢。我很惊讶地看到这一点。


您没有将您的问题标记为 C++11,但这是我将在 C++11 中执行的操作。

  while (infile >> str1) {

    vec.emplace_back(std::move(str1));
  }

这个移动构造了向量后面的字符串,没有复制进去。我们可以这样做,因为我们不需要str1的内容,因为我们已经把它放入向量中。换句话说,不需要将它复制到向量后面的全新字符串中,只需将其内容移动到那里就足够了。 vec.push_back(str1); 的第一个循环可能可能会复制 str1 的内容,这确实是不必要的。

gcc 4.7.2 中的字符串实现目前为copy on write,因此两个循环的性能相同;您使用哪一个并不重要。暂时。

不幸的是,标准现在禁止在写入时复制字符串。我不知道 gcc 开发人员何时会更改实现。如果实现发生变化,无论您是移动 (emplace_back(std::move(s))) 还是复制 (push_back(s)),性能都可能会有所不同。

如果 C++98 兼容性对您很重要,请选择 push_back()。即使将来发生最糟糕的事情并且您的字符串被复制(现在没有被复制),该副本也可以转换为 memmove() / memcpy() 这非常快,很可能比读取内容更快来自硬盘的文件,因此文件 IO 很可能仍然是瓶颈。

【讨论】:

  • +1 - 对很多人来说很可能是en.wikipedia.org/wiki/Golden_mean_%28philosophy%29
  • 我的文件包含一大堆数据,我不知道要保留多少,我该怎么做?
  • @aayat 然后给出一个合理的估计你期望的金额至少。如果这个估计值太低,不会发生任何灾难性的事情,向量会自动调整大小;我不会那么担心这个。您使用的是哪个编译器,gcc、Visual Studio 还是其他?
  • @aayat 好的。请阅读更新的答案。它涉及 gcc 实现,与您直接相关。
  • @Ali 正如你所提到的,我可以在循环之外创建 istringstream 并使用 istringstream::str(str1);当它再次循环时,字符串缓冲区不会改变
【解决方案2】:

在进行任何优化之前,您需要进行更改

while (! infile.eof() )      // problem 1
{
    getline(infile,str1);
    istringstream is;
        is >> str1;          // problem 2
    while (is >> str2){ 
        vec.push_back(str2);
        }
 }

while ( getline(infile,str1) ) // 1. don't use eof() in a while-condition
{
    istringstream is(str1);    // 2. put str1 to istringstream
    while (is >> str2){ 
        vec.push_back(str2);
        }
 }

让它按您的预期工作。


附:对于优化部分,你不需要考虑太多,除非它成为瓶颈。 Premature optimization is the root of all evil。但是,如果您确实想加快速度,请查看@Ali 的答案以获取更多信息。

【讨论】:

  • @herohuyongtao 我不是反对者,但我反对者是因为遗漏了一个重要细节。请阅读我的回答。
  • @Ali 是的,你可能是对的。但是,我仍然怀疑这将有助于加快速度。
  • @herohuyongtao Andrei Alexandrescu 在他的演讲Writing Quick Code in C++, Quickly 中提出了一个类似的例子。令人惊讶的是,在紧密循环中进行不必要的堆分配可能会比实际的文件 IO 慢。我也很惊讶。
  • @Ali 我会调查的。感谢分享。
  • @herohuyongtao +1 用于修复 OP 中的错误并正确引用其他优化机会而不是窃取它们(不幸的是,并非每个人都这样做)。
【解决方案3】:

Loop condition is wrong. 不是性能问题。假设这个 IO 循环确实是您的应用程序的瓶颈。但即使没有,它也可以是一个很好的教育练习或只是一个周末的乐趣。

你在循环中有很多临时的和动态内存分配的案例。

在循环前调用std::vector::reserve() 会有所改善。手动重新分配它以模拟 x1.2 增长因子而不是 2x 在一定大小后也会有所帮助。如果文件大小不可预测,std::list 可能更合适。

使用std::istringstream 作为分词器非常不理想。切换到基于迭代器的“视图”标记器 (Boost has one) 应该会大大提高速度。

如果您需要它非常 快并且有足够的 RAM,您可以在读取文件之前对其进行内存映射。 Boost::iostreams 可以让你快速到达那里。不过,一般来说,如果没有 Boost,你的速度可以提高一倍(Boost 还不错,但它必须是通用的,并且可以在十几个编译器上工作,这就是原因)。

如果你是一个幸运的人,使用 Unix/Linux 作为你的开发环境,在valgrind --tool=cachegrind 下运行你的程序,你会看到所有有问题的地方以及它们相对于彼此的严重程度。此外,valgrind --tool=massif 将让您识别大量的小堆分配对象,这在高性能代码中通常是无法容忍的。

【讨论】:

  • 当然,正如阿里指出的那样,根本不需要分词器,因为这些行对结果没有任何意义。但总的来说,boost 的链接很有用,valgrind 的链接更有用。
  • @JanHudec - 同意,我赞成 Ali 的回答,但我不认为我在回答中分享的有效标记字符串的技巧是浪费此页面上的空间
【解决方案4】:

最快但不是完全可移植的方法是将文件加载到内存映射区域(参见 wiki mmap

鉴于您知道文件的大小,您现在可以在该内存区域上定义前向迭代器(可能是指向 const char 的指针),您可以使用它来查找将您的文件分成“字符串”的标记。

本质上,您反复得到一对指针,分别指向每个“字符串”的末尾的第一个字符。从这对迭代器中创建您的std::string

这种方法有一些微妙的问题:

  • 您需要注意文件的字符编码,可能将此字符编码转换为您的std::string 使用的所需编码(可能是UTF-8)。

    李>
  • 分隔字符串的“令牌”(通常为\n,可能取决于平台,也可能取决于创建文件的程序。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-25
  • 1970-01-01
  • 1970-01-01
  • 2023-03-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多