9

I'm just comparing the performance of passing a string to a function. The benchmark results are interesting.

Here's my code:

void add(std::string msg)
{
    msg += "world";
}

void addRvalue(std::string&& msg)
{
    msg += "world";
}

void addRef(std::string& msg)
{
    msg += "world";
}

void StringCreation() {
    add(std::string("hello "));
}

void StringCopy() {
    std::string msg("hello ");
    add(msg);
}

void StringMove() {
    std::string msg("hello ");
    add(std::move(msg));
}

void StringRvalue() {
    std::string msg("hello ");
    addRvalue(std::move(msg));
}

void StringReference() {
    std::string msg("hello ");
    addRef(msg);
}

StringCreation(), StringRvalue() and StringReference() are equivalent. I'm surprised StringMove() is the least performant - worse than pass by value which involves a copy.

Am I right in thinking that calling StringMove() involves one move constructor followed by a copy constructor when it calls add()? It doesn't just involve one move constructor? I thought move construction was cheap for a string.

Update

I increased the length of the string passed to add() and that did make a difference. Now StringMove() is only 1.1 times slower than StringCreation and StringReference. StringCopy is now the worst, which is what I expected.

Here are the new benchmark results.

So StringMove doesn't involve copying after all - only for small strings.

jignatius
  • 5,606
  • 2
  • 7
  • 21
  • 3
    Can you please post the the benchmark result (and compiler, invocation, full code, etc.) in the question? The linked benchmark does not equal your code. – walnut Aug 30 '19 at 11:03
  • 9
    You might want to repeat your experiment with longer strings. Small strings are kept on the stack and the move ctor will have to perform a copy regardless. Long strings are dynamically allocated and can be moved efficiently. ([See also my answer in this related question](https://stackoverflow.com/a/57724115/2788450)) – Jonas Greitemann Aug 30 '19 at 11:05
  • Note: your simple `void add(std::string msg)` function doesn't actually do anything! Check the value of one of your string before and after the call! – Adrian Mole Aug 30 '19 at 11:25
  • I dont completely understand the update, `StringCopy` and `StringMove` both call the same `add`. Only in `StringRvalue` the cast via `std::move` should have any effect` – 463035818_is_not_a_number Aug 30 '19 at 11:30
  • 1
    @formerlyknownas_463035818 That's not correct. In `StringCopy`, the parameter of `add` is initialized by the copy construcotr. In `StringMove` by move constructor. – Daniel Langr Aug 30 '19 at 11:34
  • @DanielLangr I am pretty sure you are right, but I dont really understand it. Have to do some more reading. Just yesterday I read [this](https://stackoverflow.com/a/21358433/4117728) awesome answer and maybe draw the wrong conclusions from it... – 463035818_is_not_a_number Aug 30 '19 at 11:46

2 Answers2

5

Let's analyze your code and suppose long strings (without applied SSO):

void add(std::string msg) {
   msg += "world";
}

void StringCreation() {
   add(std::string("hello "));
}

Here, a converting constructor (ConvC) from the string literal is called first to initialize the temporary std::string("hello "). This temporary (an rvalue) is then used to initialize the parameter msg by the move constructor (MC). However, the latter is very likely optimized away by copy elision. Finally, the operator += is called. Bottom line: 1x ConvC and 1x +=.

void StringCopy() {
   std::string msg("hello ");
   add(msg);
}

Here, the parameter msg is copy-initialized (by copy constructor - CC) by the lvalue argument msg. Bottom line: 1x ConvC, 1x CC, and 1x +=. In case of long strings, this is the slowest version, since copy involves dynamic memory allocations (the only case).

void StringMove() {
   std::string msg("hello ");
   add(std::move(msg));
}

Why is this slower than StringCreation? Simply because there is an additional MC involved that initializes the parameter msg. It cannot be elided, since the object msg still exist after the call of add. Just it is moved-from. Bottom line: 1x ConvC, 1x MC, 1x +=.

void addRef(std::string& msg) {
   msg += "world";
}

void StringReference() {
   std::string msg("hello ");
   addRef(msg);
}

Here, the operator += is applied to the referenced object, so there is no reason for any copy/move. Bottom line: 1x ConvC, 1x +=. Same time as for StringCreation.

void addRvalue(std::string&& msg) {
   msg += "world";
}

void StringRvalue() {
   std::string msg("hello ");
   addRvalue(std::move(msg));
}

With Clang, the time is same as for StringReference. With GCC, the time is same as for StringMove. In fact, I don't have an explanation for this behavior for now. (It seems to me that GCC is creating some additional temporary initialized by MC. However, I don't know why.)

Daniel Langr
  • 18,256
  • 1
  • 39
  • 74
  • 1
    OK, just to clear the air: I guess I'm not completely au fait with how the online benchmark site works! I'll keep my answer there, for now, but I'll give yours an upvote as a decent explanation for what is *probably* happening! – Adrian Mole Aug 30 '19 at 12:58
0

In this example, none of the functions that are being "benchmarked" are actually doing anything of consequence. That is, none of them actually returns an computed value that is then used elsewhere.

So, any (half-)decent compiler would probably just decide to ignore them completely!

In order for a valid benchmark to be made, then the string results from each/every call must be used for something, even simple output to a file/console.

Try this code to see what's (not) happening:

#include<iostream>
#include<string>

using namespace std;

void add(std::string msg)
{
    msg += " + 'add'";
}

void addRef(std::string& msg)
{
    msg += " + 'addRef'";
}

void addRvalue(std::string&& msg)
{
    msg += " + 'addRefRef'";
}

int main()
{
    std::string msg("Initial string!");
    cout << msg << endl;
    add(msg);
    cout << msg << endl; // msg will be the same as before!
    addRef(msg);
    cout << msg << endl; // msg will be extended!
    addRvalue(std::move(msg));
    cout << msg << endl; // msg will again be extended
    add(std::move(msg)); 
    cout << msg << endl; // msg will be completely emptied!
    return 0;
}
Adrian Mole
  • 30,672
  • 69
  • 32
  • 52
  • 1
    I wouldn't say so. Have you tried? I modified the benchmark to return references and use `benchmark::DoNotOptimize` the return values. The results did not change at all. I assume the behavior is caused by the fact that for long strings, dynamic allocations are required and a compiler generally does not optimize the allocations away. – Daniel Langr Aug 30 '19 at 11:42
  • Some relevant discussion: [Is the compiler allowed to optimize out heap memory allocations?](https://stackoverflow.com/q/31873616/580083). – Daniel Langr Aug 30 '19 at 11:48
  • @Daniel Langr Yes, I have tried with two compilers (MSVC and clang-cl). Same results. The OP didn't give their code that actually *calls* the tested functions, so I had to do this myself, in my 'main()'. Try it for yourself! – Adrian Mole Aug 30 '19 at 11:53
  • 2
    OP did provide the benchmark here: http://quick-bench.com/wDfp1GdS5ifeDa4hLroOCdb0kbI – 463035818_is_not_a_number Aug 30 '19 at 11:56
  • 1
    (tbh if you didnt see that I wonder what question you answered) – 463035818_is_not_a_number Aug 30 '19 at 11:58
  • @formerlyknownas_463035818 - But where can we see what actually happens to the strings in those benchmark tests? – Adrian Mole Aug 30 '19 at 12:06
  • what do you mean "what actually happens to the strings" ? They are passed to the different functions – 463035818_is_not_a_number Aug 30 '19 at 12:07
  • @ formerlyknownas_463035818 - I mean what happens to the strings' values! At least, in my code, I get to see what the string content is after I call the various add... versions. – Adrian Mole Aug 30 '19 at 12:16
  • @Adrian Just for clarification: After move, `msg` **may or may not be empty** in case of **short strings and applied SSO** (it must be empty for long strings due to complexity requirements). This depends on the implementation. However, all major implementations make it empty, just it's not guaranteed: see [Unnecessary emptying of moved-from std::string](https://stackoverflow.com/q/52696413/580083). – Daniel Langr Aug 30 '19 at 12:44