C++ 标准是否要求 iostream 性能不佳,或者我只是在处理糟糕的实现?

Does the C++ standard mandate poor performance for iostreams, or am I just dealing with a poor implementation?

提问人: 提问时间:12/3/2010 最后编辑:18 revs, 3 users 79%Ben Voigt 更新时间:9/18/2014 访问量:26217

问:

每次我提到 C++ 标准库 iostreams 的性能缓慢时,我都会遇到一波难以置信的浪潮。然而,我的分析器结果显示,在iostream库代码(完整的编译器优化)上花费了大量时间,并且从iostreams切换到特定于操作系统的I/O API和自定义缓冲区管理确实提供了数量级的改进。

C++标准库做了哪些额外的工作,标准是否要求,在实践中是否有用?或者某些编译器是否提供了与手动缓冲区管理相媲美的 iostreams 实现?

基准

为了让事情顺利进行,我编写了几个简短的程序来练习 iostreams 内部缓冲:

请注意,和 版本运行的迭代次数较少,因为它们的速度要慢得多。ostringstreamstringbuf

在 ideone 上,它比 + + 慢约 3 倍,比进入原始缓冲区慢约 15 倍。这与我将实际应用程序切换到自定义缓冲时的前后分析一致。ostringstreamstd:copyback_inserterstd::vectormemcpy

这些都是内存缓冲区,因此 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 ms
  • vector<char>迭代器和边界检查:11.4 ms
  • char[]: 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 ms
  • vector<char>迭代器和边界检查:1.11 ms、0.87 ms、1.12 ms、0.89 ms、1.02 ms、1.14 ms
  • char[]:1.48 毫秒、1.57 毫秒

可视化 C++ 2010 x86,具有配置文件引导优化 、 运行 、 度量:cl /Ox /EHsc /GL /clink /ltcg:pgilink /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 ms
  • vector<char>迭代器和边界检查:4.0 ms、4.0 ms
  • char[]: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 ms
  • vector<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 ms
  • vector<char>迭代器和边界检查:0.99 ms、0.99 ms
  • char[]: 1.25 毫秒, 1.24 毫秒

编辑:运行了两次,看看结果的一致性如何。相当一致的IMO。

注意:在我的笔记本电脑上,由于我可以节省比 ideone 允许的更多的 CPU 时间,因此我将所有方法的迭代次数设置为 1000。这意味着仅在第一次通过时进行的重新分配对最终结果的影响应该很小。ostringstreamvector

编辑:哎呀,在-with-ordinary-iterator中发现了一个错误,迭代器没有被推进,因此有太多的缓存命中。我想知道表现如何.不过,它没有太大区别,仍然比 VC++ 2010 更快。vectorvector<char>char[]vector<char>char[]

结论

每次追加数据时,输出流的缓冲都需要三个步骤:

  • 检查传入块是否适合可用的缓冲区空间。
  • 复制传入块。
  • 更新数据结束指针。

我发布的最新代码片段“简单迭代器加边界检查”不仅可以做到这一点,还可以分配额外的空间,并在传入块不合适时移动现有数据。正如 Clifford 所指出的,在文件 I/O 类中缓冲不必这样做,它只会刷新当前缓冲区并重用它。因此,这应该是缓冲输出成本的上限。而这正是制作一个有效的内存缓冲区所需要的。vector<char>

那么,为什么 ideone 的速度慢了 2.5 倍,而测试时速度至少慢了 10 倍呢?在这个简单的微基准测试中,它没有被多态使用,所以这并不能解释它。stringbuf

C++ 性能 IOSTREAM

评论

24赞 Anon. 12/3/2010
您一次只写一百万个字符,想知道为什么它比复制到预分配的缓冲区慢?
23赞 Ben Voigt 12/3/2010
@Anon:我一次缓冲 400 万字节,是的,我想知道为什么这么慢。如果不够聪明,无法以指数方式增加其缓冲区大小,那是 (A) 愚蠢的,(B) 考虑 I/O 性能的人应该考虑的事情。无论如何,缓冲区会被重用,它不会每次都被重新分配。并且还使用动态增长的缓冲区。我在这里尽量公平。std::ostringstreamstd::vectorstd::vector
15赞 CB Bailey 12/3/2010
您实际上正在尝试对什么任务进行基准测试?如果您没有使用任何格式化功能,并且想要尽可能快的性能,那么您应该考虑直接使用 。这些类应该将区域设置感知格式功能与灵活的缓冲区选择(文件、字符串等)及其虚拟函数接口联系在一起。如果你不做任何格式化,那么与其他方法相比,这种额外的间接水平肯定会看起来成比例地昂贵。ostringstreamstringbufostreamrdbuf()
5赞 KitsuneYMG 12/3/2010
+1 表示 Truth OP。通过在输出涉及双精度的日志记录信息时从 移动到 来获得数量级或数量级的加速。WinXPsp3 上的 MSVC 2008。IOSTREAMS只是狗慢。ofstreamfprintf
6赞 Johannes Schaub - litb 12/3/2010
以下是委员会网站上的一些测试: open-std.org/jtc1/sc22/wg21/docs/D_5.cpp

答:

7赞 Roddy 12/3/2010 #1

您看到的问题全部出在每次调用 write() 的开销上。您添加的每个抽象级别(char[] ->vector -> string -> ostringstream)都会添加一些额外的函数调用/返回和其他内务处理 guff,如果您调用它一百万次,它们就会加起来。

我在 ideone 上修改了两个示例,一次写 10 个整数。ostringstream 时间从 53 毫秒增加到 6 毫秒(几乎提高了 10 倍),而 char 循环得到了改善(3.7 倍到 1.5 倍)——很有用,但只有两倍。

如果您关心性能,那么您需要为工作选择合适的工具。ostringstream 是有用且灵活的,但以您尝试的方式使用它会受到惩罚。char[] 的工作更难,但性能提升可能很大(请记住,GCC 可能也会为您内联 memcpys)。

简而言之,ostringstream 没有被破坏,但你越接近金属,你的代码就会运行得越快。汇编程序对某些人来说仍然具有优势。

评论

10赞 Ben Voigt 12/3/2010
有什么事情是没有的?如果有的话,它应该更快,因为它交给了一个块而不是四个单独的元素。如果比不提供任何附加功能慢,那么是的,我会称之为坏了。ostringstream::write()vector::push_back()ostringstreamstd::vector
1赞 Dragontamer5788 12/3/2010
@Ben Voigt:相反,它 vector 必须做一些事情,而 ostringstream 不必这样做,这使得 vector 在这种情况下更具性能。Vector 保证在内存中是连续的,而 ostringstream 则不是。Vector 是设计为高性能的类之一,而 ostringstream 则不是。
2赞 CB Bailey 12/3/2010
@Ben Voigt:直接使用不会删除所有函数调用,因为 的公共接口由基类中的公共非虚拟函数组成,然后调度到派生类中受保护的虚拟函数。stringbufstringbuf
2赞 Ben Voigt 12/3/2010
@Charles:在任何像样的编译器上,它都应该,因为公共函数调用将被内联到编译器已知动态类型的上下文中,它可以删除间接调用,甚至内联这些调用。
6赞 Ben Voigt 12/3/2010
@Roddy:我应该认为这都是内联模板代码,在每个编译单元中都可见。但我想这可能会因实施而异。可以肯定的是,我希望正在讨论的调用,即调用虚拟受保护的公共函数,是内联的。即使未内联,编译器也可以在内联时确定所需的确切重写,并在不通过 vtable 的情况下生成直接调用。sputnxsputnxsputnsputnxsputn
1赞 Clifford 12/3/2010 #2

为了获得更好的性能,您必须了解您使用的容器是如何工作的。在 char[] 数组示例中,预先分配了所需大小的数组。在 vector 和 ostringstream 示例中,您将强制对象重复分配和重新分配,并可能随着对象的增长多次复制数据。

使用 std::vector,可以通过像使用 char 数组一样将向量的大小初始化为最终大小来轻松解决此问题;相反,您通过将大小调整为零来不公平地削弱性能!这几乎不是一个公平的比较。

关于 ostringstream,预先分配空间是不可能的,我认为这是一种不恰当的使用。该类的实用性比简单的 char 数组大得多,但如果你不需要该实用性,那就不要使用它,因为无论如何你都会支付开销。相反,它应该用于它的好处 - 将数据格式化为字符串。C++ 提供了广泛的容器,而 ostringstram 是最不适合此目的的容器之一。

在向量和 ostringstream 的情况下,您可以获得缓冲区溢出保护,而 char 数组则无法获得保护,并且这种保护不是免费提供的。

评论

1赞 Roddy 12/3/2010
分配似乎不是 ostringstream 的问题。他只是在随后的迭代中寻求归零。无截断。我也试过了,但没什么区别。ostringstream.str.reserve(4000000)
0赞 Nim 12/3/2010
我认为 ,您可以通过传入一个虚拟字符串来“保留”,即:使用 ,不会释放任何空间,它只在需要时扩展。ostringstreamostringstream str(string(1000000 * sizeof(int), '\0'));vectorresize
1赞 Roddy 12/3/2010
“向量..防止缓冲区溢出”。一个常见的误解 - 默认情况下,通常不检查运算符的边界错误。 然而。vector[]vector.at()
2赞 Niki Yoshiuchi 12/3/2010
vector<T>::resize(0)通常不会重新分配内存
2赞 Ben Voigt 12/3/2010
@Roddy:不使用 ,而是(通过),这肯定会测试溢出。添加了另一个不使用 .operator[]push_back()back_inserterpush_back
51赞 beldaz 12/3/2010 #3

与其说是回答你的问题的细节,不如说是标题: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)

因此,大部分时间都花在了 中,在对光标位置和缓冲区进行大量检查和更新后,最终会调用(请查看详细信息)。xsputnstd::copy()c++\bits\streambuf.tcc

我对此的看法是,您已经关注了最坏的情况。如果您要处理相当大的数据块,则执行的所有检查将只占已完成总工作的一小部分。但是您的代码一次以四个字节为单位移动数据,并且每次都会产生所有额外的成本。显然,在现实生活中,人们会避免这样做——想想如果在 1m int 的数组上调用而不是在一个 int 上调用 1m 次,惩罚会是多么微不足道。在现实生活中,人们会真正欣赏IOStreams的重要功能,即其内存安全和类型安全设计。这些好处是有代价的,你已经编写了一个测试,使这些成本在执行时间中占主导地位。write

评论

0赞 Ben Voigt 12/3/2010
对于未来关于格式化插入/提取 iostreams 性能的问题来说,这听起来像是很好的信息,我可能很快就会问这个问题。但我不相信有任何方面涉及.ostream::write()
4赞 Ben Voigt 12/3/2010
+1 用于分析(我猜这是一台 Linux 机器?但是,我实际上一次添加四个字节(实际上,但我正在测试的所有编译器都有 4 字节)。这对我来说似乎并不是那么不切实际,您认为在典型代码中每次调用都会传递多大的块,例如 .sizeof iintxsputnstream << "VAR: " << var.x << ", " << var.y << endl;
42赞 Ben Voigt 12/3/2010
@beldaz:那个只调用五次的“典型”代码示例很可能位于写入 1000 万行文件的循环中。与我的基准代码相比,将数据以大块形式传递给 iostream 的实际场景要少得多。为什么我必须以最少的调用次数写入缓冲流?如果我必须自己做缓冲,那么 iostreams 有什么意义呢?对于二进制数据,我可以选择自己缓冲它,当将数百万个数字写入文本文件时,批量选项根本不存在,我必须调用每个数字。xsputnoperator <<
1赞 Ben Voigt 12/3/2010
@beldaz:可以通过简单的计算来估计 I/O 何时开始占主导地位。在 90 MB/s 的平均写入速率(当前消费级硬盘的典型值)下,刷新 4MB 缓冲区需要 <45 毫秒(吞吐量,由于操作系统写入缓存,延迟并不重要)。如果运行内部循环所需的时间比填充缓冲区的时间长,则 CPU 将是限制因素。如果内部循环运行得更快,那么 I/O 将是限制因素,或者至少还剩下一些 CPU 时间来完成真正的工作。
5赞 Ben Voigt 12/3/2010
当然,这并不意味着使用 iostreams 就一定意味着程序很慢。如果 I/O 只是程序的一小部分,那么使用性能较差的 I/O 库不会产生太大的整体影响。但是,不经常被调用并不等同于良好的性能,在 I/O 繁重的应用程序中,它确实很重要。
27赞 2 revsBen Voigt #4

我对那里的 Visual Studio 用户相当失望,他们在这个问题上有一个噱头:

  • 在 的 Visual Studio 实现中,对象(标准要求)进入保护 (不是必需的) 的关键部分。这似乎不是可选的,因此即使对于单个线程使用的本地流,也需要支付线程同步的成本,这不需要同步。ostreamsentrystreambuf

这会严重损害用于格式化消息的代码。使用 direct 可以避免使用 ,但格式化的插入运算符不能直接在 s 上工作。对于 Visual C++ 2010,关键部分的速度比基础调用慢了三倍。ostringstreamstringbufsentrystreambufostringstream::writestringbuf::sputn

看看 beldaz 在 newlib 上的分析器数据,似乎很明显 gcc 并没有做任何这样疯狂的事情。 在 gcc 下只比 长 50% 左右,但本身比 VC++ 慢得多。与使用 a 进行 I/O 缓冲相比,两者仍然非常不利,尽管与 VC++ 下的余量不同。sentryostringstream::writestringbuf::sputnstringbufvector<char>

评论

0赞 mloskot 6/12/2012
此信息是否仍然是最新的?AFAIK,GCC 附带的 C++11 实现执行这种“疯狂”锁定。当然,VS2010 仍然这样做。谁能澄清这种行为,如果“不需要”在 C++11 中仍然成立?
2赞 Ben Voigt 6/13/2012
@mloskot:我看不出对...“类 sentry 定义了一个类,该类负责执行异常安全前缀和后缀操作”,以及注释“哨兵构造函数和析构函数还可以执行其他与实现相关的操作。人们也可以从C++原则中推测,“你不为你不使用的东西付费”,C++委员会永远不会批准这样一个浪费的要求。但请随时提出有关 iostream 线程安全的问题。sentry