提问人: 提问时间:12/3/2010 最后编辑:18 revs, 3 users 79%Ben Voigt 更新时间:9/18/2014 访问量:26217
C++ 标准是否要求 iostream 性能不佳,或者我只是在处理糟糕的实现?
Does the C++ standard mandate poor performance for iostreams, or am I just dealing with a poor implementation?
问:
每次我提到 C++ 标准库 iostreams 的性能缓慢时,我都会遇到一波难以置信的浪潮。然而,我的分析器结果显示,在iostream库代码(完整的编译器优化)上花费了大量时间,并且从iostreams切换到特定于操作系统的I/O API和自定义缓冲区管理确实提供了数量级的改进。
C++标准库做了哪些额外的工作,标准是否要求,在实践中是否有用?或者某些编译器是否提供了与手动缓冲区管理相媲美的 iostreams 实现?
基准
为了让事情顺利进行,我编写了几个简短的程序来练习 iostreams 内部缓冲:
- 将二进制数据放入 http://ideone.com/2PPYw
ostringstream
- 将二进制数据放入缓冲区 http://ideone.com/Ni5ct
char[]
- 将二进制数据放入使用 http://ideone.com/Mj2Fi
vector<char>
back_inserter
- NEW:简单迭代器 http://ideone.com/9iitv
vector<char>
- NEW:将二进制数据直接放入 http://ideone.com/qc9QA
stringbuf
- NEW:简单迭代器加边界检查 http://ideone.com/YyrKy
vector<char>
请注意,和 版本运行的迭代次数较少,因为它们的速度要慢得多。ostringstream
stringbuf
在 ideone 上,它比 + + 慢约 3 倍,比进入原始缓冲区慢约 15 倍。这与我将实际应用程序切换到自定义缓冲时的前后分析一致。ostringstream
std:copy
back_inserter
std::vector
memcpy
这些都是内存缓冲区,因此 iostream 的缓慢不能归咎于磁盘 I/O 缓慢、刷新过多、与 stdio 同步或人们用来为观察到的 C++ 标准库 iostream 缓慢找借口的任何其他事情。
很高兴看到其他系统的基准测试和对常见实现所做的事情的评论(例如gcc的libc++,Visual C++,Intel C++)以及标准规定的开销。
此测试的基本原理
许多人正确地指出,iostreams 更常用于格式化输出。但是,它们也是 C++ 标准为二进制文件访问提供的唯一现代 API。但是,对内部缓冲进行性能测试的真正原因适用于典型的格式化 I/O:如果 iostream 无法保持磁盘控制器提供原始数据,那么当它们也负责格式化时,它们怎么可能跟上?
基准时序
所有这些都是外部 () 循环的迭代。k
在 ideone(gcc-4.3.4,未知操作系统和硬件)上:
ostringstream
:53 毫秒stringbuf
: 27 毫秒vector<char>
和 : 17.6 msback_inserter
vector<char>
使用普通迭代器:10.6 msvector<char>
迭代器和边界检查:11.4 mschar[]
: 3.7 毫秒
在我的笔记本电脑(Visual C++ 2010 x86,Windows 7 Ultimate 64位,英特尔酷睿i7,8 GB RAM)上:cl /Ox /EHsc
ostringstream
:73.4 毫秒,71.6 毫秒stringbuf
: 21.7 毫秒, 21.3 毫秒vector<char>
和 : 34.6 ms, 34.4 msback_inserter
vector<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 毫秒
可视化 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>
和 : 13.5 ms, 13.6 msback_inserter
vector<char>
普通迭代器:4.1 ms、3.9 msvector<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>
和 : 26.1 ms, 24.5 msback_inserter
vector<char>
普通迭代器:3.13 ms、2.48 msvector<char>
迭代器和边界检查:2.97 毫秒、2.53 毫秒char[]
:1.52 毫秒、1.25 毫秒
同一台笔记本电脑,Visual C++ 2010 64位编译器:
ostringstream
: 48.6 毫秒, 45.0 毫秒stringbuf
: 16.2 毫秒, 16.0 毫秒vector<char>
和 : 26.3 ms, 26.5 msback_inserter
vector<char>
普通迭代器:0.87 ms、0.89 msvector<char>
迭代器和边界检查:0.99 ms、0.99 mschar[]
: 1.25 毫秒, 1.24 毫秒
编辑:运行了两次,看看结果的一致性如何。相当一致的IMO。
注意:在我的笔记本电脑上,由于我可以节省比 ideone 允许的更多的 CPU 时间,因此我将所有方法的迭代次数设置为 1000。这意味着仅在第一次通过时进行的重新分配对最终结果的影响应该很小。ostringstream
vector
编辑:哎呀,在-with-ordinary-iterator中发现了一个错误,迭代器没有被推进,因此有太多的缓存命中。我想知道表现如何.不过,它没有太大区别,仍然比 VC++ 2010 更快。vector
vector<char>
char[]
vector<char>
char[]
结论
每次追加数据时,输出流的缓冲都需要三个步骤:
- 检查传入块是否适合可用的缓冲区空间。
- 复制传入块。
- 更新数据结束指针。
我发布的最新代码片段“简单迭代器加边界检查”不仅可以做到这一点,还可以分配额外的空间,并在传入块不合适时移动现有数据。正如 Clifford 所指出的,在文件 I/O 类中缓冲不必这样做,它只会刷新当前缓冲区并重用它。因此,这应该是缓冲输出成本的上限。而这正是制作一个有效的内存缓冲区所需要的。vector<char>
那么,为什么 ideone 的速度慢了 2.5 倍,而测试时速度至少慢了 10 倍呢?在这个简单的微基准测试中,它没有被多态使用,所以这并不能解释它。stringbuf
答:
您看到的问题全部出在每次调用 write() 的开销上。您添加的每个抽象级别(char[] ->vector -> string -> ostringstream)都会添加一些额外的函数调用/返回和其他内务处理 guff,如果您调用它一百万次,它们就会加起来。
我在 ideone 上修改了两个示例,一次写 10 个整数。ostringstream 时间从 53 毫秒增加到 6 毫秒(几乎提高了 10 倍),而 char 循环得到了改善(3.7 倍到 1.5 倍)——很有用,但只有两倍。
如果您关心性能,那么您需要为工作选择合适的工具。ostringstream 是有用且灵活的,但以您尝试的方式使用它会受到惩罚。char[] 的工作更难,但性能提升可能很大(请记住,GCC 可能也会为您内联 memcpys)。
简而言之,ostringstream 没有被破坏,但你越接近金属,你的代码就会运行得越快。汇编程序对某些人来说仍然具有优势。
评论
ostringstream::write()
vector::push_back()
ostringstream
std::vector
stringbuf
stringbuf
sputn
xsputn
xsputn
sputn
xsputn
为了获得更好的性能,您必须了解您使用的容器是如何工作的。在 char[] 数组示例中,预先分配了所需大小的数组。在 vector 和 ostringstream 示例中,您将强制对象重复分配和重新分配,并可能随着对象的增长多次复制数据。
使用 std::vector,可以通过像使用 char 数组一样将向量的大小初始化为最终大小来轻松解决此问题;相反,您通过将大小调整为零来不公平地削弱性能!这几乎不是一个公平的比较。
关于 ostringstream,预先分配空间是不可能的,我认为这是一种不恰当的使用。该类的实用性比简单的 char 数组大得多,但如果你不需要该实用性,那就不要使用它,因为无论如何你都会支付开销。相反,它应该用于它的好处 - 将数据格式化为字符串。C++ 提供了广泛的容器,而 ostringstram 是最不适合此目的的容器之一。
在向量和 ostringstream 的情况下,您可以获得缓冲区溢出保护,而 char 数组则无法获得保护,并且这种保护不是免费提供的。
评论
ostringstream.str.reserve(4000000)
ostringstream
ostringstream str(string(1000000 * sizeof(int), '\0'));
vector
resize
vector[]
vector.at()
vector<T>::resize(0)
通常不会重新分配内存
operator[]
push_back()
back_inserter
push_back
与其说是回答你的问题的细节,不如说是标题:2006 年 C++ 性能技术报告有一个有趣的部分关于 IOStreams(第 68 页)。与您的问题最相关的是第 6.1.2 节(“执行速度”):
由于 IOStreams 处理的某些方面是 分布在多个方面,它 该标准似乎要求 实施效率低下。但是这个 事实并非如此——通过使用某种形式 的预处理,大部分工作可以 避免。用稍微聪明一点 链接器比通常使用的链接器,它是 可以删除其中的一些 效率 低下。这将在 §6.2.3 和 §6.2.5。
由于该报告是在2006年编写的,人们希望许多建议能够被纳入目前的汇编程序,但也许情况并非如此。
正如你所提到的,分面可能不会出现在其中(但我不会盲目地假设)。那么有什么特点呢?在使用 GCC 编译的代码上运行 GProf 会给出以下细分:write()
ostringstream
- 44.23% 在
std::basic_streambuf<char>::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<char>::basic_ostringstream(std::_Ios_Openmode)
- 0.00% 在
std::fpos<int>::fpos(long long)
因此,大部分时间都花在了 中,在对光标位置和缓冲区进行大量检查和更新后,最终会调用(请查看详细信息)。xsputn
std::copy()
c++\bits\streambuf.tcc
我对此的看法是,您已经关注了最坏的情况。如果您要处理相当大的数据块,则执行的所有检查将只占已完成总工作的一小部分。但是您的代码一次以四个字节为单位移动数据,并且每次都会产生所有额外的成本。显然,在现实生活中,人们会避免这样做——想想如果在 1m int 的数组上调用而不是在一个 int 上调用 1m 次,惩罚会是多么微不足道。在现实生活中,人们会真正欣赏IOStreams的重要功能,即其内存安全和类型安全设计。这些好处是有代价的,你已经编写了一个测试,使这些成本在执行时间中占主导地位。write
评论
ostream::write()
sizeof i
int
xsputn
stream << "VAR: " << var.x << ", " << var.y << endl;
xsputn
operator <<
我对那里的 Visual Studio 用户相当失望,他们在这个问题上有一个噱头:
- 在 的 Visual Studio 实现中,对象(标准要求)进入保护 (不是必需的) 的关键部分。这似乎不是可选的,因此即使对于单个线程使用的本地流,也需要支付线程同步的成本,这不需要同步。
ostream
sentry
streambuf
这会严重损害用于格式化消息的代码。使用 direct 可以避免使用 ,但格式化的插入运算符不能直接在 s 上工作。对于 Visual C++ 2010,关键部分的速度比基础调用慢了三倍。ostringstream
stringbuf
sentry
streambuf
ostringstream::write
stringbuf::sputn
看看 beldaz 在 newlib 上的分析器数据,似乎很明显 gcc 并没有做任何这样疯狂的事情。 在 gcc 下只比 长 50% 左右,但本身比 VC++ 慢得多。与使用 a 进行 I/O 缓冲相比,两者仍然非常不利,尽管与 VC++ 下的余量不同。sentry
ostringstream::write
stringbuf::sputn
stringbuf
vector<char>
评论
sentry
上一个:你能解释一的概念吗?
评论
std::ostringstream
std::vector
std::vector
ostringstream
stringbuf
ostream
rdbuf()
ofstream
fprintf