每当我提到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倍呢?在这个简单的微基准测试中没有多态地使用它,所以这不能解释它。