C/C++ and Fortran

C/C++ and Fortran

Connect, learn, share, and engage with IBM Power.

 View Only

RVO V.S. std::move

By Archive User posted Wed July 15, 2015 06:12 AM

  

Originally posted by: Zhao Wu


Good new! The more focused community about XL compilers on POWER is now available at http://ibm.biz/xl-power-compilers.

If you are interested in the XL compilers on POWER, you may want to join the new community and subscribe to updates there. See you there!

This site remains the Cafe for C/C++ compilers for IBM Z.

Return Value Optimization

Return value optimization, simply RVO, is a compiler optimization technique that allows the compiler to construct the return value of a function at the call site. The technique is also named "elision". C++98/03 standard doesn’t require the compiler to provide RVO optimization, but most popular C++ compilers contain this optimization technique, such as IBM XL C++ compiler, GCC and Clang . This optimization technique is included in the C++11 standard due to its prevalence.  As defined in Section 12.8 in the C++11 standard, the name of the technique is "copy elision".

 

Let’s start with one example to see how the RVO works. Firstly, we have a class named BigObject. Its size could be so large that copying it would have high cost. Here I just define constructor, copy constructor and destructor for convenience.

 

class BigObject {
public:
    BigObject() {
        cout << "constructor. " << endl;
}
    ~BigObject() {
        cout << "destructor."<< endl;
 }
    BigObject(const BigObject&) {
        cout << "copy constructor." << endl;
 }
};

 

We then define one function named foo to trigger the RVO optimization and use it in the main function to see what will happen.

 

BigObject foo() {
    BigObject localObj;
    return localObj;
}
 
int main() {
    BigObject obj = foo();
}

 

Some people will call this case using named return value optimization(NRVO), because foo returns one temporary object named localObj. They think that what returns BigObject() is RVO. You don't  need to worry about it, as NRVO is one variant of RVO.

 

Let’s compile this program and run: xlC a.cpp ;./a,out. (The version of the XL C/C++ compiler used is V13 and the environment is Linux little endian.) The output is like this:

constructor.

destructor.

 

It is amazing, right? There is no copy constructor here. When the price of coping is high, RVO enables us to run the program much faster. 

 

However, when we modify our code a little, things will change.

 

BigObject foo(int n) {
    BigObject localObj, anotherLocalObj;
    if (n > 2) {
       return localObj;
    } else {
       return anotherLocalObj;
    }
}
int main() {
    BigObject obj = foo(1);
}

 

The output will be like this:

constructor.

constructor.

copy constructor.

destructor.

destructor.

destructor.


We can find that copy constructor is called. Why?  It's time to show the mechanism of RVO.

 

image

 

 

 

 

 

 

 

 

 

 

 

 

This diagram is a normal function stack frame. If we call the function without RVO, the function simply allocates the space for the return in its own frame. The process is demonstrated in the following diagram:

image

 

 

 

 

 

 

 

 

 

 

 

What will happen if we call the function with RVO?


image

 

 

 

 

 

 

 

 

 

 

 

We can find that RVO uses parent stack frame (or an arbitrary block of memory) to avoid copying. So, if we add if-else branch, the compiler doesn’t know which return value to put.

 

std::move

We first need to understand what std::move is. Many C++ programmers misunderstand std::move so that they use std::move in wrong situations. Let us see the implementation of std::move.

 

template<typename T> 
decltype(auto) move(T&& param)
{
    using ReturnType = remove_reference_t<T>&&;
    return static_cast<ReturnType>(param);
}

 

In fact, the key step std::move performs is to cast its argument to an rvalue. It also instructs the compiler that it is eligible to move the object, without moving anything. So you can also call "std::move" as "std::rvalue_cast", which seems to be more appropriate than "std::move".


The price of moving is lower than coping but higher than RVO.  Moving does the following two things:

    1. Steal all the data
    2. Trick the object we steal into forgetting everything

 

If we want to instruct the compiler to move, we can define move constructor and move assignment operator. I just define move constructor for convenience here.

 

BigObject(BigObject&&) {

    cout << "move constructor"<< endl;
}

 

Let us modify the code of foo.

 

BigObject foo(int n) {

    BigObject localObj, anotherLocalObj;
    if (n > 2) {
       return std::move(localObj);
    } else {
       return std::move(anotherLocalObj);
    }
}

 

Let’s compile it and run: xlC –std=c++11 a.cpp;./a.out. The result is:
constructor.
constructor.
move constructor
destructor.
destructor.
destructor.


We can find that move constructor is called as expected. However, we must also note that the compiler also calls destructor while RVO doesn’t.


Some people would think that returning std::move(localObj) is always beneficial. If the compiler can do RVO, then RVO. Otherwise, the compiler calls move constructor. But I must say: Don't DO THAT!


Let us see what will happen if we do this:

 

BigObject foo(int n) {

    BigObject localObj;
    return std::move(localObj);
}
int main() {
    auto f = foo(1);
}

 

Maybe you think that the compiler will do RVO, but you actually get this result:
constructor.
move constructor

destructor.
destructor.


The compiler doesn’t do RVO but call move constructor! To explain it, we need to look at the details of copy elision in C++ standard first:

in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

 

That is to say, we must keep our type of return statement the same as function return type.


Let's then, refresh our memory about std::move a little bit. std::move is just an rvalue-cast. In other words, std::move will cast localObj to localObj&& and the type is BigObject&&, but the function type is just BigObject. BigObject is not BigObject&&, so this is the reason why RVO didn’t take place just now.


We now change the foo function return type and obj type is BigObject&&:

 

BigObject&& foo(int n) {

    BigObject localObj;
    return std::move(localObj);
}
int main() {
    auto f = foo(1);
}

 

Then we compile and run it, and we will get the output like this:

constructor. 
destructor.
move constructor
destructor.


Yes! The compiler does RVO! (Note: We should not use this way in the real development, because it is a reference to a local object. Here just show how to make RVO happened.).  

 

To summarize, RVO is a compiler optimization technique, while std::move is just an rvalue cast, which also instructs the compiler that it's eligible to move the object. The price of moving is lower than copying but higher than RVO, so never apply std::move to local objects if they would otherwise be eligible for the RVO.

 

 

English Editor: Jun Qian Zhou (Ashley). Many thanks to Ashley! 

 

 

 

 

6 comments
18 views

Permalink

Comments

Thu July 23, 2015 11:52 PM

Originally posted by: Zhao Wu


Hi, BartVandewoestyne 1. Thanks for your test file, I think it is very convenient for people who want to test code. :-) 2. Right. In C++98/03, there are no move semantics. In C++11, if the function return object is rvalue and the object's class has special member move function, we call move constructor rather than copy constructor. :-)

Thu July 23, 2015 11:38 PM

Originally posted by: Zhao Wu


Hi, KXXQ_Mathias_Gaunard As I said in the post Note: We should not use this way in the real development, because it is a reference to a local object...

Thu July 23, 2015 09:16 PM

Originally posted by: KXXQ_Mathias_Gaunard


The last version returns a reference to a local variable, which is undefined behaviour. The right way to write the function is just BigObject foo(int n) { BigObject localObj; return localObj; }

Thu July 23, 2015 09:16 PM

Originally posted by: BartVandewoestyne


Interesting read. To better understand RVO, i created a testfile from the code that you mention in this post: https://github.com/BartVandewoestyne/Cpp/blob/master/examples/C%2B%2B11/return_value_optimization.cpp It is compileable under both C++98/03 and C++11. From playing with it, I also noticed that for your second example BigObject foo(int n) { BigObject localObj, anotherLocalObj; if (n > 2) { return localObj; } else { return anotherLocalObj; } } int main() { BigObject obj = foo(1); } in the case of C++98, the copy constructor will be called, but in the case of C++11, the move constructor is called. I just thought that was interesting to mention. Furthermore, note also that in his Effective Modern C++ book, Scott Meyers also discusses this topic in Item 25 on pages 174-176.

Mon July 20, 2015 10:23 AM

Originally posted by: Zhao Wu


Hi, R.A.Berliner. Thanks for your comment. 1. Yes, you are right. foo(1) is just in the step of return reference. We should store the result. I will update it. 2. In fact, I want to make more people to understand easier. So, I use colloquial language to express. Thanks again. Because of your comment, this post becomes better. :-)

Mon July 20, 2015 08:43 AM

Originally posted by: R.A.Berliner


BigObject&& foo(int n) { BigObject localObj; return std::move(localObj); } int main() { foo(1); } This is not RVO, it is a dangerous proposition as "foo" returns a reference to a local object. To actually test RVO, it would have been necessary to store the result: int main() { auto f = foo(1); } but this would have finally caused undefined behavior, as a dangling reference is used to initialize "f". Also, the statement "Trick the object we steal into forgetting everything" is very colloquial. The C++ standard makes different statements about the effect a move assignment or construction has to the source object. E.g. a std::string is not guaranteed to be empty after having been used as the source object of a move.