208
votes

It is sometimes claimed that C++11/14 can get you a performance boost even when merely compiling C++98 code. The justification is usually along the lines of move semantics, as in some cases the rvalue constructors are automatically generated or now part of the STL. Now I'm wondering whether these cases were previously actually already handled by RVO or similar compiler optimizations.

My question then is if you could give me an actual example of a piece of C++98 code that, without modification, runs faster using a compiler supporting the new language features. I do understand that a standard conforming compiler is not required to do the copy elision and just by that reason move semantics might bring about speed, but I'd like to see a less pathological case, if you will.

EDIT: Just to be clear, I am not asking whether new compilers are faster than old compilers, but rather if there is code whereby adding -std=c++14 to my compiler flags it would run faster (avoid copies, but if you can come up with anything else besides move semantics, I'd be interested, too)

2
Remember that copy elision and return value optimization is performed when constructing a new object using a copy constructor. However, in a copy assignment operator, there is no copy elision (how can it be, since the compiler doesn't know what to do with an already constructed object that is not a temporary). Therefore, in that case, C++11/14 wins big, by giving you the possibility of using a move assignment operator. About your question though, I don't think C++98 code should be faster if compiled by a C++11/14 compiler, maybe it is faster because the compiler is newer.vsoftco
Also code that uses the standard library is potentially faster, even if you make it fully compatible with C++98, because in C++11/14 the underlying library uses internally move semantics when possible. So code that looks identical in C++98 and C++11/14 will be (possibly) faster in the latter case, whenever you use the standard library objects such as vectors, lists etc and move semantics makes a difference.vsoftco
@vsoftco, That is the kind of situation I was alluding to, but could not come up with an example: From what I remember if I have to define the copy constructor, the move constructor won't be automatically generated, which leaves us with very simple classes where RVO, I think, always works. An exception might be something in conjuction with the STL containers, where the rvalue constructors are generated by the library implementer (meaning I wouldn't have to change anything in the code for it to use moves).alarge
classes do not need to be simple in order to not have a copy constructor. C++ thrive on value semantics, and copy constructor, assignment operator, destructor etc should be the exception.sp2danny
@Eric Thank you for the link, it was interesting. However, having quickly looked it through, the speed advantages in it seem to come mostly from adding std::move and move constructors (which would require modifications to existing code). The only thing really related to my question was the sentence "You get immediate speed advantages simply by recompiling", which is not backed up by any examples (it does mention STL on the same slide, as I did in my question, but nothing specific). I was asking for some examples. If I am reading the slides wrong, do let me know.alarge

2 Answers

225
votes

I am aware of 5 general categories where recompiling a C++03 compiler as C++11 can cause unbounded performance increases that are practically unrelated to quality of implementation. These are all variations of move semantics.

std::vector reallocate

struct bar{
  std::vector<int> data;
};
std::vector<bar> foo(1);
foo.back().data.push_back(3);
foo.reserve(10); // two allocations and a delete occur in C++03

every time the foo's buffer is reallocated in C++03 it copied every vector in bar.

In C++11 it instead moves the bar::datas, which is basically free.

In this case, this relies on optimizations inside the std container vector. In every case below, the use of std containers is just because they are C++ objects that have efficient move semantics in C++11 "automatically" when you upgrade your compiler. Objects that don't block it that contain a std container also inherit the automatic improved move constructors.

NRVO failure

When NRVO (named return value optimization) fails, in C++03 it falls back on copy, on C++11 it falls back on move. Failures of NRVO are easy:

std::vector<int> foo(int count){
  std::vector<int> v; // oops
  if (count<=0) return std::vector<int>();
  v.reserve(count);
  for(int i=0;i<count;++i)
    v.push_back(i);
  return v;
}

or even:

std::vector<int> foo(bool which) {
  std::vector<int> a, b;
  // do work, filling a and b, using the other for calculations
  if (which)
    return a;
  else
    return b;
}

We have three values -- the return value, and two different values within the function. Elision allows the values within the function to be 'merged' with the return value, but not with each other. They both cannot be merged with the return value without merging with each other.

The basic issue is that NRVO elision is fragile, and code with changes not near the return site can suddenly have massive performance reductions at that spot with no diagnostic emitted. In most NRVO failure cases C++11 ends up with a move, while C++03 ends up with a copy.

Returning a function argument

Elision is also impossible here:

std::set<int> func(std::set<int> in){
  return in;
}

in C++11 this is cheap: in C++03 there is no way to avoid the copy. Arguments to functions cannot be elided with the return value, because the lifetime and location of the parameter and return value is managed by the calling code.

However, C++11 can move from one to the other. (In a less toy example, something might be done to the set).

push_back or insert

Finally elision into containers does not happen: but C++11 overloads rvalue move insert operators, which saves copies.

struct whatever {
  std::string data;
  int count;
  whatever( std::string d, int c ):data(d), count(c) {}
};
std::vector<whatever> v;
v.push_back( whatever("some long string goes here", 3) );

in C++03 a temporary whatever is created, then it is copied into the vector v. 2 std::string buffers are allocated, each with identical data, and one is discarded.

In C++11 a temporary whatever is created. The whatever&& push_back overload then moves that temporary into the vector v. One std::string buffer is allocated, and moved into the vector. An empty std::string is discarded.

Assignment

Stolen from @Jarod42's answer below.

Elision cannot occur with assignment, but move-from can.

std::set<int> some_function();

std::set<int> some_value;

// code

some_value = some_function();

here some_function returns a candidate to elide from, but because it is not used to construct an object directly, it cannot be elided. In C++03, the above results in the contents of the temporary being copied into some_value. In C++11, it is moved into some_value, which basically is free.


For the full effect of the above, you need a compiler that synthesizes move constructors and assignment for you.

MSVC 2013 implements move constructors in std containers, but does not synthesize move constructors on your types.

So types containing std::vectors and similar do not get such improvements in MSVC2013, but will start getting them in MSVC2015.

clang and gcc have long since implemented implicit move constructors. Intel's 2013 compiler will support implicit generation of move constructors if you pass -Qoption,cpp,--gen_move_operations (they don't do it by default in an effort to be cross-compatible with MSVC2013).

46
votes

if you have something like:

std::vector<int> foo(); // function declaration.
std::vector<int> v;

// some code

v = foo();

You got a copy in C++03, whereas you got a move assignment in C++11. so you have free optimisation in that case.