2

I just saw a college doing this optimization today:

enter image description here

i.e., change this:

std::string somestring =  "somestring";
std::map<std::string, int> myglobalmap;

std::string myfunction( const std::string& mystring, int myint )
{
    myglobalmap.insert( std::pair<std::string, int>( mystring, myint ) );
}
myfunction(somestring, 10)

To:

std::string somestring =  "somestring";
std::map<std::string, int> myglobalmap;

std::string myfunction( std::string mystring, int myint )
{
    myglobalmap[ std::move(mystring) ] = myint;
}
myfunction(somestring, 10)

He claimed that passing the value mystring by copy instead of passing it as a constant reference would be faster because the move operation would be only performed with objects on the stack. But I could not see why this would help. I searched about the move operator and I found out it does not move anything, it just makes my expression to return a reference.

Then, if this is true, passing the value by copy would not be slower than passing it by reference and calling std::move or does calling std::move help with objects on the stack in this case? If I understand correctly, std::move should only help with objects on the heap. Then, calling it with something on the stack should not help or it does?

Related questions:

  1. What is std::move(), and when should it be used?
  2. Is it possible to std::move local stack variables?
Evandro Coan
  • 8,560
  • 11
  • 83
  • 144
  • 1
    With a call like that, the string copy is just moved from inside the function to outside... `myfunction(std::move(somestring), 10);` would avoid the copy... but would mutate `somestring`. – Jarod42 Mar 31 '21 at 07:53
  • Internal of string use dynamic memory. – Jarod42 Mar 31 '21 at 07:55

3 Answers3

4

and I found out it does not move anything, it just makes my expression to return a reference.

This is correct. Crucially, the result of the function is an xvalue.

Then, if this is true, passing the value by copy would not be slower than passing it by reference and calling std::move or does calling std::move

In case of std::string (assuming the contained string isn't trivially small), a copy and a move is slower than indirection through a reference and move.


In your first example, you indirect through a reference and always make a copy.

In the second example, you copy only if the function argument is an lvalue. Second is faster than the fist when the argument is an rvalue (as long as the stored string is long enough for the difference to be significant).


Both versions have undefined behaviour because the functions are declared to return non-void, but do not return a value.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • Later could be faster if such key does not exists. If it does most likely it would be slower. – Slava Mar 31 '21 at 01:15
  • @Slava How did you come to the second conclusion? Copying a string has linear time complexity. – eerorika Mar 31 '21 at 01:31
  • 1) " ... a copy and a move is slower than indirection through a reference and move" -> OK Agreed. 2) "...Second is faster than the fist when the argument is an rvalue" -> OK Agreed. Still I do not find the answer to the Q when argument is lvalue and I do the indirection through a reference and then copy it. – TonySalimi Mar 31 '21 at 12:53
  • @Gupta The question was about which function is faster. It depends on how the function is called. I interpret the call as an example instead of the only case that OP cares about. Since both functions copy in that case, they are both slow in case where argument is an lvalue compared to the second function being called with an rvalue. – eerorika Mar 31 '21 at 13:11
  • So, you mean in the case of the question, (i.e. send strings by lvalue), the performance of the both cases will be the same? (assume that the key does not exist in map) – TonySalimi Mar 31 '21 at 13:23
  • @Gupta The question is about the function as far as I can tell. Functions can be called in different ways. Yes, the performance will be similar given an lvaue argument. – eerorika Mar 31 '21 at 13:27
  • 1
    Agreed with your comment. But I did a test with the two mentioned functions and passing an lvalue to them. In my tests, the second one is faster than the first one (70% for debug for 90% release). I cannot still explain this. (Windows 10, VS2015) – TonySalimi Mar 31 '21 at 13:34
  • Check my code here: https://godbolt.org/z/dv5f8zTn7 – TonySalimi Mar 31 '21 at 13:36
  • 2
    @Gupta Benchmarking is very difficult and a single percentage number doesn't tell anything meaningful. The difference could be affected by entirely incidental change in the environment. That said, the first version does one move more than the second when the key does exist in the map. Your bechmark mostly measures the case of the key already existing. It's also marginally unfair since first function is tested once then the map is empty, although that should have a diluted effect. – eerorika Mar 31 '21 at 13:52
  • If key does exist second function could make one copy (binding argument), the first one with const reference will not make any copy so first one could be faster. – Slava Apr 01 '21 at 02:09
3

First, I'd like to say that this changes the behavior of myfunction. The first version does not insert the integer if the key already exists in the map. The second version replaces the integer with the new value.

That said, In the case where the passed string is not already in the map, and hence it would have made a copy, this could be slightly more efficient. If myfunction is passed a temporary, the compiler could move construct (or even optimize the move out), and that would then be moved into the map. While std::move doesn't move anything, using it causes the map::operator[] to use the rvalue reference overload, which can in turn invoke the move constructor on std::string.

However, in cases where the key already exists, it might cause the creation of an extra copy that's not needed.

Dave S
  • 20,507
  • 3
  • 48
  • 68
  • Can you directly answer to the case of Q, wherein 1) passed string is not a temporary and 2) the key always does not exist in the map? – TonySalimi Mar 31 '21 at 13:00
  • " In the case where the passed string is not already in the map, and hence it would have made a copy, this could be slightly more efficient. " Please elaborate. To the casual reviewer, the copy has already been paid by the pass-by-value copy of the string. – Jeffrey Mar 31 '21 at 14:15
0

I had missed an important point in the question, there was a lock right before myglobalmap.insert( and the new myglobalmap[.

i.e., the change was this:

std::string somestring =  "somestring";
std::map<std::string, int> myglobalmap;
std::mutex g_pages_mutex;

std::string myfunction( const std::string& mystring, int myint )
{
    std::lock_guard<std::mutex> guard(g_pages_mutex);
    myglobalmap.insert( std::pair<std::string, int>( mystring, myint ) );
    return "";
}
myfunction(somestring, 10)

To:

std::string somestring =  "somestring";
std::map<std::string, int> myglobalmap;
std::mutex g_pages_mutex;

std::string myfunction( std::string mystring, int myint )
{
    std::lock_guard<std::mutex> guard(g_pages_mutex);
    myglobalmap[ std::move(mystring) ] = myint;
    return "";
}
myfunction(somestring, 10)

So in extension to the answer provided by @eerorika, the optimization performed was to call the copy constructor before getting the lock. Meaning the second version should be faster as the copy is performed without holding a lock.

Evandro Coan
  • 8,560
  • 11
  • 83
  • 144