2
\$\begingroup\$

I am trying to find a way to convert printable data types into a string in the fastest way possible.

I originally read that a static variable of stringstream that does all of the formatting is one of the fastest ways, since constantly constructing temporary variables of stringstream is slow.

The problem with using stringstream.str() is because it creates a copy of the entire string, which may lead to performance issues, so I made this function to get rid of the need of stringstream.str().

#include <iostream>
#include <sstream>
#include <streambuf>
#include <vector>
#include <string>

#define ALL(c) (c).begin(), (c).end()

stringstream str_buffer;

string extract()
{
    streambuf& buffer = *str_buffer.rdbuf();
    vector<char> sequence(str_buffer.tellp());
    buffer.sgetn(sequence.data(), str_buffer.tellp());

    str_buffer.seekp(0);
    buffer.pubseekpos(0);
    return string(ALL(sequence));
}

To use this function, just do str_buffer << var; string as_str = extract(); to get var as a string.

However, I have not yet compared timing benchmark results of this function and simply using stringstream.str(), so I hope that my function is faster if it is in theory. Any suggestions to further improve?

\$\endgroup\$
  • \$\begingroup\$ So and return string(ALL(sequence)); doesn't take a copy? \$\endgroup\$ – πάντα ῥεῖ Dec 28 '19 at 1:04
  • 1
    \$\begingroup\$ you might want to have a look at <charconv> or <format> from C++20 \$\endgroup\$ – miscco Dec 28 '19 at 13:01
  • 1
    \$\begingroup\$ Go and benchmark your code right now. We can have all the theoretical discussions you want, but it's very hard to predict what the fastest way is, especially with compilers as good at optimizing as they are today. \$\endgroup\$ – G. Sliepen Dec 28 '19 at 20:39
2
\$\begingroup\$

Your function modifies the get/put position of the buffer. I think this is a bug, and it makes benchmarks misleading. Try calling extract twice in a row -- you'll get different results. You set the put position after calling sgetn but you should have set the get position. This should be intuitive. After a read, you "undo" your reading. Fix the bug with something like this:

string extract() {
    streambuf& buffer = *str_buffer.rdbuf();
    vector<char> sequence(str_buffer.tellp());
    auto g = str_buffer.tellg();
    buffer.sgetn(sequence.data(), str_buffer.tellp());
    str_buffer.seekg(g); // seekg to the original position
    // as far as I can work out, buffer.pubseekpos is not necessary here?
    return string(ALL(sequence));
}

Your function still does not compute the same thing as str. str returns the entire buffer. Your function returns from the input position to the output position. These are different -- try calling str_buffer.seekg(3) before comparing the output of the two functions if you want to see for yourself. You could seekg(0) before you call sgetn:

    auto g = str_buffer.tellg();
    str_buffer.seekg(0);
    buffer.sgetn(sequence.data(), str_buffer.tellp());
    str_buffer.seekg(g);

I think this implementation is now correct enough to benchmark. I tried the updated version vs str on http://quick-bench.com. I used clang-9 and libstdc++(GNU). I tried a stringstream with 1000 characters and also 10,000 characters.

This implementation is about 130 times slower than str with no optimizations and with -O1/Clang, and it is and 3.5 times slower with -O2/Clang.


How can you speed up your function? Well the easiest way is to use str! But let's make some small improvements. You copy the buffer into a vector<char> and then copy the vector into a string. What if we only have one string?

string extractOneString() {
    std::string sequence;
    sequence.resize(str_buffer.tellp());
    auto g = str_buffer.tellg();
    str_buffer.seekg(0);
    str_buffer.rdbuf()->sgetn(sequence.data(), str_buffer.tellp());
    str_buffer.seekg(g);
    return sequence;
}

Now the unoptimized version is 3.5 times slower -- just like the optimized version before. As it turns out, the Clang's -O2 was already making this change for you.

Still, 3 times slower is not good.

This is maybe not obvious, but if you look at the assembly (perhaps using http://godbolt.org), you'll notice that resize calls memset to set the new buffer to zero. That's not necessary since you're about to overwrite the buffer anyway. There's no easy way to get around this (unless you count using str). I think this contributes to the 3x slowdown (not sure and I don't think it's worth confirming).


A few notes about your code:

  1. Don't use using namespace std. People have written about this in lots of places so I won't here.

  2. The ALL macro is ugly and unnecessary. If you cannot stand to write iterators, look into the new ranges library.


It's worth learning sooner rather than later not to spend time optimizing the wrong thing. You wrote this code and then (I) measured it. Next time, measure your code first and then decide whether it's worth changing. This has the added advantage that you're already set up to measure the code post change.

I think it's also worth focusing on the basic semantics of your program before worrying too much about measured performance. An experienced C++ dev would see right away that your extract function has a unnecessary copies and would most likely fix that before profiling. Learning to avoid copies and other idioms will help you avoid having to measure every little thing.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.