每当我提到C ++标准库iostream的性能下降时,我都会感到一阵怀疑。但是,我的探查器结果显示,大量时间花费在iostream库代码(完整的编译器优化)上,从iostream切换到特定于操作系统的I / O API和自定义缓冲区管理确实提高了一个数量级。
C ++标准库正在做什么额外的工作,它是标准所必需的,并且在实践中是否有用?还是某些编译器提供了与手动缓冲区管理竞争的iostream实现?
基准测试
为了使事情顺利进行,我编写了一些简短的程序来练习iostream的内部缓冲:
- 将二进制数据放入
ostringstream
http://ideone.com/2PPYw - 将二进制数据放入
char[]
缓冲区http://ideone.com/Ni5ct vector<char>
使用http://ideone.com/Mj2Fi将二进制数据放入back_inserter
- 新功能:
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
+ 慢3倍std::vector
,比memcpy
在原始缓冲区中慢15倍。当我将实际应用程序切换到自定义缓冲时,这感觉与前后分析一致。
这些都是内存中的缓冲区,因此不能将iostream的缓慢归咎于磁盘慢的I / O,过多的刷新,与stdio的同步,或者人们用来掩盖C ++标准库的缓慢的任何其他事情。 iostream。
很高兴看到其他系统上的基准测试以及对常见实现的注释(例如gcc的libc ++,Visual C ++,Intel C ++)以及该标准要求多少开销。
此测试的原理
许多人正确地指出,iostream更常用于格式化输出。但是,它们也是C ++标准提供的唯一用于二进制文件访问的现代API。但是,对内部缓冲进行性能测试的真正原因适用于典型的格式化I / O:如果iostream无法为磁盘控制器提供原始数据,那么当它们负责格式化时又如何保持正常运行呢?
基准时间
所有这些都是外部(k
)循环的每次迭代。
在ideone上(gcc-4.3.4,未知的操作系统和硬件):
ostringstream
:53毫秒stringbuf
:27毫秒vector<char>
和back_inserter
:17.6毫秒vector<char>
普通迭代器:10.6毫秒vector<char>
迭代器和边界检查:11.4毫秒char[]
:3.7毫秒
在我的笔记本电脑上(Visual C ++ 2010 x86,cl /Ox /EHsc
Windows 7 Ultimate 64位,Intel Core i7,8 GB RAM):
ostringstream
:73.4毫秒,71.6毫秒stringbuf
:21.7毫秒,21.3毫秒vector<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毫秒,1.57毫秒
VISUAL C ++ 2010 x86上,与档案导引优化cl /Ox /EHsc /GL /c
,link /ltcg:pgi
,运行,link /ltcg:pgo
,措施:
ostringstream
:61.2毫秒,60.5毫秒vector<char>
使用普通的迭代器:1.04 ms,1.03 ms
使用cygwin gcc 4.3.4的相同笔记本电脑,相同操作系统g++ -O3
:
ostringstream
:62.7毫秒,60.5毫秒stringbuf
:44.4毫秒,44.5毫秒vector<char>
和back_inserter
:13.5 ms,13.6 msvector<char>
使用普通迭代器:4.1毫秒,3.9毫秒vector<char>
迭代器和边界检查:4.0 ms,4.0 mschar[]
:3.57毫秒,3.75毫秒
同一台笔记本电脑,Visual C ++ 2008 SP1 ,cl /Ox /EHsc
:
ostringstream
:88.7毫秒,87.6毫秒stringbuf
:23.3毫秒,23.4毫秒vector<char>
和back_inserter
:26.1 ms,24.5 msvector<char>
使用普通的迭代器:3.13毫秒,2.48毫秒vector<char>
迭代器和边界检查:2.97 ms,2.53 mschar[]
:1.52毫秒,1.25毫秒
同一台笔记本电脑,Visual C ++ 2010 64位编译器:
ostringstream
:48.6毫秒,45.0毫秒stringbuf
:16.2毫秒,16.0毫秒vector<char>
和back_inserter
:26.3 ms,26.5 msvector<char>
使用普通的迭代器:0.87 ms,0.89 msvector<char>
迭代器和边界检查:0.99毫秒,0.99毫秒char[]
:1.25毫秒,1.24毫秒
编辑:都跑了两次,以查看结果的一致性。相当一致的海事组织。
注意:在笔记本电脑上,由于我可以节省比ideone所允许的更多的CPU时间,因此我将所有方法的迭代次数设置为1000。这意味着,ostringstream
和vector
重新分配,这仅发生在第一轮,应该对最终结果的影响很小。
编辑:糟糕,在vector
-with-ordinary-iterator中发现了一个错误,该迭代器没有得到改进,因此缓存命中次数过多。我想知道vector<char>
表现如何char[]
。不过,它并没有太大的变化,vector<char>
仍然比char[]
VC ++ 2010 更快。
结论
每次附加数据时,输出流的缓冲需要三个步骤:
- 检查传入的块是否适合可用的缓冲区空间。
- 复制传入的块。
- 更新数据结束指针。
我发布的最新代码段“ vector<char>
简单迭代器加边界检查”不仅可以做到这一点,而且还可以分配额外的空间,并在传入的块不合适时移动现有数据。正如Clifford指出的那样,不必在文件I / O类中进行缓冲,而只需刷新当前缓冲区并重用它即可。因此,这应该是缓冲输出成本的上限。而这正是使内存缓冲区正常工作所需要的。
那么,为什么stringbuf
在Ideone上速度要慢2.5倍,而在测试时至少要慢10倍?在这个简单的微基准测试中并没有多态地使用它,因此并没有解释它。
std::ostringstream
不够聪明,无法以指数方式增加其缓冲区大小std::vector
,那是(A)愚蠢的(B)人们在思考I / O性能时应该考虑的事情。无论如何,缓冲区被重用,不会每次都重新分配。并且std::vector
还使用了动态增长的缓冲区。我想在这里公平。
ostringstream
并且想要尽可能快的性能,则应考虑直接使用stringbuf
。ostream
假定这些类通过rdbuf()
其虚拟功能接口将具有区域设置的格式化功能与灵活的缓冲区选择(文件,字符串等)联系在一起。如果您不进行任何格式化,那么与其他方法相比,额外级别的间接寻址肯定会成比例地昂贵。
ofstream
从而提高了阶数或幅度fprintf
。WinXPsp3上的MSVC 2008。iostreams太慢了。