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

Does the C++ standard mandate poor performance for iostreams, or am I just dealing with a poor implementation?

每当我提到C++标准库IOFROM的慢速性能时,我就会遇到一种不信任的浪潮。不过,我有一个探查器结果显示,在iostream库代码(完整的编译器优化)中花费了大量时间,从iostreams切换到操作系统特定的I/O API和自定义缓冲区管理确实带来了一个数量级的改进。

C++标准库所做的额外工作是标准要求的,在实践中有用吗?或者,一些编译器是否提供了与手动缓冲区管理竞争的iostream实现?

基准点

为了让事情顺利进行,我编写了几个简短的程序来练习iostreams内部缓冲:

  • 将二进制数据放入ostringstreamhttp://ideone.com/2ppyw
  • 将二进制数据放入char[]缓冲区http://ideone.com/ni5ct
  • 使用back_inserterhttp://ideone.com/mj2fi将二进制数据放入vector
  • 新增:vector简单迭代器http://ideone.com/9iitv
  • 新增:将二进制数据直接放入stringbufhttp://ideone.com/qc9qa
  • 新增:vector简单迭代器加边界检查http://ideone.com/yyrky

注意,ostringstreamstringbuf版本运行的迭代次数更少,因为它们的速度慢得多。

在ideone中,ostringstreamstd:copyback_inserterstd::vector慢3倍,比memcpy慢15倍。当我将实际应用程序切换到自定义缓冲区时,这种感觉与分析前后一致。

这些都是内存缓冲区,因此慢流的慢度不能归咎于慢速磁盘I/O、过多的刷新、与STDIO同步,或者人们用来解释C++标准库IOSWATH慢速的任何其他事情。

很高兴看到其他系统上的基准和对常见实现(比如GCC的LBC+++、Visual C++、英特尔C++)的注释,以及标准规定的开销有多大。

本试验的基本原理

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

基准时间

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

在IDeone上(GCC-4.3.4,未知操作系统和硬件):

  • ostringstream53毫秒
  • stringbuf27毫秒
  • vectorback_inserter17.6 ms
  • 带普通迭代器的vector:10.6 ms
  • vector迭代器和边界检查:11.4ms
  • char[]3.7毫秒

在我的笔记本电脑上(Visual C++ 2010 x86,EDCOX1×22),Windows 7最终64位,英特尔内核I7,8 GB RAM):

  • ostringstream:73.4毫秒,71.6毫秒
  • stringbuf:21.7 ms,21.3 ms
  • vectorback_inserter:34.6 ms,34.4 ms
  • 带普通迭代器的vector:1.10 ms,1.04 ms
  • vector迭代器和边界检查: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,用轮廓引导优化EDOCX1,30,EDOCX1,31,,运行,EDOCX1,32,测量:

  • ostringstream:61.2 ms,60.5 ms
  • 带普通迭代器的vector:1.04 ms,1.03 ms

使用Cygwin GCC 4.3.4 g++ -O3的相同笔记本电脑、相同操作系统:

  • ostringstream62.7 ms,60.5 ms
  • stringbuf44.4 ms,44.5 ms
  • vectorback_inserter13.5 ms,13.6 ms
  • 带普通迭代器的vector:4.1 ms,3.9 ms
  • vector迭代器和边界检查:4.0 ms,4.0 ms
  • char[]3.57 ms,3.75 ms

相同的笔记本电脑,Visual C++ 2008 SP1,EDOCX1,22:

  • ostringstream88.7 ms,87.6 ms
  • stringbuf:23.3 ms,23.4 ms
  • vectorback_inserter:26.1 ms,24.5 ms
  • 带普通迭代器的vector:3.13 ms,2.48 ms
  • vector迭代器和边界检查:2.97 ms,2.53 ms
  • char[]1.52 ms,1.25 ms

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

  • ostringstream:48.6 ms,45.0 ms
  • stringbuf16.2 ms,16.0 ms
  • vectorback_inserter:26.3 ms,26.5 ms
  • 带普通迭代器的vector:0.87 ms,0.89 ms
  • vector迭代器和边界检查:0.9


    没有回答你的问题的细节,就标题而言:2006个关于C++性能的技术报告有一个有趣的关于iOFFROW的章节(P.68)。与您的问题最相关的是第6.1.2节("执行速度"):

    Since certain aspects of IOStreams processing are
    distributed over multiple facets, it
    appears that the Standard mandates an
    inefficient implementation. But this
    is not the case — by using some form
    of preprocessing, much of the work can
    be avoided. With a slightly smarter
    linker than is typically used, it is
    possible to remove some of these
    inefficiencies. This is discussed in
    §6.2.3 and §6.2.5.

    既然报告是2006年写的,人们希望许多建议都能被纳入当前的编纂者之中,但也许情况并非如此。

    正如您所提到的,面可能在write()中不起作用(但我不会盲目假设)。那么特征是什么呢?在用gcc编译的ostringstream代码上运行gprof给出了以下分解:

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

    因此,大部分时间都花在xsputn上,在大量检查和更新光标位置和缓冲区之后,最终调用std::copy()(请查看c++\bits\streambuf.tcc)。

    我的看法是,你关注的是最坏的情况。如果您处理的是相当大的数据块,那么所执行的所有检查只占全部工作的一小部分。但是您的代码一次以四个字节移动数据,每次都会产生额外的成本。很明显,在现实生活中我们可以避免这样做——考虑一下,如果write在一个1米整数的数组上被调用,而不是在一个1米整数上被调用,那么惩罚将是多么微不足道。在现实生活中,我们真的会欣赏iostream的重要特性,即它的内存安全和类型安全设计。这样的好处是有代价的,您已经编写了一个测试,使这些成本在执行时间中占主导地位。


    我对外面的Visual Studio用户感到非常失望,他们宁愿给我这个:

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

    这会严重损害使用ostringstream来格式化消息的代码。使用EDCOX1〔4〕直接避免使用EDCOX1〔1〕,但格式化的插入运算符不能直接作用于EDCOX1〔2〕。对于VisualC++ 2010,临界区使EDCOX1×7减慢三倍,与EDCOX1的8调用无关。

    从贝尔达兹在Newlib上的分析数据来看,很明显GCC的sentry并没有做任何疯狂的事情。GCC下的ostringstream::writestringbuf::sputn长约50%,但stringbuf本身比vc++慢得多。两种方法相比,使用vector进行I/O缓冲仍然非常不利,尽管与在vc++下的幅度不同。


    您看到的问题都在每个write()调用的开销中。您添加的每一个抽象级别(char[]->vector->string->ostringstream)都添加了更多的函数调用/返回,以及其他一些家政方面的把戏,如果您调用了一百万次,这些把戏就会加起来。

    我修改了IDeone上的两个例子,一次写十个整数。Ostringstream时间从53到6 ms(几乎是10倍的改善),而char循环改善(3.7到1.5)-有用,但只有2倍。

    如果你很关心绩效,那么你需要为工作选择合适的工具。Ostringstream是有用的和灵活的,但是如果你想用它的方式使用它,就会受到惩罚。char[]是比较难的工作,但是性能的提高是很好的(记住GCC可能也会为您内联memcpys)。

    简而言之,ostringstream没有被破坏,但是越接近金属,代码运行的越快。装配工对一些人仍然有优势。


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

    对于std::vector,这可以通过将向量的大小初始化为最终大小来轻松解决,就像对char数组所做的那样;相反,通过将大小调整为零,会不公平地削弱性能!这可不是一个公平的比较。

    关于排斥流,不可能预先分配空间,我建议这是一种不适当的使用。类的实用程序比简单的char数组大得多,但是如果您不需要这个实用程序,那么就不要使用它,因为在任何情况下都要支付开销。相反,它应该用于将数据格式化为字符串的目的。C++提供了广泛的容器,而OSTRUNSTRAM是最不合适的。

    对于向量和ostringstream,您可以从缓冲区溢出中获得保护,但使用char数组并不能获得保护,而且这种保护也不是免费的。