Jonathan Boccara's blog

Return value optimizations

Published November 28, 2016 - 2 Comments

Daily C++

In Make your functions functional, we saw that it was preferable to have functions return objects by value, for code clarity.

Now to avoid incurring performance costs by doing this, the compiler can optimize away the copies related to the return by value, if you respect a few simple rules. This post describes these optimizations and shows how to benefit from them in your code.

There are 2 optimizations related to return value:

  • the RVO (Return Value Optimization),
  • the NRVO (Named Return Value Optimization)

To understand these optimizations, consider the object at call site being assigned the value returned by a function (returning by value):

T t = f();

The common idea of these two optimizations is to allow the compiler to use the memory space of this object t, which is outside the function, to directly construct the object being initialized inside the function and that is returned from it. This effectively removes the need for copying intermediary objects.

The RVO

For example, consider the following function returning by value:

T f()
{
    ....
    return T(constructor arguments);
}

With, at call site:

T t = f();

Theoretically, there could be 3 objects of type T created here:

  • the object constructed inside f in the return statement (which happens to be a temporary because it does not have a name),
  • the temporary object returned by f, copied from the one above,
  • the named object t, copied from the one above.

The RVO lets the compiler remove the two temporaries by directly initializing t with the constructor arguments passed inside the body of f.

EDIT: Note that the RVO can still apply even when the function has several return statements, as long as the returned objects are created on the return statements (thank you Rainer for pointing this out):

T f()
{
    if (....)
    {
        return T(....);
    }
    else
    {
        return T(....);
    }
}

But for the RVO to be applied, the returned object has to be constructed on a return statement. Therefore this object does not have a name.

The NRVO

The NRVO (Named-RVO) goes one step further: it can remove the intermediary objects even if the returned object has a name and is therefore not constructed on the return statement. So this object can be constructed before the return statement, like in the following example:

T f()
{
    T result(....);
    ....
    return result;
}

But, like with the RVO, the function still needs to return a unique object (which is the case on the above example), so that the compiler can determine which object inside of f it has to construct at the memory location of t (outside of f).

For example, the NRVO can still be applied in the following case:

T f()
{
    T result(....);
    if (....)
    {
        return result;
    }
    ....
    return result;
}

because only one object, result, can be returned from the function.

Note though that compilers have different optimization capabilities, and there is no guarantee that the above optimizations will be applied (although this might be enforced in a future version of the standard for some cases). As a general rule, virtually all compilers apply RVO, and NRVO is applied by most compilers where the function is not too complex (and this varies from compiler to compiler).

But as a developer, you can always try to faciliate RVO and NRVO by returning only one object from all the return paths of your functions, and by limiting the complexity in the structure of your functions.

This will avoid incurring performance costs when returning by value from a function, thus letting you benefit from better code clarity and expressiveness.

 

Related articles

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin

Comments are closed