Return value optimizations and side-effects

落爺英雄遲暮 提交于 2019-12-18 10:34:01

问题


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.


回答1:


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.)




回答2:


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.




回答3:


It states it pretty clear, doesn't it? It allows to omit ctor with side effects. So you should never have side effects in ctors or if you insist, you should use techniques which eliminate (N)RVO. As to the second I believe it prohibits NRVO since std::move produces T&& and not T which would be candidate for NRVO(RVO) because std::move removes name and NRVO requires it(thanks to @DyP comment).

Just tested the following code on MSVC:

#include <iostream>

class A
{
public:
    A()
    {
        std::cout << "Ctor\n";
    }
    A(const A&)
    {
        std::cout << "Copy ctor\n";
    }
    A(A&&)
    {
        std::cout << "Move\n";
    }

};

A foo()
{
    A a;
    return a;
}

int main() 
{
    A a = foo();
    return 0;
}

it produces Ctor, so we have lost side effects for move ctor. And if you add std::move to foo() you will have NRVO eliminated.




回答4:


  1. This is probably obvious but if you avoid writing copy/move constructors with side effects (most have no need for them) then the problem is totally moot. Even in simple side effect cases like construction/destruction counting it should still be fine. The only case to possibly worry is complicated side effects and that's a strong design smell to re-examime your code.

  2. This sounds like premature optimization to me. Just write the obvious, easily maintainable code, and let the compiler optimize. Only if profiling shows that certain areas are performing poorly should you consider adopting changes to improve performance.



来源:https://stackoverflow.com/questions/19792135/return-value-optimizations-and-side-effects

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!