Can modern C++ get you performance for free?

alarge picture alarge · Dec 22, 2014 · Viewed 13.8k times · Source

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)

Answer

Yakk - Adam Nevraumont picture Yakk - Adam Nevraumont · Dec 22, 2014

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).