每当我提到c++标准库iostreams的缓慢性能时,我都会遇到一波难以置信的声音。然而,我的分析结果显示,在iostream库代码上花费了大量时间(完整的编译器优化),从iostream切换到特定于操作系统的I/O api和自定义缓冲区管理确实带来了一个数量级的改进。

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

基准

为了让事情继续下去,我写了几个简短的程序来练习iostreams内部缓冲:

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

请注意,ostringstream和stringbuf版本运行的迭代次数更少,因为它们要慢得多。

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

这些都是内存中的缓冲区,所以iostream的慢不能归咎于磁盘I/O慢,太多的刷新,与stdio的同步,或者人们用来解释观察到的c++标准库iostream慢的任何其他事情。

如果能看到其他系统上的基准测试,以及对常见实现(如gcc的libc++、Visual c++、Intel c++)所做的事情的评论,以及标准要求的开销有多少,那就太好了。

测试的基本原理

许多人已经正确地指出,iostreams更常用于格式化输出。然而,它们也是c++标准为二进制文件访问提供的唯一现代API。但是,在内部缓冲上进行性能测试的真正原因适用于典型的格式化I/O:如果iostreams不能为磁盘控制器提供原始数据,那么当它们还负责格式化时,它们怎么可能跟上呢?

基准时间

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

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

Ostringstream: 53毫秒 Stringbuf: 27毫秒 Vector <char> and back_inserter: 17.6 ms Vector <char>使用普通迭代器:10.6 ms Vector <char>迭代器和边界检查:11.4 ms Char []: 3.7 ms

在我的笔记本电脑上(Visual c++ 2010 x86, cl /Ox /EHsc, Windows 7终极64位,英特尔酷睿i7, 8 GB RAM):

Ostringstream: 73.4毫秒,71.6毫秒 Stringbuf: 21.7 ms, 21.3 ms Vector <char> and 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,与配置文件引导优化cl /Ox /EHsc /GL / C, link /ltcg:pgi,运行,link /ltcg:pgo,测量:

Ostringstream: 61.2 ms, 60.5 ms Vector <char>使用普通迭代器:1.04 ms, 1.03 ms

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

Ostringstream: 62.7 ms, 60.5 ms Stringbuf: 44.4 ms, 44.5 ms Vector <char> and 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 ms, 87.6 ms Stringbuf: 23.3 ms, 23.4 ms Vector <char> and 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 ms, 45.0 ms Stringbuf: 16.2 ms, 16.0 ms Vector <char> and back_inserter: 26.3 ms, 26.5 ms Vector <char>使用普通迭代器:0.87 ms, 0.89 ms Vector <char>迭代器和边界检查:0.99 ms, 0.99 ms Char []: 1.25 ms, 1.24 ms

编辑:全部运行两次,看看结果是否一致。在我看来相当稳定。

注意:在我的笔记本电脑上,因为我可以腾出比ideone更多的CPU时间,所以我将所有方法的迭代次数设置为1000。这意味着只在第一次传递时发生的ostringstream和vector重分配应该对最终结果没有什么影响。

编辑:哎呀,在vector-with-ordinary-iterator中发现了一个错误,迭代器没有被高级化,因此有太多的缓存命中。我想知道vector<char>如何优于char[]。不过这并没有太大的区别,vector<char>在vc++ 2010下仍然比char[]快。

结论

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

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

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

那么为什么stringbuf在ideone上慢了2.5倍,而在我测试时至少慢了10倍呢?在这个简单的微基准测试中没有多态地使用它,所以这不能解释它。


您所看到的问题都存在于每次调用write()的开销中。您添加的每个抽象级别(char[] -> vector -> string -> ostringstream)都会添加更多的函数调用/返回和其他一些家务杂事,如果您调用它一百万次的话,这些杂事就会累加起来。

我修改了ideone上的两个例子,一次写10个int。ostringstream时间从53毫秒提高到6毫秒(几乎提高了10倍),而char循环时间提高了(3.7到1.5)——很有用,但只是提高了两倍。

如果您非常关心性能,那么您需要为工作选择合适的工具。Ostringstream很有用,也很灵活,但是按照您试图使用的方式使用它是有代价的。Char[]是比较困难的工作,但是性能收益可能很大(请记住,GCC可能也会为您内联memcpys)。

简而言之,ostringstream没有损坏,但是越接近金属,代码运行就越快。对某些人来说,汇编器仍然有优势。


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

对于std::vector,这很容易通过初始化vector的大小到最终大小来解决,就像你做char数组一样;相反,您通过将大小调整为零而相当不公平地削弱了性能!这很难说是一个公平的比较。

对于ostringstream,预分配空间是不可能的,我认为这是一种不适当的使用。这个类比简单的char数组有更大的效用,但是如果您不需要这个效用,那么就不要使用它,因为在任何情况下您都会支付开销。相反,它应该用于它所擅长的地方——将数据格式化为字符串。c++提供了广泛的容器,ostringstream是最不适合用于此目的的容器之一。

在vector和ostringstream的情况下,你得到了防止缓冲区溢出的保护,而在char数组中你得不到这种保护,而且这种保护不是免费的。


并没有像标题那样回答你的问题的细节:2006年c++性能技术报告有一个关于IOStreams的有趣部分(第68页)。与你的问题最相关的是第6.1.2节(“执行速度”):

因为IOStreams处理的某些方面是 它分布在多个方面 看来标准要求 低效率的实现。但这 难道不是这样吗——用某种形式 对于预处理,大部分工作都可以 被避免的。稍微聪明一点 链接器比通常使用的,它是 有可能移除其中一些 低效率。这在 §6.2.3和§6.2.5。

由于该报告是在2006年撰写的,人们会希望其中的许多建议已经被纳入目前的编纂者中,但情况可能并非如此。

正如你所提到的,facet可能不会出现在write()中(但我不会盲目地假设)。那么特点是什么呢?在用GCC编译的ostringstream代码上运行GProf会得到以下分解:

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

所以大部分时间都花在xsputn上,它在大量检查和更新游标位置和缓冲区后最终调用std::copy()(查看c++\bits\streambuf)。详情请告知)。

My take on this is that you've focused on the worst-case situation. All the checking that is performed would be a small fraction of the total work done if you were dealing with reasonably large chunks of data. But your code is shifting data in four bytes at a time, and incurring all the extra costs each time. Clearly one would avoid doing so in a real-life situation - consider how negligible the penalty would have been if write was called on an array of 1m ints instead of on 1m times on one int. And in a real-life situation one would really appreciate the important features of IOStreams, namely its memory-safe and type-safe design. Such benefits come at a price, and you've written a test which makes these costs dominate the execution time.


我对Visual Studio的用户很失望,他们在这个问题上很有发言权:

在ostream的Visual Studio实现中,哨兵对象(这是标准所要求的)进入一个临界区,保护streambuf(这不是必需的)。这似乎不是可选的,所以即使是单个线程使用的本地流,也需要支付线程同步的成本,因为它不需要同步。

这严重损害了使用ostringstream格式化消息的代码。直接使用stringbuf可以避免使用sentry,但是格式化的插入操作符不能直接作用于streambuf。对于Visual c++ 2010,临界区将使ostringstream::write的速度降低三倍,而不是底层的stringbuf::sputn调用的速度。

看看beldaz在newlib上的分析器数据,很明显gcc的哨兵不会做这样疯狂的事情。在gcc下编写ostringstream::只比stringbuf::sputn长50%,但是stringbuf本身比在vc++下慢得多。这两种方法与使用vector<char>进行I/O缓冲相比仍然非常不利,尽管与vc++下的情况不同。