Why Does Visual Studio Not Perform Return Value Optimization (Rvo) in This Case

Why return value optimization does not work when return ()

This is NRVO, not RVO.

Here is the rule which allows NRVO (class.copy/31):

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

As you can see, in the case of (a), the expression is not a name (because of the added parenthesis), so NRVO is not allowed.

Why is Visual C++ not performing return-value optimization on the most trivial code?

When I test with this:

#include <iostream>
struct Foo {
Foo(Foo const &r) { std::cout << "Copying...\n"; }
~Foo() { std::cout << "Destructing...\n"; }
Foo() {}
};

Foo foo() { return Foo(); }

int main() { Foo f = foo(); }

...the output I get is:

Destructing...

No invocation of the copy constructor, and only one of the destructor.

Isn't return value optimization (RVO) a bug?

The standard mandates that operations with concern a program's observable state must not be optimized away, except for copy construction in certain circumstances. You must not rely on copy constructors to be executed, even if they have side effects you expect to see (e.g., console output).

Return value optimization in Visual Studio 2015?

Function arguments and return values are initialized using copy-initialization. Copy-initialization requires copy constructors to be accessible, even if they are elided by (N)RVO:

If T is a class type, and the type of other is different, or if T is non-class type, but the type of other is a class type, user-defined conversion sequences that can convert from the type of other to T (or to a type derived from T if T is a class type and a conversion function is available) are examined and the best one is selected through overload resolution. The result of the conversion, which is a prvalue temporary if a converting constructor was used, is then used to direct-initialize the object. The last step is usually optimized out and the result of the conversion is constructed directly in the memory allocated for the target object, but the appropriate constructor (move or copy) is required to be accessible even though it's not used.

Visual Studio not performing RVO when ternary operator is used and move/copy ctors are deleted

Is it a bug?

Yes. This is a bug in MSVC. Pretty much every other compiler that supports C++17 compiles it. Below we have the assembly produced by:

  • ellcc https://godbolt.org/z/PfzDTs
  • gcc https://godbolt.org/z/oXpDyk
  • clang https://godbolt.org/z/KX99Yc
  • power64 AT12.0 https://godbolt.org/z/XvWiEa
  • icc 19.0.1 https://godbolt.org/z/pZWBJ5

And all of them compile it with -std=c++17 or -std=c++1z (for ellcc).

What does the standard say?

Conditional expressions (the ones formed by the ternary operator) produce values according to these rules (see section 8.5.16).

Paragraph 1 of 8.5.16 describes sequencing, and parts 2 through 7 describe the value category of the resulting expression (see section 8.2.1 for a description of value categories).

  • Paragraph 2 covers the case that either the second or third operands are void.
  • Paragraph 3 covers the case that both the second and third operands are glvalued bitfields (i.e., not prvalues)
  • Paragraph 4 covers the case the second and third operands have different types
  • Paragraph 5 covers the case that the second and third operands are glvalues of the same type (also not prvalues)
  • Paragraph 6:

Otherwise, the result is a prvalue. If the second and third operands do not have the same type, and either has (possibly cv-qualified) class type, overload resolution is used to determine the conversions (if any) to be applied to the operands (16.3.1.2, 16.6). If the overload resolution fails, the program is ill-formed. Otherwise, the conversions thus determined are applied, and the converted operands are used in place of the original operands for the remainder of this subclause.

This gives us our answer. The result is a prvalue, so it's unnecessary to use the copy or move constructors, as the value will be instantiated in the memory provided by the calling function (this memory location is passed as a "hidden" parameter to your function).

Does your program implicitly refer to the move constructor or the copy constructor?

Jon Harper was kind enough to point out that the standard states:

A program that refers to a deleted function implicitly or explicitly, other than to declare it, is ill-formed. (11.4.3.2)

This begs the question: does your program implicitly refer to the move constructor or the copy constructor?

The answer to this is no. Because the result of the conditional expression is a prvalue, no temporary is materialized, and as a result neither the move constructor or the copy constructor is referenced, either explicitly or implicitly. To quote cppreference (emphasis mine):

Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. The copy/move constructors need not be present or accessible, as the language rules ensure that no copy/move operation takes place, even conceptually:

  • In a return statement, when the operand is a prvalue of the same class type (ignoring cv-qualification) as the function return type:

    T f() { return T(); }

    f(); // only one call to default constructor of T

  • In the initialization of a variable, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the variable type:

T x = T(T(f())); // only one call to default constructor of T, to initialize x

Distinguishing between NRVO and RVO

One source of contention is whether or not Copy Elision is guaranteed. It is important to distinguish between Named Return Value Optimization, and pure Return Value Optimization.

If you return a local variable, it's not guaranteed. This is Named Return Value Optimization. If your return statement is an expression that's a prvalue, it is guaranteed.

For example:

NoCopyOrMove foo() {
NoCopyOrMove myVar{}; //Initialize
return myVar; //Error: Move constructor deleted
}

I am returning a an expression (myVar) that is the name of an object of automatic storage. In this case, return value optimization is permitted but not guaranteed. Section 15.8.3 of the standard applies here.

On the other hand, if I write:

NoCopyOrMove foo() {
return NoCopyOrMove(); // No error (C++17 and above)
}

Copy Elision is guaranteed, and no copy or move takes place. Similarly, if I write:

NoCopyOrMove foo(); //declare foo
NoCopyOrMove bar() {
return foo(); //Returns what foo returns
}

Copy Elision is still guaranteed because the result of foo() is a prvalue.

Conclusion

MSVC does, in fact, have a bug.

Why is partial RVO not performed?

Let's consider what happens if RVO is done for f, meaning it is constructed directly in the return value. If first==true and f gets returned, great, no copy is needed. But if first==false then s gets returned instead, so the program will copy construct s over the top of f before the destructor for f has run. Then after that, the destructor for f will run, and now the return value is an invalid object that has already been destroyed!

If RVO is done for s instead the same argument applies, except that now the problem happens when first==true.

Whichever one you choose, you avoid a copy in 50% of cases and get undefined behaviour in the other 50% of cases! That's not a desirable optimization!

In order to make this work the order of destruction of the local variables would have to be altered so that f is destroyed before copying s into that memory location (or vice versa), and that's a very risky thing to mess with. The order of destruction is a fundamental property of C++ that should not be fiddled with, or you'll break RAII and who knows how many other assumptions.

Will RVO work in this case?

The compiler is allowed to do RVO here, but it is not required to do so, cf. cppreference

If a function returns a class type by value, and the return statement's expression is the name of a non-volatile object with automatic storage duration, which isn't a function parameter, or a catch clause parameter, and which has the same type (ignoring top-level cv-qualification) as the return type of the function, then copy/move (since C++11) is omitted. When that local object is constructed, it is constructed directly in the storage where the function's return value would otherwise be moved or copied to. This variant of copy elision is known as NRVO, "named return value optimization".

So the compiler cannot optimize copying retObject when returning, but it can omit creating the temporary return value object and instead directly copy retObject to whatever MyClass::BuildObject is assigned.

Why does Return Value Optimization not happen if no destructor is defined?

The language rule which allows this in case of returning a prvalue (the second example) is:

[class.temporary]

When an object of class type X is passed to or returned from a function, if X has at least one eligible copy or move constructor ([special]), each such constructor is trivial, and the destructor of X is either trivial or deleted, implementations are permitted to create a temporary object to hold the function parameter or result object.
The temporary object is constructed from the function argument or return value, respectively, and the function's parameter or return object is initialized as if by using the eligible trivial constructor to copy the temporary (even if that constructor is inaccessible or would not be selected by overload resolution to perform a copy or move of the object).
[Note: This latitude is granted to allow objects of class type to be passed to or returned from functions in registers.
— end note
]



Why does Return Value Optimization not happen [in some cases]?

The motivation for the rule is explained in the note of the quoted rule. Essentially, RVO is sometimes less efficient than no RVO.

If a destructor is defined by enabling the #if above, then the RVO does happen (and it also happens in some other cases such as defining a virtual method or adding a std::string member).

In the second case, this is explained by the rule because creating the temporary is only allowed when the destructor is trivial.

In the NRVO case, I suppose this is up to the language implementation.

C++ Return Value Optimization

Return Value Optimization (RVO) states that a compiler can elide one or both copies, but it is not required. This means that:

A a (Foo());

Is free to do 0, 1, or 2 copy constructors:

2 - In function Foo(), A a creates an A. When it tries to return, it copies the A into the return value; the resulting initialization A a(Foo()); copies the result of Foo() into a new A.

1 - One of those copies doesn't happen (probably the copy into Foo's return value.

0 - Neither of those copies happen. The A a created inside of Foo directly becomes the A created in this line: A a(Foo());

Msdn has a lot of detail about how the visual c++ compiler handles RVO. It has some neat code examples that explain how it effectively works.



Related Topics



Leave a reply



Submit