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)
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
reallocatestruct 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::data
s, 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.
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.
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 move
s that temporary into the vector v
. One std::string
buffer is allocated, and moved into the vector. An empty std::string
is discarded.
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::vector
s 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).