Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does the caller of a function know whether Return Value Optimization was used?

Tags:

c++

My understanding of return value optimization is that the compiler secretly passes the address of the object in which the return value will be stored, and makes the changes to that object instead of a local variable.

For example, the code

std::string s = f();

std::string f()
{
    std::string x = "hi";
    return x;
}

Becomes SIMILAR to

std::string s;
f(s);

void f(std::string& x)
{
    x = "hi";
}

When RVO is used. This means that the interface of the function has changed, as there is an extra hidden parameter.

Now consider the following case I stole from Wikipedia

std::string f(bool cond)
{
    std::string first("first");
    std::string second("second");
    // the function may return one of two named objects
    // depending on its argument. RVO might not be applied
    return cond ? first : second;
}

Let's assume that a compiler will apply RVO to the first case, but not to this second case. But doesn't the interface of the function change depending on whether RVO was applied? If the body of function f is not visible to the compiler, how does the compiler know whether RVO was applied and whether the caller needs to pass the hidden address parameter?

like image 269
Neil Kirk Avatar asked Sep 05 '13 13:09

Neil Kirk


People also ask

How does return value optimization work?

In the context of the C++ programming language, return value optimization (RVO) is a compiler optimization that involves eliminating the temporary object created to hold a function's return value. RVO is allowed to change the observable behaviour of the resulting program by the C++ standard.

Does C have return value optimization?

> Note also that C doesn't have return-value-optimization, hence all your struct-returning functions will cause a call to memcpy (won't happen when compiled in C++ mode of course).

How do I turn off return value optimization?

Named RVO GCC performs NRVO by default, but it can be disabled using the -fno-elide-constructors + compiler option. In contrast, MSVC disables NRVO by default, but it can be enabled using /O2 optimization+.


Video Answer


2 Answers

There's no change in the interface. In all cases, the results of the function must appear in the scope of the caller; typically, the compiler uses a hidden pointer. The only difference is that when RVO is used, as in your first case, the compiler will "merge" x and this return value, constructing x at the address given by the pointer; when it is not used, the compiler will generate a call to the copy constructor in the return statement, to copy whatever into this return value.

I might add that your second example is not very close to what happens. At the call site, you get almost always something like:

<raw memory for string> s;
f( &s );

And the called function will either construct a local variable or temporary directly at the address it was passed, or copy construct some othe value at this address. So that in your last example, the return statement would be more or less the equivalent of:

if ( cont ) {
    std::string::string( s, first );
} else {
    std::string::string( s, second );
}

(Showing the implicit this pointer passed to the copy constructor.) In the first case, if RVO applies, the special code would be in the constructor of x:

std::string::string( s, "hi" );

and then replacing x with *s everywhere else in the function (and doing nothing at the return).

like image 56
James Kanze Avatar answered Nov 15 '22 18:11

James Kanze


Lets play with NRVO, RVO and copy elision!

Here is a type:

#include <iostream>
struct Verbose {
  Verbose( Verbose const& ){ std::cout << "copy ctor\n"; }
  Verbose( Verbose && ){ std::cout << "move ctor\n"; }
  Verbose& operator=( Verbose const& ){ std::cout << "copy asgn\n"; }
  Verbose& operator=( Verbose && ){ std::cout << "move asgn\n"; }
};

that is pretty verbose.

Here is a function:

Verbose simple() { return {}; }

that is pretty simple, and uses direct construction of its return value. If Verbose lacked a copy or move constructor, the above function would work!

Here is a function that uses RVO:

Verbose simple_RVO() { return Verbose(); }

here the unnamed Verbose() temporary object is being told to copy itself to the return value. RVO means that the compiler can skip that copy, and directly construct Verbose() into the return value, if and only if there is a copy or move constructor. The copy or move constructor is not called, but rather elided.

Here is a function that uses NRVO:

 Verbose simple_NRVO() {
   Verbose retval;
   return retval;
 }

For NRVO to occur, every path must return the exact same object, and you can't be sneaky about it (if you cast the return value to a reference, then return that reference, that will block NRVO). In this case, what the compiler does is construct the named object retval directly into the return value location. Similar to RVO, a copy or move constructor must exist, but is not called.

Here is a function that fails to use NRVO:

 Verbose simple_no_NRVO(bool b) {
   Verbose retval1;
   Verbose retval2;
   if (b)
     return retval1;
   else
     return retval2;
 }

as there are two possible named objects it could return, it cannot construct both of them in the return value location, so it must do an actual copy. In C++11, the object returned will be implicitly moved instead of copied, as it is a local variable being returned from a function in a simple return statement. So there is at least that.

Finally, there is copy elision at the other end:

Verbose v = simple(); // or simple_RVO, or simple_NRVO, or...

When you call a function, you provide it with its arguments, and you inform it where it should put its return value. The caller is responsible for cleaning up the return value and allocating the memory (on the stack) for it.

This communication is done in some way via the calling convention, often implicitly (ie, via the stack pointer).

Under many calling conventions, the location where the return value can be stored can end up being used as a local variable.

In general, if you have a variable of the form:

Verbose v = Verbose();

the implied copy can be elided -- Verbose() is constructed directly in v, rather than a temporary being created then copied to v. In the same way, the return value of simple (or simple_NRVO, or whatever) can be elided if the run time model of the compiler supports it (and it usually does).

Basically, the calling site can tell simple_* to put the return value in a particular spot, and simply treat that spot as the local variable v.

Note that NRVO and RVO and implicit move are all done within the function, and the caller needs know nothing about it.

Similarly, the eliding at the calling site is all done outside the function, and if the calling convention supports it you do not need any support from the body of the function.

This doesn't have to be true in every calling convention and run time model, so the C++ standard makes these optimizations optional.

like image 31
Yakk - Adam Nevraumont Avatar answered Nov 15 '22 18:11

Yakk - Adam Nevraumont