C++标准是否要求iostreams性能差,还是我只是处理性能差的实现?
C++标准是否要求iostreams性能差,还是我只是处理性能差的实现?
每当我提到C++标准库iostreams的性能慢时,总会遭到不信任的浪潮。然而,我有分析器结果显示iostream库代码花费了大量时间(完全的编译器优化),而从iostreams切换到特定于操作系统的I/O API和自定义缓冲区管理确实提供了一个数量级的改进。
C++标准库到底在做什么额外的工作,是否符合标准,并且在实践中是否有用?还是某些编译器提供了与手动缓冲区管理竞争的iostreams实现?
基准测试
为了推动事情发展,我编写了几个简短的程序来练习iostreams内部缓冲区:
- 将二进制数据放入ostringstream中
- 将二进制数据放入char[]缓冲区中
- 使用back_inserter将二进制数据放入< vector</ code>中
- 新增:vector简单迭代器
- 新增:将二进制数据直接放入stringbuf中
- 新增:vector简单迭代器加边界检查
请注意,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,未知的OS和硬件):
ostringstream
:53毫秒stringbuf
:27毫秒vector
和back_inserter
:17.6毫秒vector
使用普通迭代器:10.6毫秒vector
迭代器和边界检查: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
和back_inserter
:34.6毫秒,34.4毫秒vector
使用普通迭代器:1.10毫秒,1.04毫秒vector
迭代器和边界检查:1.11毫秒,0.87毫秒,1.12毫秒,0.89毫秒,1.02毫秒,1.14毫秒char[]
:1.48毫秒,1.57毫秒
使用Profile-Guided Optimization的Visual C++ 2010 x86:cl / Ox / EHsc / GL / c
,link / ltcg:pgi
,运行,link / ltcg :pgo
,测量:
ostringstream
:61.2毫秒,60.5毫秒vector
使用普通迭代器:1.04毫秒,1.03毫秒
同一台笔记本电脑,相同的操作系统,使用cygwin gcc 4.3.4 g ++ -O3
:
ostringstream
:62.7毫秒,60.5毫秒stringbuf
:44.4毫秒,44.5毫秒vector
和back_inserter
:13.5毫秒,13.6毫秒vector
使用普通迭代器:4.1毫秒,3.9毫秒vector
迭代器和边界检查:4.0毫秒,4.0毫秒char[]
:3.57毫秒,3.75毫秒
同一台笔记本电脑,使用Visual C++ 2008 SP1:cl / Ox / EHsc
:
ostringstream
:88.7毫秒,87.6毫秒stringbuf
:23.3毫秒,23.4毫秒vector
和back_inserter
:26.1毫秒,24.5毫秒vector
使用普通迭代器:3.13毫秒,2.48毫秒vector
迭代器和边界检查:2.97毫秒,2.53毫秒char[]
:1.52毫秒,1.25毫秒
同一台笔记本电脑,使用Visual C++ 2010 64位编译器:
ostringstream
: 48.6毫秒, 45.0毫秒stringbuf
: 16.2毫秒, 16.0毫秒vector
和back_inserter
: 26.3毫秒, 26.5毫秒vector
使用普通迭代器: 0.87毫秒, 0.89毫秒vector
迭代器和边界检查: 0.99毫秒, 0.99毫秒char[]
: 1.25毫秒, 1.24毫秒
EDIT: 执行了两次,以查看结果的一致性。在我看来结果很一致。
注意:在我的笔记本电脑上,由于它可以节省更多的CPU时间,而ideone允许我设置所有方法的迭代次数为1000。这意味着只有在第一次通过时才进行的ostringstream
和vector
重新分配对最终结果应该没有什么影响。
EDIT: 错误,发现vector
-with-ordinary-iterator中有一个错误,迭代器没有被提前,因此缓存命中率过高。我想知道为什么vector
的性能要优于char[]
。虽然几乎没有任何影响,但是在VC++2010下,vector
仍然比char[]
更快。
结论
每次追加数据时,输出流的缓存需要三个步骤:
- 检查传入的块是否适合可用的缓冲区空间。
- 复制传入的块。
- 更新数据结束指针。
我发布的最新代码片段,\"vector
simple iterator plus bounds check\"不仅可以执行这个操作,还可以在传入的块不适合时分配额外的空间并移动现有的数据。正如Clifford指出的那样,在文件I/O类中进行缓存不必这样做,它只需刷新当前缓存并重复使用它。因此,这应该是缓存输出成本的上限。这正是制作工作内存缓存所需的。
那么,为什么在ideone上,stringbuf
的速度要慢2.5倍,而在我的测试中至少要慢10倍呢?在这个简单的微基准测试中,并没有以多态方式使用它,因此这并不能解释它的速度问题。
我对Visual Studio的用户们感到相当失望,因为他们在这个问题上容易犯错:\n在Visual Studio中实现的ostream
中,sentry
对象(按照标准要求)进入一个关键段来保护streambuf
(这不是必需的)。这似乎不是可选的,所以即使是单线程使用的本地流也需要付出线程同步的代价,而这种情况并不需要同步。\n这对使用ostringstream
格式化消息的代码造成了相当严重的影响。直接使用stringbuf
可以避免使用sentry
,但是格式化插入运算符不能直接作用于streambuf
。对于Visual C++ 2010,关键段使得ostringstream::write
比底层的stringbuf::sputn
调用慢了三倍。\n通过查看beldaz在newlib上的分析器数据,很明显,gcc的sentry
并没有像这样做出任何疯狂的行为。在gcc下,ostringstream::write
仅比stringbuf::sputn
慢约50%,但stringbuf
本身比VC ++下要慢得多。而两者都相对于使用vector
进行I/O缓冲来说还是很不利的,尽管不像在VC++下差距那么大。
不是回答你问题的具体细节,而是标题:2006年的 C++性能技术报告 中有一个有趣的IOStreams部分(第68页)。与你的问题最相关的部分在第6.1.2节(“执行速度”)中:
由于IOStreams处理的某些方面是分布在多个facets上的,因此似乎标准规定了一种低效的实现。但这不是事实——通过使用某种形式的预处理,许多工作可以避免。通过使用比通常使用的稍微聪明一些的链接器,可以去除其中一些低效性。这在§6.2.3和§6.2.5中讨论。
由于该报告是2006年编写的,人们希望许多建议已经被纳入了当前的编译器中,但也许不是这样。
如你所述,facets可能不出现在write()
中(但我不会盲目地假设这一点)。那么出现了什么?对使用GCC编译的ostringstream
代码运行GProf,会得到以下分解:
- 44.23%在
std::basic_streambuf
中::xsputn(char const *,int) - 34.62%在
std::ostream::write(char const *,int)
中 - 12.50%在
main
中 - 6.73%在
std::ostream::sentry::sentry(std::ostream &)
中 - 0.96%在
std::string::_M_replace_safe(unsigned int,unsigned int,char const *,unsigned int)
中 - 0.96%在
std::basic_ostringstream
中::basic_ostringstream(std::_Ios_Openmode) - 0.00%在
std::fpos
中::fpos(long long)
因此,大部分时间都花费在xsputn
上,最终在大量检查和更新游标位置和缓冲区后调用std::copy()
(在
我看这个问题是你把注意力放在了最坏情况上。如果你处理的是相当大的数据块,那么所执行的所有检查只是总工作量的一小部分。但是,你的代码每次以四个字节来移动数据,并产生了所有额外的成本。显然,在现实应用中要避免这样的情况——考虑一下如果将 "write" 调用在一个包含了 1 百万个整数的数组上而不是在一个整数上调用 1 百万次,惩罚会是多么微不足道。在实际应用中,人们真正欣赏IOStreams的重要特征,即其内存安全和类型安全的设计。这些好处是有代价的,而你编写的测试使这些成本支配了执行时间。