每当我提到c++标准库iostreams的缓慢性能时,我都会遇到一波难以置信的声音。然而,我的分析结果显示,在iostream库代码上花费了大量时间(完整的编译器优化),从iostream切换到特定于操作系统的I/O api和自定义缓冲区管理确实带来了一个数量级的改进。
c++标准库做了哪些额外的工作,它是标准所要求的吗?它在实践中有用吗?或者一些编译器提供的iostreams实现可以与手动缓冲区管理相竞争?
为了让事情继续下去,我写了几个简短的程序来练习iostreams内部缓冲:
ostringstream
http://ideone.com/2PPYwchar[]
缓冲区http://ideone.com/Ni5ctback_inserter
http://ideone.com/Mj2Fi将二进制数据放入vector<char>
vector<char>
简单迭代器http://ideone.com/9iitvstringbuf
http://ideone.com/qc9QA中vector<char>
简单迭代器加边界检查http://ideone.com/YyrKy注意,ostringstream
和stringbuf
版本运行的迭代次数更少,因为它们要慢得多。
在ideone上,ostringstream
比std:copy
+ back_inserter
+ std::vector
慢大约3倍,比memcpy
慢大约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>
和back_inserter
: 17.6 msvector<char>
使用普通迭代器:10.6 msvector<char>
迭代器和边界检查:11.4 mschar[]
: 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 msvector<char>
和back_inserter
: 34.6 ms, 34.4 msvector<char>
与普通迭代器:1.10 ms, 1.04 msvector<char>
迭代器和边界检查:1.11 ms, 0.87 ms, 1.12 ms, 0.89 ms, 1.02 ms, 1.14 mschar[]
: 1.48 ms, 1.57 msVisual c++ 2010 x86,使用Profile-Guided Optimization cl /Ox /EHsc /GL /c
, link /ltcg:pgi
, run, link /ltcg:pgo
, measure:
ostringstream
: 61.2 ms, 60.5 msvector<char>
与普通迭代器:1.04 ms, 1.03 ms同样的笔记本电脑,同样的操作系统,使用cygwin gcc 4.3.4 g++ -O3
:
ostringstream
: 62.7 ms, 60.5 msstringbuf
: 44.4 ms, 44.5 msvector<char>
和back_inserter
: 13.5 ms, 13.6 msvector<char>
与普通迭代器:4.1 ms, 3.9 msvector<char>
迭代器和边界检查:4.0 ms, 4.0 mschar[]
: 3.57 ms, 3.75 ms同一台笔记本电脑,Visual c++ 2008 SP1, cl /Ox /EHsc
:
ostringstream
: 88.7 ms, 87.6 msstringbuf
: 23.3 ms, 23.4 msvector<char>
和back_inserter
: 26.1 ms, 24.5 msvector<char>
与普通迭代器:3.13 ms, 2.48 msvector<char>
迭代器和边界检查:2.97 ms, 2.53 mschar[]
: 1.52 ms, 1.25 ms同样的笔记本电脑,Visual c++ 2010 64位编译器:
ostringstream
: 48.6 ms, 45.0 msstringbuf
: 16.2 ms, 16.0 msvector<char>
和back_inserter
: 26.3 ms, 26.5 msvector<char>
使用普通迭代器:0.87 ms, 0.89 msvector<char>
迭代器和边界检查:0.99 ms, 0.99 mschar[]
: 1.25 ms, 1.24 ms编辑:全部运行两次,看看结果是否一致。在我看来相当稳定。
注意:在我的笔记本电脑上,因为我可以腾出比ideone更多的CPU时间,所以我将所有方法的迭代次数设置为1000。这意味着ostringstream
和vector
重新分配,只发生在第一次传递时,应该对最终结果没有什么影响。
编辑:哎呀,在vector
-with-ordinary-iterator中发现了一个错误,迭代器没有被高级化,因此有太多的缓存命中。我想知道vector<char>
是如何胜过char[]
的。不过这并没有太大的区别,在vc++ 2010下,vector<char>
仍然比char[]
快。
每次追加数据时,缓冲输出流需要三个步骤:
我发布的最新代码片段“vector<char>
simple iterator +边界检查”不仅可以做到这一点,它还可以分配额外的空间,并在传入块不适合时移动现有数据。正如Clifford所指出的,在文件I/O类中缓冲不需要这样做,它只需要刷新当前缓冲区并重用它。所以这应该是缓冲输出成本的上限。这正是创建一个工作的内存缓冲区所需要的。
那么为什么stringbuf
在ideone上慢2.5倍,而在我测试时至少慢10倍?在这个简单的微基准测试中没有多态地使用它,所以这不能解释它。