C ++标准是否要求iostream的性能较差,或者我只是在处理较差的实现?


196

每当我提到C ++标准库iostream的性能下降时,我都会感到一阵怀疑。但是,我的探查器结果显示,大量时间花费在iostream库代码(完整的编译器优化)上,从iostream切换到特定于操作系统的I / O API和自定义缓冲区管理确实提高了一个数量级。

C ++标准库正在做什么额外的工作,它是标准所必需的,并且在实践中是否有用?还是某些编译器提供了与手动缓冲区管理竞争的iostream实现?

基准测试

为了使事情顺利进行,我编写了一些简短的程序来练习iostream的内部缓冲:

请注意,ostringstreamstringbuf版本运行的迭代次数较少,因为它们要慢得多。

在ideone上,其ostringstream速度比std:copy+ back_inserter+ 慢3倍std::vector,比memcpy在原始缓冲区中慢15倍。当我将实际应用程序切换到自定义缓冲时,这感觉与前后分析一致。

这些都是内存中的缓冲区,因此不能将iostream的缓慢归咎于磁盘慢的I / O,过多的刷新,与stdio的同步,或者人们用来掩盖C ++标准库的缓慢的任何其他事情。 iostream。

很高兴看到其他系统上的基准测试以及对常见实现的注释(例如gcc的libc ++,Visual C ++,Intel C ++)以及该标准要求多少开销。

此测试的原理

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

基准时间

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

在ideone上(gcc-4.3.4,未知的操作系统和硬件):

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

在我的笔记本电脑上(Visual C ++ 2010 x86,cl /Ox /EHscWindows 7 Ultimate 64位,Intel Core i7,8 GB 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毫秒,1.57毫秒

VISUAL C ++ 2010 x86上,与档案导引优化cl /Ox /EHsc /GL /clink /ltcg:pgi,运行,link /ltcg:pgo,措施:

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

使用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毫秒,3.9毫秒
  • vector<char> 迭代器和边界检查:4.0 ms,4.0 ms
  • char[]:3.57毫秒,3.75毫秒

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

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

同一台笔记本电脑,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 ms,0.89 ms
  • vector<char> 迭代器和边界检查:0.99毫秒,0.99毫秒
  • char[]:1.25毫秒,1.24毫秒

编辑:都跑了两次,以查看结果的一致性。相当一致的海事组织。

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

编辑:糟糕,在vector-with-ordinary-iterator中发现了一个错误,该迭代器没有得到改进,因此缓存命中次数过多。我想知道vector<char>表现如何char[]。不过,它并没有太大的变化,vector<char>仍然比char[]VC ++ 2010 更快。

结论

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

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

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

那么,为什么stringbuf在Ideone上速度要慢2.5倍,而在测试时至少要慢10倍?在这个简单的微基准测试中并没有多态地使用它,因此并没有解释它。


24
您一次要写一百万个字符,想知道为什么它比复制到预分配的缓冲区要慢?
Anon。

19
@Anon:我一次缓冲四百万个字节,是的,我想知道为什么这很慢。如果std::ostringstream不够聪明,无法以指数方式增加其缓冲区大小std::vector,那是(A)愚蠢的(B)人们在思考I / O性能时应该考虑的事情。无论如何,缓冲区被重用,不会每次都重新分配。并且std::vector还使用了动态增长的缓冲区。我想在这里公平。
Ben Voigt 2010年

14
您实际上是在尝试进行基准测试吗?如果您不使用的任何格式设置功能,ostringstream并且想要尽可能快的性能,则应考虑直接使用stringbufostream假定这些类通过rdbuf()其虚拟功能接口将具有区域设置的格式化功能与灵活的缓冲区选择(文件,字符串等)联系在一起。如果您不进行任何格式化,那么与其他方法相比,额外级别的间接寻址肯定会成比例地昂贵。
CB Bailey 2010年

5
为真相+1。当输出涉及双精度的日志信息时,我们从移到,ofstream从而提高了阶数或幅度fprintf。WinXPsp3上的MSVC 2008。iostreams太慢了。
KitsuneYMG 2010年

6
这是委员会网站上的一些测试:open-std.org/jtc1/sc22/wg21/docs/D_5.cpp
Johannes Schaub-litb 2010年

Answers:


49

没有回答您的问题的细节,而是标题:2006年 年C ++性能技术报告中有一个有趣的IOStreams部分(第68页)。与您的问题最相关的是第6.1.2节(“执行速度”):

由于IOStreams处理的某些方面分布在多个方面,因此,似乎该标准要求实施效率低下。但是事实并非如此-通过使用某种形式的预处理,可以避免很多工作。使用比通常使用的连接器更智能的链接器,可以消除其中一些低效率的问题。§6.2.3和§6.2.5中对此进行了讨论。

自从该报告于2006年编写以来,人们希望许多建议已被合并到当前的编译器中,但事实并非如此。

正如您所提到的,刻面可能没有出现write()(但我不会盲目地假设)。那么功能是什么?在ostringstream使用GCC编译的代码上运行GProf 可获得以下细分:

  • 在44.23% std::basic_streambuf<char>::xsputn(char const*, int)
  • 在34.62% std::ostream::write(char const*, int)
  • 12.50%在 main
  • 6.73%在 std::ostream::sentry::sentry(std::ostream&)
  • 0.96%在 std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int)
  • 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查看有关详细信息)。

我对此的看法是,您一直专注于最坏的情况。如果您要处理相当大的数据块,那么执行的所有检查将只是总工作量的一小部分。但是您的代码一次将数据移入四个字节,并且每次都产生所有额外费用。显然,在现实生活中避免这样做-考虑如果write对一百万个整数的数组调用而不是对一个整数一百万次的调用,惩罚将是微不足道的。在现实生活中,人们真的会喜欢IOStreams的重要功能,即其内存安全和类型安全的设计。这样的好处是有代价的,您编写了一个测试,使这些成本支配了执行时间。


对于将来有关格式化的iostream插入/提取性能的问题,这听起来像是很棒的信息,我可能很快就会问到。但是我不相信有任何方面ostream::write()
Ben Voigt 2010年

4
+1进行概要分析(我认为这是Linux机器吗?)。但是,实际上我一次要添加四个字节(实际上sizeof i,但是我正在测试的所有编译器都具有4个字节int)。而这似乎并没有那么不现实的我,什么大小的块,你认为在每次调用获取传递到xsputn像典型的代码stream << "VAR: " << var.x << ", " << var.y << endl;
Ben Voigt 2010年

39
@beldaz:那个只调用xsputn五次的“典型”代码示例很可能在一个写入1000万行文件的循环中。与我的基准代码相比,将数据大块传递到iostream的实际情况要少得多。为什么我必须用最少的调用次数来写缓冲流?如果必须自己缓冲,iostream的意义何在?对于二进制数据,我可以选择自己进行缓冲,当将数百万个数字写入文本文件时,bulk选项就不存在了,我必须operator <<为每个选项调用。
Ben Voigt 2010年

1
@beldaz:可以通过简单的计算来估计何时I / O开始占主导地位。以当前消费者级硬盘典型的90 MB / s的平均写入速率,刷新4 MB缓冲区所需的时间小于45毫秒(吞吐量,由于操作系统写入缓存而导致的延迟并不重要)。如果运行内部循环所需的时间比填充缓冲区所需的时间长,则CPU将成为限制因素。如果内部循环运行得更快,则I / O将成为限制因素,或者至少剩下一些CPU时间来完成实际工作。
Ben Voigt 2010年

5
当然,这并不意味着使用iostream一定意味着程序缓慢。如果I / O只是程序的很小一部分,那么使用性能较差的I / O库不会对整体产生太大影响。但是,没有足够频繁地调用它并不意味着良好的性能,在I / O繁重的应用程序中,它确实很重要。
Ben Voigt 2010年

27

我对在外面的Visual Studio用户感到失望,他们对这一个给我个好印象:

  • 在的Visual Studio实现中ostream,该sentry对象(标准要求)进入一个关键部分,以保护streambuf(不需要)该对象。这似乎不是可选的,因此,即使对于单个线程使用的本地流,也无需同步,您要付出线程同步的费用。

这会ostringstream严重破坏用于格式化消息的代码。stringbuf直接使用避免使用sentry,但是格式化的插入运算符不能直接在streambufs上使用。对于Visual C ++ 2010,ostringstream::write与基础stringbuf::sputn调用相比,关键部分的速度降低了三倍。

在newlib查看beldaz的探查器数据,似乎很清楚,gcc sentry并没有做任何疯狂的事情。 ostringstream::write在gcc下,它只比花费大约50%的时间stringbuf::sputn,但是stringbuf它本身比在VC ++下要慢得多。两者仍然与使用vector<char>I / O缓冲相比非常不利,尽管与VC ++相比差距不大。


这些信息是否仍然是最新的?GCC随附的AFAIK,C ++ 11实现执行此“疯狂”锁定。当然,VS2010仍然可以做到。谁能澄清这种行为,并且在C ++ 11中是否仍然存在“不需要”?
mloskot 2012年

2
@mloskot:我对sentry…… 没有线程安全要求。“类哨兵定义了一个负责执行异常安全前缀和后缀操作的类。” 和注释“哨兵构造函数和析构函数还可以执行其他依赖于实现的操作。” 人们还可以从C ++原则“您不用为不使用的东西付费”中推测C ++委员会永远不会批准这种浪费的要求。但是,请随时提出有关iostream线程安全性的问题。
Ben Voigt 2012年

8

您看到的问题全在每次调用write()的开销中。您添加的每个抽象级别(char []-> vector-> string-> ostringstream)都会增加一些函数调用/返回和其他管家功能(如果您调用一百万次,则相加)。

我在ideone上修改了两个示例,一次可以写10个整数。ostringstream时间从53毫秒减少到6毫秒(几乎提高了10倍),而char循环得到了改善(3.7到1.5倍)-有用,但仅增加了两倍。

如果您担心性能,那么您需要为工作选择正确的工具。ostringstream有用且灵活,但是以您尝试的方式使用它会受到惩罚。char []更加辛苦,但是性能却可以提高(请记住,gcc也可能会为您内联memcpys)。

简而言之,ostringstream并没有被破坏,但是您越接近金属,您的代码将运行得越快。汇编程序对于某些人仍然具有优势。


8
不必ostringstream::write()做什么vector::push_back()呢?如果有的话,它应该更快,因为它传递了一个块而不是四个单独的元素。如果ostringstreamstd::vector不提供任何其他功能慢的话,是的,我称之为断线。
Ben Voigt 2010年

1
@Ben Voigt:相反,在这种情况下,向量载体必须执行ostringstream不必执行的操作,从而使向量更高效。保证向量在内存中是连续的,而ostringstream不是。Vector是旨在提高性能的类之一,而ostringstream不是。
Dragontamer5788

2
@Ben Voigt:stringbuf直接使用不会删除所有函数调用,因为stringbuf的公共接口由基类中的公共非虚拟函数组成,然后将其分派给派生类中的受保护虚拟函数。
CB Bailey 2010年

2
@Charles:在任何合适的编译器上都应该这样做,因为公共函数调用将被内联到编译器已知动态类型的上下文中,因此它可以删除间接调用,甚至可以内联那些调用。
Ben Voigt 2010年

6
@Roddy:我应该认为这是所有内联模板代码,在每个编译单元中都可见。但我想这可能会因实施情况而异。可以肯定的是,我希望讨论中的调用,sputn即调用虚拟protected 的public 函数xsputn可以内联。即使xsputn没有内联,编译器也可以在内联sputn时确定xsputn所需的确切替代值,并生成直接调用,而无需通过vtable。
Ben Voigt 2010年

1

为了获得更好的性能,您必须了解所使用的容器的工作方式。在您的char []数组示例中,预先分配了所需大小的数组。在vector和ostringstream示例中,您正在强迫对象重复分配和重新分配,并可能随着对象的增长多次复制数据。

使用std :: vector可以轻松解决此问题,方法是像处理char数组一样将向量的大小初始化为最终大小。相反,您宁可通过将大小调整为零来不公平地削弱性能!这几乎不是一个公平的比较。

关于ostringstream,不可能预分配空间,我建议这是不适当的用法。该类比简单的char数组具有更大的实用程序,但是如果您不需要该实用程序,则不要使用它,因为在任何情况下您都将支付开销。相反,应将其用于其优点-将数据格式化为字符串。C ++提供了各种各样的容器,而ostringstram对此最不适合。

在使用vector和ostringstream的情况下,您可以获得防止缓冲区溢出的保护,而不能通过char数组来获得保护,并且这种保护不是免费提供的。


1
分配似乎不是ostringstream的问题。他只是寻找零以进行后续迭代。没有截断。我也试过ostringstream.str.reserve(4000000)了,没有什么区别。
罗迪2010年

我认为使用ostringstream,您可以通过传递虚拟字符串来“保留”,即:ostringstream str(string(1000000 * sizeof(int), '\0'));使用vectorresize不会释放任何空间,它只会在需要时扩展。
Nim 2010年

1
“向量..防止缓冲区溢出的保护”。常见的误解- vector[]默认情况下,通常不会检查运算符的边界错误。vector.at()但是。
罗迪

2
vector<T>::resize(0)通常不会重新分配内存
Niki Yoshiuchi 2010年

2
@Roddy:不使用operator[],但是push_back()(通过back_inserter),这确实可以测试溢出。添加了另一个不使用的版本push_back
Ben Voigt 2010年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.