Does the C++ standard mandate poor performance for

2019-01-01 03:05发布

Every time I mention slow performance of C++ standard library iostreams, I get met with a wave of disbelief. Yet I have profiler results showing large amounts of time spent in iostream library code (full compiler optimizations), and switching from iostreams to OS-specific I/O APIs and custom buffer management does give an order of magnitude improvement.

What extra work is the C++ standard library doing, is it required by the standard, and is it useful in practice? Or do some compilers provide implementations of iostreams that are competitive with manual buffer management?

Benchmarks

To get matters moving, I've written a couple of short programs to exercise the iostreams internal buffering:

Note that the ostringstream and stringbuf versions run fewer iterations because they are so much slower.

On ideone, the ostringstream is about 3 times slower than std:copy + back_inserter + std::vector, and about 15 times slower than memcpy into a raw buffer. This feels consistent with before-and-after profiling when I switched my real application to custom buffering.

These are all in-memory buffers, so the slowness of iostreams can't be blamed on slow disk I/O, too much flushing, synchronization with stdio, or any of the other things people use to excuse observed slowness of the C++ standard library iostream.

It would be nice to see benchmarks on other systems and commentary on things common implementations do (such as gcc's libc++, Visual C++, Intel C++) and how much of the overhead is mandated by the standard.

Rationale for this test

A number of people have correctly pointed out that iostreams are more commonly used for formatted output. However, they are also the only modern API provided by the C++ standard for binary file access. But the real reason for doing performance tests on the internal buffering applies to the typical formatted I/O: if iostreams can't keep the disk controller supplied with raw data, how can they possibly keep up when they are responsible for formatting as well?

Benchmark Timing

All these are per iteration of the outer (k) loop.

On ideone (gcc-4.3.4, unknown OS and hardware):

  • ostringstream: 53 milliseconds
  • stringbuf: 27 ms
  • vector<char> and back_inserter: 17.6 ms
  • vector<char> with ordinary iterator: 10.6 ms
  • vector<char> iterator and bounds check: 11.4 ms
  • char[]: 3.7 ms

On my laptop (Visual C++ 2010 x86, cl /Ox /EHsc, Windows 7 Ultimate 64-bit, Intel Core i7, 8 GB RAM):

  • ostringstream: 73.4 milliseconds, 71.6 ms
  • stringbuf: 21.7 ms, 21.3 ms
  • vector<char> and back_inserter: 34.6 ms, 34.4 ms
  • vector<char> with ordinary iterator: 1.10 ms, 1.04 ms
  • vector<char> iterator and bounds check: 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, with Profile-Guided Optimization cl /Ox /EHsc /GL /c, link /ltcg:pgi, run, link /ltcg:pgo, measure:

  • ostringstream: 61.2 ms, 60.5 ms
  • vector<char> with ordinary iterator: 1.04 ms, 1.03 ms

Same laptop, same OS, using cygwin gcc 4.3.4 g++ -O3:

  • ostringstream: 62.7 ms, 60.5 ms
  • stringbuf: 44.4 ms, 44.5 ms
  • vector<char> and back_inserter: 13.5 ms, 13.6 ms
  • vector<char> with ordinary iterator: 4.1 ms, 3.9 ms
  • vector<char> iterator and bounds check: 4.0 ms, 4.0 ms
  • char[]: 3.57 ms, 3.75 ms

Same laptop, Visual C++ 2008 SP1, cl /Ox /EHsc:

  • ostringstream: 88.7 ms, 87.6 ms
  • stringbuf: 23.3 ms, 23.4 ms
  • vector<char> and back_inserter: 26.1 ms, 24.5 ms
  • vector<char> with ordinary iterator: 3.13 ms, 2.48 ms
  • vector<char> iterator and bounds check: 2.97 ms, 2.53 ms
  • char[]: 1.52 ms, 1.25 ms

Same laptop, Visual C++ 2010 64-bit compiler:

  • ostringstream: 48.6 ms, 45.0 ms
  • stringbuf: 16.2 ms, 16.0 ms
  • vector<char> and back_inserter: 26.3 ms, 26.5 ms
  • vector<char> with ordinary iterator: 0.87 ms, 0.89 ms
  • vector<char> iterator and bounds check: 0.99 ms, 0.99 ms
  • char[]: 1.25 ms, 1.24 ms

EDIT: Ran all twice to see how consistent the results were. Pretty consistent IMO.

NOTE: On my laptop, since I can spare more CPU time than ideone allows, I set the number of iterations to 1000 for all methods. This means that ostringstream and vector reallocation, which takes place only on the first pass, should have little impact on the final results.

EDIT: Oops, found a bug in the vector-with-ordinary-iterator, the iterator wasn't being advanced and therefore there were too many cache hits. I was wondering how vector<char> was outperforming char[]. It didn't make much difference though, vector<char> is still faster than char[] under VC++ 2010.

Conclusions

Buffering of output streams requires three steps each time data is appended:

  • Check that the incoming block fits the available buffer space.
  • Copy the incoming block.
  • Update the end-of-data pointer.

The latest code snippet I posted, "vector<char> simple iterator plus bounds check" not only does this, it also allocates additional space and moves the existing data when the incoming block doesn't fit. As Clifford pointed out, buffering in a file I/O class wouldn't have to do that, it would just flush the current buffer and reuse it. So this should be an upper bound on the cost of buffering output. And it's exactly what is needed to make a working in-memory buffer.

So why is stringbuf 2.5x slower on ideone, and at least 10 times slower when I test it? It isn't being used polymorphically in this simple micro-benchmark, so that doesn't explain it.

4条回答
公子世无双
2楼-- · 2019-01-01 03:14

Not answering the specifics of your question so much as the title: the 2006 Technical Report on C++ Performance has an interesting section on IOStreams (p.68). Most relevant to your question is in Section 6.1.2 ("Execution Speed"):

Since certain aspects of IOStreams processing are distributed over multiple facets, it appears that the Standard mandates an inefficient implementation. But this is not the case — by using some form of preprocessing, much of the work can be avoided. With a slightly smarter linker than is typically used, it is possible to remove some of these inefficiencies. This is discussed in §6.2.3 and §6.2.5.

Since the report was written in 2006 one would hope that many of the recommendations would have been incorporated into current compilers, but perhaps this is not the case.

As you mention, facets may not feature in write() (but I wouldn't assume that blindly). So what does feature? Running GProf on your ostringstream code compiled with GCC gives the following breakdown:

  • 44.23% in std::basic_streambuf<char>::xsputn(char const*, int)
  • 34.62% in std::ostream::write(char const*, int)
  • 12.50% in main
  • 6.73% in std::ostream::sentry::sentry(std::ostream&)
  • 0.96% in std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int)
  • 0.96% in std::basic_ostringstream<char>::basic_ostringstream(std::_Ios_Openmode)
  • 0.00% in std::fpos<int>::fpos(long long)

So the bulk of the time is spent in xsputn, which eventually calls std::copy() after lots of checking and updating of cursor positions and buffers (have a look in c++\bits\streambuf.tcc for the details).

My take on this is that you've focused on the worst-case situation. All the checking that is performed would be a small fraction of the total work done if you were dealing with reasonably large chunks of data. But your code is shifting data in four bytes at a time, and incurring all the extra costs each time. Clearly one would avoid doing so in a real-life situation - consider how negligible the penalty would have been if write was called on an array of 1m ints instead of on 1m times on one int. And in a real-life situation one would really appreciate the important features of IOStreams, namely its memory-safe and type-safe design. Such benefits come at a price, and you've written a test which makes these costs dominate the execution time.

查看更多
永恒的永恒
3楼-- · 2019-01-01 03:21

To get better performance you have to understand how the containers you are using work. In your char[] array example, the array of the required size is allocated in advance. In your vector and ostringstream example you are forcing the objects to repeatedly allocate and reallocate and possibly copy data many times as the object grows.

With std::vector this is easly resolved by initialising the size of the vector to the final size as you did the char array; instead you rather unfairly cripple the performance by resizing to zero! That is hardly a fair comparison.

With respect to ostringstream, preallocating the space is not possible, I would suggest that it is an inappropruate use. The class has far greater utility than a simple char array, but if you don't need that utility, then don't use it, because you will pay the overhead in any case. Instead it should be used for what it is good for - formatting data into a string. C++ provides a wide range of containers and an ostringstram is amongst the least appropriate for this purpose.

In the case of the vector and ostringstream you get protection from buffer overrun, you don't get that with a char array, and that protection does not come for free.

查看更多
牵手、夕阳
4楼-- · 2019-01-01 03:23

I'm rather disappointed in the Visual Studio users out there, who rather had a gimme on this one:

  • In the Visual Studio implementation of ostream, the sentry object (which is required by the standard) enters a critical section protecting the streambuf (which is not required). This doesn't seem to be optional, so you pay the cost of thread synchronization even for a local stream used by a single thread, which has no need for synchronization.

This hurts code that uses ostringstream to format messages pretty severely. Using the stringbuf directly avoids the use of sentry, but the formatted insertion operators can't work directly on streambufs. For Visual C++ 2010, the critical section is slowing down ostringstream::write by a factor of three vs the underlying stringbuf::sputn call.

Looking at beldaz's profiler data on newlib, it seems clear that gcc's sentry doesn't do anything crazy like this. ostringstream::write under gcc only takes about 50% longer than stringbuf::sputn, but stringbuf itself is much slower than under VC++. And both still compare very unfavorably to using a vector<char> for I/O buffering, although not by the same margin as under VC++.

查看更多
时光乱了年华
5楼-- · 2019-01-01 03:25

The problem you see is all in the overhead around each call to write(). Each level of abstraction that you add (char[] -> vector -> string -> ostringstream) adds a few more function call/returns and other housekeeping guff that - if you call it a million times - adds up.

I modified two of the examples on ideone to write ten ints at a time. The ostringstream time went from 53 to 6 ms (almost 10 x improvement) while the char loop improved (3.7 to 1.5) - useful, but only by a factor of two.

If you're that concerned about performance then you need to choose the right tool for the job. ostringstream is useful and flexible, but there's a penalty for using it the way you're trying to. char[] is harder work, but the performance gains can be great (remember the gcc will probably inline the memcpys for you as well).

In short, ostringstream isn't broken, but the closer you get to the metal the faster your code will run. Assembler still has advantages for some folk.

查看更多
登录 后发表回答