C ++标准是否要求iostream的性能很差,或者我只是处理一个糟糕的实现?
每当我提到C ++标准库iostream的性能下降时,我都会有一阵怀疑的感觉。 然而,我的profiler结果显示在iostream库代码中花费了大量的时间(完全编译器优化),并且从iostreams切换到特定于OS的I / O API和自定义缓冲区pipe理确实给了一个数量级的改进。
C ++标准库在做什么额外的工作,这是标准所要求的,在实践中是否有用? 还是做一些编译器提供与手动缓冲区pipe理相竞争的iostream的实现?
基准
为了让问题得以解决,我写了一些简短的程序来实现iostream的内部缓冲:
- 把二进制数据放入
ostringstream
http://ideone.com/2PPYw - 把二进制数据放入
char[]
缓冲区http://ideone.com/Ni5ct - 使用
back_inserter
http://ideone.com/Mj2Fi将二进制数据放入一个vector<char>
- 新 :
vector<char>
简单迭代器http://ideone.com/9iitv - 新增function :将二进制数据直接放入
stringbuf
http://ideone.com/qc9QA - 新 :
vector<char>
简单的迭代器加边界检查http://ideone.com/YyrKy
请注意, ostringstream
和stringbuf
版本运行较less,因为它们速度较慢。
在ideone上, ostringstream
比std:copy
+ back_inserter
+ std::vector
慢大约3倍,比memcpy
慢大约15倍。 当我将我的真实应用程序切换到自定义缓冲时,这与之前和之后的分析感觉一致。
这些都是内存中的缓冲区,所以iostreams的缓慢不能归咎于慢速磁盘I / O,太多的冲洗,与stdio同步,或任何其他的事情人们用来原谅慢C ++标准库iostream的。
能够看到其他系统的基准testing以及常见实现的评论(如gcc的libc ++,Visual C ++,Intel C ++)以及标准要求的开销是多less。
这个testing的基本原理
许多人正确地指出,iostream更常用于格式化输出。 但是,它们也是二进制文件访问的C ++标准提供的唯一现代API。 但是,对内部缓冲进行性能testing的真正原因适用于典型的格式化I / O:如果iostream不能保持磁盘控制器提供的原始数据,那么当它们负责格式化时,它们如何保持可用?
基准时间
所有这些都是外部( k
)循环的迭代。
关于ideone(gcc-4.3.4,未知操作系统和硬件):
-
ostringstream
:53毫秒 -
stringbuf
:27毫秒 -
vector<char>
和back_inserter
:17.6 ms -
vector<char>
与普通的迭代器:10.6毫秒 -
vector<char>
迭代器和边界检查:11.4 ms -
char[]
:3.7 ms
在我的笔记本电脑上(Visual C ++ 2010 x86, cl /Ox /EHsc
,Windows 7 Ultimate 64位,Intel Core cl /Ox /EHsc
RAM):
-
ostringstream
:73.4毫秒,71.6毫秒 -
stringbuf
:21.7毫秒,21.3毫秒 -
vector<char>
和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,configuration文件引导优化cl /Ox /EHsc /GL /c
, link /ltcg:pgi
,run, link /ltcg:pgo
,measure:
-
ostringstream
:61.2ms,60.5ms -
vector<char>
与普通的迭代器:1.04毫秒,1.03毫秒
相同的笔记本电脑,相同的操作系统,使用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 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毫秒,87.6毫秒 -
stringbuf
:23.3ms,23.4ms -
vector<char>
和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毫秒,45.0毫秒 -
stringbuf
:16.2毫秒,16.0毫秒 -
vector<char>
和back_inserter
:26.3 ms,26.5 ms -
vector<char>
与普通迭代器:0.87毫秒,0.89毫秒 -
vector<char>
迭代器和边界检查:0.99 ms,0.99 ms -
char[]
:1.25 ms,1.24 ms
编辑:跑了两遍,看看结果如何一致。 非常一致的IMO。
注:在我的笔记本电脑上,由于我可以节省更多的CPU时间比ideone允许,我设置的迭代次数为1000所有方法。 这意味着ostringstream
和vector
重新分配只会在第一遍时发生,对最终结果应该没有什么影响。
编辑:哎呀,发现在vector
与普通迭代器中的错误,迭代器没有被高级,因此有太多的caching命中。 我想知道vector<char>
是如何超越char[]
。 尽pipe如此,在VC ++ 2010下, vector<char>
仍然比char[]
更快。
结论
每次输出数据时,输出stream的缓冲需要三个步骤:
- 检查传入块是否适合可用的缓冲区空间。
- 复制传入块。
- 更新数据结束指针。
我发布的最新代码片段“ vector<char>
简单的迭代器加边界检查”不仅仅是这样做的,它还分配额外的空间并在传入的块不适合时移动现有的数据。 正如Clifford所指出的那样,缓冲在一个文件I / O类中不需要这样做,它只会刷新当前的缓冲区并重用它。 所以这应该是缓冲输出成本的上限。 这正是内存缓冲区工作所需要的。
那么为什么stringbuf在ideone上速度要慢2.5倍,而在我testing时至less慢10倍呢? 它在这个简单的微基准testing中并没有被多形地使用,所以没有解释它。
没有像标题那样回答你的问题的具体情况:2006 年的C ++技术性能技术报告对IOStreams有一个有趣的部分(p.68)。 与您的问题最相关的是在第6.1.2节(“执行速度”):
由于IOStream处理的某些方面分布在多个方面,标准似乎要求执行效率低下。 但事实并非如此 – 通过使用某种forms的预处理,大部分工作都可以避免。 使用比通常使用的稍微更智能的链接器,可以消除其中的一些低效率。 这在第6.2.3和第6.2.5节中讨论。
由于该报告是在2006年编写的,人们希望这些build议中的许多将会被纳入当前的编译器,但也许情况并非如此。
正如你所提到的,方面可能不会write()
在write()
(但我不会盲目地假设)。 那么function是什么? 在使用GCC编译的ostringstream
代码上运行GProf会产生以下故障:
-
std::basic_streambuf<char>::xsputn(char const*, int)
44.23%std::basic_streambuf<char>::xsputn(char const*, int)
- 在
std::ostream::write(char const*, int)
占34.62% - 12.50%
main
- 6.73%在
std::ostream::sentry::sentry(std::ostream&)
- 在
std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int)
0.96% - 0.96%在
std::basic_ostringstream<char>::basic_ostringstream(std::_Ios_Openmode)
- 0.00%在
std::fpos<int>::fpos(long long)
所以大部分时间都花在了xsputn
,最终在对光标位置和缓冲区进行大量检查和更新之后调用了std::copy()
(有关详细信息,请参阅c++\bits\streambuf.tcc
)。
我认为这是你关注最糟糕的情况。 如果你处理的数据量相当大,那么执行的所有检查只占总工作量的一小部分。 但是,你的代码每次只能以四个字节移动数据,并且每次都会产生额外的成本。 显然,在现实生活中,人们会避免这样做 – 考虑如果在一个1m的数组上调用write
,而不是在一个int上的1m次上调用write
则可以忽略不计。 在真实的情况下,人们会非常欣赏IOStreams的重要特性,即其内存安全和types安全的devise。 这样的好处是有代价的,而且你已经写了一个testing,使得这些成本占据了执行时间的主导地位。
我对那里的Visual Studio用户感到非常失望,他们对此感到厌烦:
- 在
ostream
的Visual Studio实现中,sentry
对象(标准所要求的)进入保护streambuf
(这不是必需的)的关键部分。 这似乎不是可选的,所以即使对于单个线程使用的本地stream,也要花费线程同步的代价,而不需要同步。
这伤害了使用ostringstream
来严格格式化消息的代码。 使用stringbuf
直接避免了使用sentry
,但格式化的插入操作符不能直接在streambuf
上工作。 对于Visual C ++ 2010来说,关键的部分是将ostringstream::write
放缓三倍,而不是基本的stringbuf::sputn
调用。
看看beldaz在newlib上的profiler数据 ,似乎很清楚,gcc的sentry
并没有像这样疯狂。 在gcc下的ostringstream::write
只比stringbuf::sputn
长50%,但是stringbuf::sputn
本身比在VC ++下慢得多。 而且两者仍然比较不利地使用一个vector<char>
进行I / O缓冲,尽pipe与VC ++不同。
你看到的问题是每个调用write()的开销。 您添加的每个抽象级别(char [] – > vector – > string – > ostringstream)会添加一些更多的函数调用/返回和其他内务处理,如果您将其称为一百万次 – 则累加起来。
我修改了两个关于ideone的例子,一次写十个整数。 ostringstream时间从53到6毫秒(几乎提高了10倍),而字符循环改进了(3.7到1.5) – 有用,但只有两个因素。
如果你关心性能,那么你需要select正确的工具。 ostringstream是有用和灵活的,但有一个罚款,以你想要的方式使用它。 char []是更难的工作,但性能增益可以是伟大的(记住gcc也可能为你插入memcpys)。
简而言之,ostringstream不会被破坏,但是越接近金属,代码运行得越快。 汇编对于一些人来说还是有优势的。
为了获得更好的性能,您必须了解您使用的容器的工作方式。 在你的char []数组示例中,所需大小的数组是事先分配的。 在你的vector和ostringstream例子中,当对象增长的时候,你迫使对象多次分配,重新分配和复制数据。
与std :: vector一样,通过初始化vector的大小来达到最终的大小,就像char数组一样。 相反,你不公平地削减performance,调整为零! 这不是一个公平的比较。
关于ostringstream,预先分配空间是不可能的,我build议这是一个inappropruate使用。 这个类比简单的char数组有更大的实用性,但是如果你不需要这个实用工具,那就不要使用它,因为在任何情况下你都会支付开销。 相反,它应该用于它的好处 – 将数据格式化为string。 C ++提供了大量的容器,而ostringstram是最不适合这个用途的。
在向量和ostringstream的情况下,你可以得到缓冲区溢出保护,你不会得到一个char数组,并且这个保护不是免费的。