C ++标准是否要求iostream的性能很差,或者我只是处理一个糟糕的实现?

每当我提到C ++标准库iostream的性能下降时,我都会有一阵怀疑的感觉。 然而,我的profiler结果显示在iostream库代码中花费了大量的时间(完全编译器优化),并且从iostreams切换到特定于OS的I / O API和自定义缓冲区pipe理确实给了一个数量级的改进。

C ++标准库在做什么额外的工作,这是标准所要求的,在实践中是否有用? 还是做一些编译器提供与手动缓冲区pipe理相竞争的iostream的实现?

基准

为了让问题得以解决,我写了一些简短的程序来实现iostream的内部缓冲:

  • 把二进制数据放入ostringstream http://ideone.com/2PPYw
  • 把二进制数据放入char[]缓冲区http://ideone.com/Ni5ct
  • 使用back_inserter http://ideone.com/Mj2Fi将二进制数据放入一个vector<char>
  • vector<char>简单迭代器http://ideone.com/9iitv
  • 新增function :将二进制数据直接放入stringbuf http://ideone.com/qc9QA
  • vector<char>简单的迭代器加边界检查http://ideone.com/YyrKy

请注意, ostringstreamstringbuf版本运行较less,因为它们速度较慢。

在ideone上, ostringstreamstd:copy + back_inserter + std::vector慢大约3倍,比memcpy慢大约15倍。 当我将我的真实应用程序切换到自定义缓冲时,这与之前和之后的分析感觉一致。

这些都是内存中的缓冲区,所以iostreams的缓慢不能归咎于慢速磁盘I / O,太多的冲洗,与stdio同步,或任何其他的事情人们用来原谅慢C ++标准库iostream的。

能够看到其他系统的基准testing以及常见实现的评论(如gcc的libc ++,Visual C ++,Intel C ++)以及标准要求的开销是多less。

这个testing的基本原理

许多人正确地指出,iostream更常用于格式化输出。 但是,它们也是二进制文件访问的C ++标准提供的唯一现代API。 但是,对内部缓冲进行性能testing的真正原因适用于典型的格式化I / O:如果iostream不能保持磁盘控制器提供的原始数据,那么当它们负责格式化时,它们如何保持可用?

基准时间

所有这些都是外部( k )循环的迭代。

关于ideone(gcc-4.3.4,未知操作系统和硬件):

  • ostringstream :53毫秒
  • stringbuf :27毫秒
  • vector<char>back_inserter :17.6 ms
  • vector<char>与普通的迭代器:10.6毫秒
  • vector<char>迭代器和边界检查:11.4 ms
  • char[] :3.7 ms

在我的笔记本电脑上(Visual C ++ 2010 x86, cl /Ox /EHsc ,Windows 7 Ultimate 64位,Intel Core cl /Ox /EHsc RAM):

  • ostringstream :73.4毫秒,71.6毫秒
  • stringbuf :21.7毫秒,21.3毫秒
  • vector<char>back_inserter :34.6 ms,34.4 ms
  • vector<char>与普通迭代器:1.10 ms,1.04 ms
  • vector<char>迭代器和边界检查:1.11 ms,0.87 ms,1.12 ms,0.89 ms,1.02 ms,1.14 ms
  • char[] :1.48 ms,1.57 ms

Visual C ++ 2010 x86,configuration文件引导优化cl /Ox /EHsc /GL /clink /ltcg:pgi ,run, link /ltcg:pgo ,measure:

  • ostringstream :61.2ms,60.5ms
  • vector<char>与普通的迭代器:1.04毫秒,1.03毫秒

相同的笔记本电脑,相同的操作系统,使用cygwin gcc 4.3.4 g++ -O3

  • ostringstream :62.7毫秒,60.5毫秒
  • stringbuf :44.4毫秒,44.5毫秒
  • vector<char>back_inserter :13.5 ms,13.6 ms
  • 使用普通迭代器的vector<char> :4.1 ms,3.9 ms
  • vector<char>迭代器和边界检查:4.0 ms,4.0 ms
  • char[] :3.57 ms,3.75 ms

同样的笔记本电脑,Visual C ++ 2008 SP1, cl /Ox /EHsc

  • ostringstream :88.7毫秒,87.6毫秒
  • stringbuf :23.3ms,23.4ms
  • vector<char>back_inserter :26.1 ms,24.5 ms
  • 使用普通迭代器的vector<char> :3.13 ms,2.48 ms
  • vector<char>迭代器和边界检查:2.97 ms,2.53 ms
  • char[] :1.52 ms,1.25 ms

相同的笔记本电脑,Visual C ++ 2010 64位编译器:

  • ostringstream :48.6毫秒,45.0毫秒
  • stringbuf :16.2毫秒,16.0毫秒
  • vector<char>back_inserter :26.3 ms,26.5 ms
  • vector<char>与普通迭代器:0.87毫秒,0.89毫秒
  • vector<char>迭代器和边界检查:0.99 ms,0.99 ms
  • char[] :1.25 ms,1.24 ms

编辑:跑了两遍,看看结果如何一致。 非常一致的IMO。

注:在我的笔记本电脑上,由于我可以节省更多的CPU时间比ideone允许,我设置的迭代次数为1000所有方法。 这意味着ostringstreamvector重新分配只会在第一遍时发生,对最终结果应该没有什么影响。

编辑:哎呀,发现在vector与普通迭代器中的错误,迭代器没有被高级,因此有太多的caching命中。 我想知道vector<char>是如何超越char[] 。 尽pipe如此,在VC ++ 2010下, vector<char>仍然比char[]更快。

结论

每次输出数据时,输出stream的缓冲需要三个步骤:

  • 检查传入块是否适合可用的缓冲区空间。
  • 复制传入块。
  • 更新数据结束指针。

我发布的最新代码片段“ vector<char>简单的迭代器加边界检查”不仅仅是这样做的,它还分配额外的空间并在传入的块不适合时移动现有的数据。 正如Clifford所指出的那样,缓冲在一个文件I / O类中不需要这样做,它只会刷新当前的缓冲区并重用它。 所以这应该是缓冲输出成本的上限。 这正是内存缓冲区工作所需要的。

那么为什么stringbuf在ideone上速度要慢2.5倍,而在我testing时至less慢10倍呢? 它在这个简单的微基准testing中并没有被多形地使用,所以没有解释它。

没有像标题那样回答你的问题的具体情况:2006 年的C ++技术性能技术报告对IOStreams有一个有趣的部分(p.68)。 与您的问题最相关的是在第6.1.2节(“执行速度”):

由于IOStream处理的某些方面分布在多个方面,标准似乎要求执行效率低下。 但事实并非如此 – 通过使用某种forms的预处理,大部分工作都可以避免。 使用比通常使用的稍微更智能的链接器,可以消除其中的一些低效率。 这在第6.2.3和第6.2.5节中讨论。

由于该报告是在2006年编写的,人们希望这些build议中的许多将会被纳入当前的编译器,但也许情况并非如此。

正如你所提到的,方面可能不会write()write() (但我不会盲目地假设)。 那么function是什么? 在使用GCC编译的ostringstream代码上运行GProf会产生以下故障:

  • std::basic_streambuf<char>::xsputn(char const*, int) 44.23% std::basic_streambuf<char>::xsputn(char const*, int)
  • std::ostream::write(char const*, int)占34.62%
  • 12.50% main
  • 6.73%在std::ostream::sentry::sentry(std::ostream&)
  • std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int) 0.96%
  • 0.96%在std::basic_ostringstream<char>::basic_ostringstream(std::_Ios_Openmode)
  • 0.00%在std::fpos<int>::fpos(long long)

所以大部分时间都花在了xsputn ,最终在对光标位置和缓冲区进行大量检查和更新之后调用了std::copy() (有关详细信息,请参阅c++\bits\streambuf.tcc )。

我认为这是你关注最糟糕的情况。 如果你处理的数据量相当大,那么执行的所有检查只占总工作量的一小部分。 但是,你的代码每次只能以四个字节移动数据,并且每次都会产生额外的成本。 显然,在现实生活中,人们会避免这样做 – 考虑如果在一个1m的数组上调用write ,而不是在一个int上的1m次上调用write则可以忽略不计。 在真实的情况下,人们会非常欣赏IOStreams的重要特性,即其内存安全和types安全的devise。 这样的好处是有代价的,而且你已经写了一个testing,使得这些成本占据了执行时间的主导地位。

我对那里的Visual Studio用户感到非常失望,他们对此感到厌烦:

  • ostream的Visual Studio实现中, sentry对象(标准所要求的)进入保护streambuf (这不是必需的)的关键部分。 这似乎不是可选的,所以即使对于单个线程使用的本地stream,也要花费线程同步的代价,而不需要同步。

这伤害了使用ostringstream来严格格式化消息的代码。 使用stringbuf直接避免了使用sentry ,但格式化的插入操作符不能直接在streambuf上工作。 对于Visual C ++ 2010来说,关键的部分是将ostringstream::write放缓三倍,而不是基本的stringbuf::sputn调用。

看看beldaz在newlib上的profiler数据 ,似乎很清楚,gcc的sentry并没有像这样疯狂。 在gcc下的ostringstream::write只比stringbuf::sputn长50%,但是stringbuf::sputn本身比在VC ++下慢得多。 而且两者仍然比较不利地使用一个vector<char>进行I / O缓冲,尽pipe与VC ++不同。

你看到的问题是每个调用write()的开销。 您添加的每个抽象级别(char [] – > vector – > string – > ostringstream)会添加一些更多的函数调用/返回和其他内务处理,如果您将其称为一百万次 – 则累加起来。

我修改了两个关于ideone的例子,一次写十个整数。 ostringstream时间从53到6毫秒(几乎提高了10倍),而字符循环改进了(3.7到1.5) – 有用,但只有两个因素。

如果你关心性能,那么你需要select正确的工具。 ostringstream是有用和灵活的,但有一个罚款,以你想要的方式使用它。 char []是更难的工作,但性能增益可以是伟大的(记住gcc也可能为你插入memcpys)。

简而言之,ostringstream不会被破坏,但是越接近金属,代码运行得越快。 汇编对于一些人来说还是有优势的。

为了获得更好的性能,您必须了解您使用的容器的工作方式。 在你的char []数组示例中,所需大小的数组是事先分配的。 在你的vector和ostringstream例子中,当对象增长的时候,你迫使对象多次分配,重新分配和复制数据。

与std :: vector一样,通过初始化vector的大小来达到最终的大小,就像char数组一样。 相反,你不公平地削减performance,调整为零! 这不是一个公平的比较。

关于ostringstream,预先分配空间是不可能的,我build议这是一个inappropruate使用。 这个类比简单的char数组有更大的实用性,但是如果你不需要这个实用工具,那就不要使用它,因为在任何情况下你都会支付开销。 相反,它应该用于它的好处 – 将数据格式化为string。 C ++提供了大量的容器,而ostringstram是最不适合这个用途的。

在向量和ostringstream的情况下,你可以得到缓冲区溢出保护,你不会得到一个char数组,并且这个保护不是免费的。