Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why should one rely on Named Return Value Optimization?

Tags:

c++

nrvo

I was reading about NRVO and trying to understand when should one rely on it and when not. Now I have a question: why should one rely on NRVO at all? It is always possible to explicitly pass a return parameter by reference, so is there any reason to rely on NRVO instead?

like image 782
Sergii Bogomolov Avatar asked Dec 16 '22 07:12

Sergii Bogomolov


2 Answers

Dealing with return values is simply easier than dealing with methods that return by writing to a reference parameter. Consider the following 2 methods

C GetByRet() { ... }
void GetByParam(C& returnValue) { ... }

First problem is that it makes it impossible to chain method calls

Method(GetByRet());  
// vs. 
C temp;
GetByParam(temp);
Method(temp);

It also makes features like auto impossible to use. Not so much of a problem for a type like C but more important for types like std::map<std::string, std::list<std::string>*>

auto ret = GetByRet();
// vs.
auto value; // Error! 
GetByParam(value);

Also as GMacNickG pointed out, what if the type C has a private constructor that normal code can't use? Maybe the constructor is private or there just isn't a default constructor. Once again GetByRet works like a champ and GetByParam fails

C ret = GetByRet();  // Score! 
// vs.
C temp; // Error! Can't access the constructor 
GetByParam(temp);
like image 195
JaredPar Avatar answered Dec 29 '22 19:12

JaredPar


This is not an answer, but it is also an answer in some sense...

Given a function that takes an argument by pointer, there is a trivial transformation that will yield a function that returns by value and is trivially optimizable by the compiler.

void f(T *ptr) {     
   // uses ptr->...
}
  1. Add a reference to the object in the function and replace all uses of ptr with the reference

    void f(T *ptr) { T & obj = *ptr; /* uses obj. instead of ptr-> */ }

  2. Now remove the argument, add the return type, replace T& obj with T obj and change all returns to yield 'obj'

    T f() { T obj; // No longer a ref! /* code does not change */ return obj; }

  3. At this point you have a function that returns by value for which NRVO is trivial, since all of the return statements refer to the same object.

This transformed function has some of the same shortcomings that the pass by pointer has, but it is never worse that it. But it demonstrates that whenever pass by pointer is an option, return by value is also an option with the same cost.

Exactly the same cost?

This is beyond the language, but when the compiler generates code it does so following an ABI (Application Binary Interface) that allows code build by different runs of the compiler (or even different compilers in the same platform) to interact. All currently used ABIs share a common trait for return by value functions: For large (does not fit in registers) return types, memory for the returned object is allocated by the caller, and the function takes an extra pointer with the location of that memory. That is when the compiler sees

T f();

The calling convention transforms that into:

void mangled_name_for_f( T* __result )

So if you compare the alternatives: T t; f(&t); and T t = f(); in both cases the generated code allocates the space in the caller's frame, [1], calls a function passing a pointer. At the end of the function the compiler will [2] return. Where [#] is the location where the object's constructor is actually called in each one of the alternatives. The costs of both alternatives are the same, with the difference that in [1] the object must be default constructed, while in [2] you might already know the final values of the object and you might be able to do something more efficient.

Regarding performance, is that all there is?

Not really. If you later need to pass that object to a function that takes the argument by value say void g(T value), in the case of pass-by-pointer, there is a named object in the caller's stack, so the object must be copied (or moved) to the location where the calling convention requires the value argument to be. In the case of return by value, the compiler knowing that it will call g(f()) knows that the only use of the returned object from f() is being the argument of g(), so it can just pass a pointer to the appropriate location when calling f(), which means that there won't be any copies done. At this point, the manual approach starts falling really behind the compiler's approach even if the implementation of f uses the dumb transformation above!

T obj;    // default initialize
f(&obj);  // assign (or modify in place)
g(obj);   // copy

g(f());   // single object is returned and passed to g(), no copies
like image 44
David Rodríguez - dribeas Avatar answered Dec 29 '22 20:12

David Rodríguez - dribeas