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

Ben*_*igt 193 c++ performance iostream

每当我提到C++标准库iostream的慢性能时,我都会遇到一阵难以置信的风潮.然而,我有剖析器结果显示在iostream库代码中花费了大量时间(完全编译器优化),并且从iostream切换到特定于操作系统的I/O API和自定义缓冲区管理确实提供了一个数量级的改进.

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

基准

为了解决问题,我编写了几个简短的程序来练习iostreams内部缓冲:

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

在ideone上,它ostringstreamstd: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:如果iostreams无法保持磁盘控制器提供原始数据,那么当他们负责格式化时,他们怎么可能跟上呢?

基准时间

所有这些都是outer(k)循环的每次迭代.

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

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

在我的笔记本电脑上(Visual C++ 2010 x86,cl /Ox /EHscWindows 7旗舰版64位,Intel Core i7,8 GB RAM):

  • ostringstream:73.4毫秒,71.6毫秒
  • stringbuf:21.7 ms,21.3 ms
  • vector<char>back_inserter:34.6毫秒,34.4毫秒
  • vector<char> 与普通迭代器:1.10毫秒,1.04毫秒
  • 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 /c,link /ltcg:pgi运行,link /ltcg:pgo,措施:

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

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

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

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

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

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

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

编辑:全部跑两次,看看结果是多么一致.相当一致的IMO.

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

编辑:糟糕,在vector-with-ordinary-iterator中发现了一个错误,迭代器没有被提升,因此缓存命中次数过多.我想知道如何vector<char>表现出色char[].虽然它没有太大的区别,但vector<char>仍然比char[]VC++ 2010 更快.

结论

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

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

我发布的最新代码片段" vector<char>simple iterator plus bounds check"不仅可以实现这一点,还可以分配额外的空间,并在传入块不适合时移动现有数据.正如Clifford指出的那样,在文件I/O类中缓冲不必这样做,它只是刷新当前缓冲区并重用它.所以这应该是缓冲输出成本的上限.它正是制作工作内存缓冲区所需要的.

那么为什么stringbuf在ideone上放慢2.5倍,在测试时放慢至少10倍?它并没有在这个简单的微基准测试中使用多态,因此不能解释它.

bel*_*daz 47

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

由于IOStreams处理的某些方面分布在多个方面,因此标准似乎要求实施效率低下.但事实并非如此 - 通过使用某种形式的预处理,可以避免大部分工作.使用比通常使用的更聪明的链接器,可以消除这些低效率中的一些.这将在§6.2.3和§6.2.5中讨论.

由于该报告是在2006年编写的,人们希望许多建议能够被纳入当前的编制者,但也许情况并非如此.

如你所述,facets可能没有特色write()(但我不会盲目地假设).那么功能是什么?在ostringstream使用GCC编译的代码上运行GProf 会产生以下细分:

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

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

我对此的看法是,你专注于最坏情况.如果您处理相当大的数据块,所执行的所有检查将只是完成的总工作的一小部分.但是您的代码一次只能以四个字节的速度移动数据,并且每次都会产生所有额外成本.很明显,人们会避免在现实生活中这样做 - 考虑如果write在一个1m的数组上调用而不是在1m的1m上调用,那么惩罚是多么微不足道.在现实生活中,人们会非常欣赏IOStream的重要特性,即其内存安全和类型安全设计.这样的好处是有代价的,你编写了一个测试,使这些成本占据执行时间.

  • @beldaz:只调用`xsputn`五次的"典型"代码示例很可能位于一个写入1000万行文件的循环中.将数据传递到大块的iostreams比我的基准代码要少得多.为什么我必须用最少的呼叫写入**缓冲的**流?如果我必须做自己的缓冲,那么iostreams的重点是什么?使用二进制数据,我可以选择自己缓冲它,当将数百万个数字写入文本文件时,批量选项就不存在了,我必须为每个数字调用`operator <<. (38认同)
  • 当然,这并不意味着使用iostream必然意味着一个缓慢的程序.如果I/O只是程序的一小部分,那么使用性能较差的I/O库不会产生太大的影响.但是,不经常被称为重要的是与良好的性能不同,并且在I/O繁重的应用程序中,它确实很重要. (5认同)
  • 用于分析的+1(这是我认为的Linux机器吗?).但是,我实际上一次添加四个字节(实际上是`sizeof i`,但我正在测试的所有编译器都有4字节`int`).这对我来说似乎并不是那么不切实际,你认为在典型代码中每次调用`xsputn`都会传递什么大小的块,例如`stream <<"VAR:"<< var.x <<","< <var.y << endl;`. (4认同)

Ben*_*igt 27

我对那里的Visual Studio用户感到非常失望,他们更喜欢这个用户:

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

这会伤害ostringstream用于格式化消息的代码.使用stringbuf直接避免使用sentry,但格式化插入操作符不能直接在streambufs 上工作.对于Visual C++ 2010,关键部分ostringstream::write与底层stringbuf::sputn调用相比减慢了三倍.

看看beldaz在newlib上的探查器数据,很明显gcc sentry没有像这样做任何疯狂的事情. ostringstream::write在gcc下只需要大约50%的时间stringbuf::sputn,但stringbuf本身比VC++慢得多.并且两者仍然非常不利地使用vector<char>for I/O缓冲,尽管与VC++不同.

  • @mloskot:我看到`sentry`没有线程安全要求......"类sentry定义了一个负责执行异常安全前缀和后缀操作的类." 和一个注释"哨兵构造函数和析构函数也可以执行其他依赖于实现的操作." 人们也可以推测C++委员会永远不会批准如此浪费的要求的C++原则"你不为你不使用的东西买单".但随便问一个关于iostream线程安全的问题. (2认同)

Rod*_*ddy 8

您看到的问题是每次调用write()时的开销.你添加的每个抽象级别(char [] - > vector - > string - > ostringstream)都会增加一些函数调用/返回和其他内务处理guff - 如果你称之为一百万次 - 加起来.

我修改了ideone上的两个例子,一次写入十个整数.ostringstream时间从53到6毫秒(几乎10倍的改进),而char循环改进(3.7到1.5) - 很有用,但只有两倍.

如果你担心性能,那么你需要为工作选择合适的工具.ostringstream是有用且灵活的,但是按照你想要的方式使用它会受到惩罚.char []是更难的工作,但性能提升可能很大(记住gcc可能也会为你编写memcpys).

简而言之,ostringstream没有被破坏,但是越接近金属,代码运行得越快.汇编程序对某些人来说仍然有优势.

  • `ostringstream :: write()`必须做什么`vector :: push_back()`不?如果有的话,它应该更快,因为它是一个块而不是四个单独的元素.如果`ostringstream`比`std :: vector`慢,而没有提供任何额外的功能,那么我会称之为破坏. (8认同)
  • @Roddy:我认为这是所有内联模板代码,在每个编译单元中都可见.但我想这可能因实施而异.可以肯定的是,我希望讨论中的调用,内部调用虚拟受保护的`xsputn`的``sputn`函数.即使`xsputn`没有内联,编译器也可以在内联`sputn`时确定所需的确切`xsputn`覆盖,并在不通过vtable的情况下生成直接调用. (6认同)
  • @Ben Voigt:直接使用`stringbuf`不会删除所有函数调用,因为`stringbuf`的公共接口由基类中的公共非虚函数组成,然后派生到派生类中的受保护虚函数. (2认同)
  • @Charles:在任何体面的编译器上都应该这样,因为公共函数调用将被内联到编译器已知动态类型的上下文中,它可以删除间接甚至内联这些调用. (2认同)