Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Return value optimizations and side-effects

Return value optimization (RVO) is an optimization technique involving copy elision, which eliminates the temporary object created to hold a function's return value in certain situations. I understand the benefit of RVO in general, but I have a couple of questions.

The standard says the following about it in §12.8, paragraph 32 of this working draft (emphasis mine).

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization.

It then lists a number of criteria when the implementation may perform this optimization.


I have a couple of questions regarding this potential optimization:

  1. I am used to optimizations being constrained such that they cannot change observable behaviour. This restriction does not seem to apply to RVO. Do I ever need to worry about the side effects mentioned in the standard? Do corner cases exist where this might cause trouble?

  2. What do I as a programmer need to do (or not do) to allow this optimization to be performed? For example, does the following prohibit the use of copy elision (due to the move):

std::vector<double> foo(int bar){     std::vector<double> quux(bar,0);     return std::move(quux); } 

Edit

I posted this as a new question because the specific questions I mentioned are not directly answered in other, related questions.

like image 420
Marc Claesen Avatar asked Nov 05 '13 15:11

Marc Claesen


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.

Is return value optimization guaranteed?

Compilers often perform Named Return Value Optimization (NRVO) in such cases, but it is not guaranteed.

What is named return value optimization?

(Named) Return value optimization is a common form of copy elision. It refers to the situation where an object returned by value from a method has its copy elided. The example set forth in the standard illustrates named return value optimization, since the object is named.

What is NRVO?

the NRVO (Named Return Value Optimization)


Video Answer


2 Answers

I am used to optimizations being constrained such that they cannot change observable behaviour.

This is correct. As a general rule -- known as the as-if rule -- compilers can change code if the change is not observable.

This restriction does not seem to apply to RVO.

Yes. The clause quoted in the OP gives an exception to the as-if rule and allows copy construction to be omitted, even when it has side effects. Notice that the RVO is just one case of copy-elision (the first bullet point in C++11 12.8/31).

Do I ever need to worry about the side effects mentioned in the standard?

If the copy constructor has side effects such that copy elision when performed causes a problem, then you should reconsider the design. If this is not your code, you should probably consider a better alternative.

What do I as a programmer need to do (or not do) to allow this optimization to be performed?

Basically, if possible, return a local variable (or temporary) with the same cv unqualified type as the function return type. This allows RVO but doens't enforce it (the compiler might not perform RVO).

For example, does the following prohibit the use of copy elision (due to the move):

// notice that I fixed the OP's example by adding <double> std::vector<double> foo(int bar){     std::vector<double> quux(bar, 0);     return std::move(quux); } 

Yes, it does because you're not returning the name of a local variable. This

std::vector<double> foo(int bar){     std::vector<double> quux(bar,0);     return quux; } 

allows RVO. One might be worried that if RVO is not performed then moving is better than coping (which would explain the use of std::move above). Don't worry about that. All major compilers will do the RVO here (at least in release build). Even if a compiler doesn't do RVO but the conditions for RVO are met then it will try to do a move rather than a copy. In summary, using std::move above will certainly make a move. Not using it will likely neither copy nor move anything and, in the worst (unlikely) case, will move.

(Update: As haohaolee's pointed out (see comments), the following paragraphs are not correct. However, I leave them here because they suggest an idea that might work for classes that don't have a constructor taking a std::initializer_list (see the reference at the bottom). For std::vector, haohaolee found a workaround.)

In this example you can force the RVO (strict speaking this is no longer RVO but let's keep calling this way for simplicity) by returning a braced-init-list from which the return type can be created:

std::vector<double> foo(int bar){     return {bar, 0}; // <-- This doesn't work. Next line shows a workaround:     // return {bar, 0.0, std::vector<double>::allocator_type{}}; } 

See this post and R. Martinho Fernandes's brilliant answer.

Be carefull! Have the return type been std::vector<int> the last code above would have a different behavior from the original. (This is another story.)

like image 60
Cassio Neri Avatar answered Oct 04 '22 13:10

Cassio Neri


I highly recommend reading "Inside the C++ Object Model" by Stanely B. Lippman for detailed information and some historical backround on how the named return value optimization works.

For example, in chapter 2.1 he has this to say about named return value optimization:

In a function such as bar(), where all return statements return the same named value, it is possible for the compiler itself to optimize the function by substituting the result argument for the named return value. For example, given the original definition of bar():

X bar()  {     X xx;     // ... process xx     return xx;  }  

__result is substituted for xx by the compiler:

void  bar( X &__result )  {     // default constructor invocation     // Pseudo C++ Code     __result.X::X();      // ... process in __result directly      return;  } 

(....)

Although the NRV optimization provides significant performance improvement, there are several criticisms of this approach. One is that because the optimization is done silently by the compiler, whether it was actually performed is not always clear (particularly since few compilers document the extent of its implementation or whether it is implemented at all). A second is that as the function becomes more complicated, the optimization becomes more difficult to apply. In cfront, for example, the optimization is applied only if all the named return statements occur at the top level of the function. Introduce a nested local block with a return statement, and cfront quietly turns off the optimization.

like image 39
Paul Avatar answered Oct 04 '22 12:10

Paul