C++标准是否要求iostreams性能差,还是我只是处理性能差的实现?

21 浏览
0 Comments

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毫秒
  • vectorback_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毫秒
  • vectorback_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 / clink / 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毫秒
  • vectorback_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毫秒
  • vectorback_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毫秒
  • vectorback_inserter: 26.3毫秒, 26.5毫秒
  • vector使用普通迭代器: 0.87毫秒, 0.89毫秒
  • vector迭代器和边界检查: 0.99毫秒, 0.99毫秒
  • char[]: 1.25毫秒, 1.24毫秒

EDIT: 执行了两次,以查看结果的一致性。在我看来结果很一致。

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

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倍呢?在这个简单的微基准测试中,并没有以多态方式使用它,因此这并不能解释它的速度问题。

admin 更改状态以发布 2023年5月22日
0
0 Comments

我对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++下差距那么大。

0
0 Comments

不是回答你问题的具体细节,而是标题: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的重要特征,即其内存安全和类型安全的设计。这些好处是有代价的,而你编写的测试使这些成本支配了执行时间。

0