【问题标题】:Why should one rely on Named Return Value Optimization?为什么要依赖命名返回值优化?
【发布时间】:2013-08-14 16:47:47
【问题描述】:

我正在阅读有关 NRVO 的信息,并试图了解什么时候应该依赖它,什么时候不应该依赖它。现在我有一个问题:为什么要完全依赖 NRVO?总是可以通过引用显式传递返回参数,那么有什么理由改用 NRVO 吗?

【问题讨论】:

  • 因为有时需要返回一个值,或者复制一个函数参数。

标签: c++ nrvo


【解决方案1】:

处理返回值比处理通过写入引用参数返回的方法更容易。考虑以下两种方法

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

第一个问题是无法链接方法调用

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

它还使auto 等功能无法使用。对于像 C 这样的类型来说问题不大,但对于像 std::map<std::string, std::list<std::string>*> 这样的类型来说更重要

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

也正如 GMacNickG 指出的那样,如果 C 类型有一个普通代码无法使用的私有构造函数怎么办?也许构造函数是private 或者只是没有默认构造函数。 GetByRet 再次像冠军一样工作,GetByParam 失败了

C ret = GetByRet();  // Score! 
// vs.
C temp; // Error! Can't access the constructor 
GetByParam(temp);

【讨论】:

  • 另外,有些类型不能默认构造。 (如果 这是 甚至可能的话,用垃圾参数初始化它会浪费时间。)
  • +1,还有一点要补充:typedef std::vector<std::string> vector; vector readlines(); void readlines(vector*); 第一个告诉你它创建了一个带有读取行的向量。第二个不是很清楚,是不是clear()这个向量?是append吗?
  • 链接和默认构造函数可用性的好例子。谢谢。
【解决方案2】:

这不是一个答案,但在某种意义上也是一个答案……

给定一个通过指针获取参数的函数,有一个简单的转换将产生一个按值返回的函数,并且编译器可以轻松优化。

void f(T *ptr) {     
   // uses ptr->...
}
  1. 在函数中添加对对象的引用,并将ptr的所有使用替换为引用

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

  2. 现在删除参数,添加返回类型,将 T& obj 替换为 T obj 并将所有返回更改为 yield 'obj'

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

  3. 此时,您有一个按值返回的函数,NRVO 对于它来说是微不足道的,因为所有的返回语句都引用同一个对象。

这个转换后的函数与指针传递有一些相同的缺点,但它从来没有比它更糟。但它证明了只要指针传递是一个选项,值返回也是一个选项,具有相同的成本。

费用一模一样?

这超出了语言的范围,但是当编译器生成代码时,它会遵循 ABI(应用程序二进制接口),该 ABI(应用程序二进制接口)允许由不同运行的编译器(甚至同一平台中的不同编译器)构建的代码进行交互。所有当前使用的 ABI 都具有按值函数返回的共同特征:对于 large(不适合寄存器)返回类型,返回对象的内存由调用者分配,函数需要额外的带有该内存位置的指针。那是编译器看到的时候

T f();

调用约定将其转换为:

void mangled_name_for_f( T* __result )

因此,如果您比较替代方案:T t; f(&t);T t = f(); 在这两种情况下,生成的代码都会在调用者的框架 [1] 中分配空间,调用传递指针的函数。在函数结束时,编译器将 [2] 返回。其中 [#] 是在每个备选方案中实际调用对象构造函数的位置。两种选择的成本是相同的,不同之处在于,在 [1] 中,对象必须是默认构造的,而在 [2] 中,您可能已经知道对象的最终值,并且您可以做一些更有效的事情。

关于性能,仅此而已吗?

不是真的。如果您稍后需要将该对象传递给一个按值获取参数的函数,例如void g(T value),在按指针传递的情况下,调用者的堆栈中有一个命名对象,因此必须复制该对象(或移动)到调用约定需要 value 参数的位置。在按值返回的情况下,知道它将调用g(f()) 的编译器知道从f() 返回的对象的唯一用途是作为g() 的参数,因此它可以只传递一个指向适当的指针调用f()时的位置,这意味着不会有任何副本。在这一点上,手动方法开始落后于编译器的方法即使f的实现使用了上面的哑转换!

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

【讨论】:

    【解决方案3】:

    实际上不可能(或不希望)总是通过引用返回一个值(将operator+ 视为一个基本的反例)。

    回答您的问题:您通常不依赖或期望 NRVO 总是发生,但您确实期望编译器能够进行合理的优化工作。只有当分析表明复制返回值代价高昂时,您才需要担心通过提示或备用接口帮助编译器。

    编辑some function could be optimized just by using return parameter

    首先,请记住,如果函数不经常被调用,或者编译器有足够的智能,你就不能保证 return-by-out-parameter 是一种优化。其次,请记住,您将拥有代码的未来维护者,并且编写清晰、易于理解的代码是您可以提供的最大帮助之一(无论代码损坏的速度有多快)。第三,花点时间阅读http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/,看看它是否会改变你的想法。

    【讨论】:

    • 当我知道某些函数可以通过使用返回参数而不是返回值来优化时,我就是睡不好 :) 我应该放轻松并返回值,还是应该使用返回参数可能吗?
    • @SergiiBogomolov:放轻松。更简单的代码更好,而且通常更快。
    【解决方案4】:

    许多人认为将非常量引用参数传递给函数,然后在函数中更改这些参数不是很直观。

    此外,还有许多按值返回结果的预定义运算符(例如,operator+operator- 等算术运算符)。由于您希望保留此类运算符的默认语义(和签名),因此您不得不依赖 NRVO 来优化按值返回的临时对象。

    最后,在许多情况下,按值返回比传入要通过非常量引用(或指针)更改的参数更容易链接。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-04
      • 1970-01-01
      • 2013-10-16
      相关资源
      最近更新 更多