Does the C++ standard mandate poor performance for iostreams, or am I just dealing with a poor implementation?
每当我提到C++标准库IOFROM的慢速性能时,我就会遇到一种不信任的浪潮。不过,我有一个探查器结果显示,在iostream库代码(完整的编译器优化)中花费了大量时间,从iostreams切换到操作系统特定的I/O API和自定义缓冲区管理确实带来了一个数量级的改进。
C++标准库所做的额外工作是标准要求的,在实践中有用吗?或者,一些编译器是否提供了与手动缓冲区管理竞争的iostream实现?
基准点为了让事情顺利进行,我编写了几个简短的程序来练习iostreams内部缓冲:
- 将二进制数据放入
ostringstream http://ideone.com/2ppyw - 将二进制数据放入
char[] 缓冲区http://ideone.com/ni5ct - 使用
back_inserter http://ideone.com/mj2fi将二进制数据放入vector 中 - 新增:
vector 简单迭代器http://ideone.com/9iitv - 新增:将二进制数据直接放入
stringbuf http://ideone.com/qc9qa - 新增:
vector 简单迭代器加边界检查http://ideone.com/yyrky
注意,
在ideone中,
这些都是内存缓冲区,因此慢流的慢度不能归咎于慢速磁盘I/O、过多的刷新、与STDIO同步,或者人们用来解释C++标准库IOSWATH慢速的任何其他事情。
很高兴看到其他系统上的基准和对常见实现(比如GCC的LBC+++、Visual C++、英特尔C++)的注释,以及标准规定的开销有多大。
本试验的基本原理许多人正确地指出iostreams更常用于格式化输出。然而,它们也是由C++标准提供的唯一的二进制文件访问的现代API。但是,对内部缓冲区进行性能测试的真正原因适用于典型的格式化I/O:如果iostreams不能保持磁盘控制器提供原始数据,那么在它们负责格式化时,它们又如何能够保持这种状态呢?
基准时间所有这些都是外部(
在IDeone上(GCC-4.3.4,未知操作系统和硬件):
ostringstream 53毫秒stringbuf 27毫秒vector 和back_inserter 17.6 ms- 带普通迭代器的
vector :10.6 ms vector 迭代器和边界检查:11.4mschar[] 3.7毫秒
在我的笔记本电脑上(Visual C++ 2010 x86,EDCOX1×22),Windows 7最终64位,英特尔内核I7,8 GB RAM):
ostringstream :73.4毫秒,71.6毫秒stringbuf :21.7 ms,21.3 msvector 和back_inserter :34.6 ms,34.4 ms- 带普通迭代器的
vector :1.10 ms,1.04 ms vector 迭代器和边界检查:1.11 ms、0.87 ms、1.12 ms、0.89 ms、1.02 ms、1.14 mschar[] :1.48 ms,1.57 ms
Visual C++ 2010 x86,用轮廓引导优化EDOCX1,30,EDOCX1,31,,运行,EDOCX1,32,测量:
ostringstream :61.2 ms,60.5 ms- 带普通迭代器的
vector :1.04 ms,1.03 ms
使用Cygwin GCC 4.3.4
ostringstream 62.7 ms,60.5 msstringbuf 44.4 ms,44.5 msvector 和back_inserter 13.5 ms,13.6 ms- 带普通迭代器的
vector :4.1 ms,3.9 ms vector 迭代器和边界检查:4.0 ms,4.0 mschar[] 3.57 ms,3.75 ms
相同的笔记本电脑,Visual C++ 2008 SP1,EDOCX1,22:
ostringstream 88.7 ms,87.6 msstringbuf :23.3 ms,23.4 msvector 和back_inserter :26.1 ms,24.5 ms- 带普通迭代器的
vector :3.13 ms,2.48 ms vector 迭代器和边界检查: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 和back_inserter :26.3 ms,26.5 ms- 带普通迭代器的
vector :0.87 ms,0.89 ms vector 迭代器和边界检查:0.9没有回答你的问题的细节,就标题而言:2006个关于C++性能的技术报告有一个有趣的关于iOFFROW的章节(P.68)。与您的问题最相关的是第6.1.2节("执行速度"):
Since certain aspects of IOStreams processing are
distributed over multiple facets, it
appears that the Standard mandates an
inefficient implementation. But this
is not the case — by using some form
of preprocessing, much of the work can
be avoided. With a slightly smarter
linker than is typically used, it is
possible to remove some of these
inefficiencies. This is discussed in
§6.2.3 and §6.2.5.既然报告是2006年写的,人们希望许多建议都能被纳入当前的编纂者之中,但也许情况并非如此。
正如您所提到的,面可能在
write() 中不起作用(但我不会盲目假设)。那么特征是什么呢?在用gcc编译的ostringstream 代码上运行gprof给出了以下分解:std::basic_streambuf 44.23%::xsputn(char const*, int) std::ostream::write(char const*, int) 中34.62%- 12.50%在
main 中 std::ostream::sentry::sentry(std::ostream&) 中6.73%std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int) 中0.96%std::basic_ostringstream 中0.96%::basic_ostringstream(std::_Ios_Openmode) std::fpos 0.00%::fpos(long long)
因此,大部分时间都花在
xsputn 上,在大量检查和更新光标位置和缓冲区之后,最终调用std::copy() (请查看c++\bits\streambuf.tcc )。我的看法是,你关注的是最坏的情况。如果您处理的是相当大的数据块,那么所执行的所有检查只占全部工作的一小部分。但是您的代码一次以四个字节移动数据,每次都会产生额外的成本。很明显,在现实生活中我们可以避免这样做——考虑一下,如果
write 在一个1米整数的数组上被调用,而不是在一个1米整数上被调用,那么惩罚将是多么微不足道。在现实生活中,我们真的会欣赏iostream的重要特性,即它的内存安全和类型安全设计。这样的好处是有代价的,您已经编写了一个测试,使这些成本在执行时间中占主导地位。我对外面的Visual Studio用户感到非常失望,他们宁愿给我这个:
- 在
ostream 的Visual Studio实现中,sentry 对象(标准要求)进入保护streambuf 的关键部分(不要求)。这似乎不是可选的,因此即使对于单个线程使用的本地流(不需要同步),也要支付线程同步的成本。
这会严重损害使用
ostringstream 来格式化消息的代码。使用EDCOX1〔4〕直接避免使用EDCOX1〔1〕,但格式化的插入运算符不能直接作用于EDCOX1〔2〕。对于VisualC++ 2010,临界区使EDCOX1×7减慢三倍,与EDCOX1的8调用无关。从贝尔达兹在Newlib上的分析数据来看,很明显GCC的
sentry 并没有做任何疯狂的事情。GCC下的ostringstream::write 比stringbuf::sputn 长约50%,但stringbuf 本身比vc++慢得多。两种方法相比,使用vector 进行I/O缓冲仍然非常不利,尽管与在vc++下的幅度不同。您看到的问题都在每个write()调用的开销中。您添加的每一个抽象级别(char[]->vector->string->ostringstream)都添加了更多的函数调用/返回,以及其他一些家政方面的把戏,如果您调用了一百万次,这些把戏就会加起来。
我修改了IDeone上的两个例子,一次写十个整数。Ostringstream时间从53到6 ms(几乎是10倍的改善),而char循环改善(3.7到1.5)-有用,但只有2倍。
如果你很关心绩效,那么你需要为工作选择合适的工具。Ostringstream是有用的和灵活的,但是如果你想用它的方式使用它,就会受到惩罚。char[]是比较难的工作,但是性能的提高是很好的(记住GCC可能也会为您内联memcpys)。
简而言之,ostringstream没有被破坏,但是越接近金属,代码运行的越快。装配工对一些人仍然有优势。
为了获得更好的性能,您必须了解您使用的容器是如何工作的。在char[]数组示例中,需要大小的数组是预先分配的。在vector和ostringstream示例中,您强制对象重复分配和重新分配,并可能随着对象的增长多次复制数据。
对于std::vector,这可以通过将向量的大小初始化为最终大小来轻松解决,就像对char数组所做的那样;相反,通过将大小调整为零,会不公平地削弱性能!这可不是一个公平的比较。
关于排斥流,不可能预先分配空间,我建议这是一种不适当的使用。类的实用程序比简单的char数组大得多,但是如果您不需要这个实用程序,那么就不要使用它,因为在任何情况下都要支付开销。相反,它应该用于将数据格式化为字符串的目的。C++提供了广泛的容器,而OSTRUNSTRAM是最不合适的。
对于向量和ostringstream,您可以从缓冲区溢出中获得保护,但使用char数组并不能获得保护,而且这种保护也不是免费的。