RVO(返回值优化)适用于所有对象吗?

54

RVO(返回值优化)在C++编译器中是否对所有对象和情况都有保证或适用(特别是GCC)?

如果答案是否定的,那么这种优化对类/对象的条件是什么?如何强制或鼓励编译器对特定的返回值进行RVO?

5个回答

52
返回值优化(Return Value Optimization)通常都可以应用,但是命名返回值优化(Named Return Value Optimization)并不是普遍适用的。简而言之,在进行优化时,编译器必须在构造对象的位置知道哪个对象将被返回。
在 RVO 的情况下(其中会返回一个临时对象),这个条件很显然就满足了:对象在 return 语句中被构造,然后返回。
在 NRVO 的情况下,你需要分析代码以了解编译器是否能够得到信息。如果函数的分析很简单,那么编译器很可能会对其进行优化(例如只有一个包含非条件性单个 return 语句的函数;多个返回同一对象的 return 语句;类似于 T f() { if (condition) { T r; return r; } else { T r2; return r2; } } 的多个返回语句,编译器知道 rr2 会被返回...)。
请注意,在简单情况下才能假定进行优化,具体而言,维基百科中的示例实际上可以通过智能的编译器进行优化。
std::string f( bool x ) {
   std::string a("a"), b("b");
   if ( x ) return a; 
   else return b;
}

可以被编译器重写为:

std::string f( bool x ) {
   if ( x ) {
      std::string a("a"), b("b");
      return a;
   } else {
      std::string a("a"), b("b");
      return b;
   }
}

编译器可以知道,在第一个分支中,a 将被构造以替代返回的对象,在第二个分支中也是如此对 b。但我不会指望它。如果代码比较复杂,就假设编译器不能产生优化。

编辑:还有一种情况我没有明确提到,编译器不允许(在大多数情况下即使允许,也不可能这样做)从函数参数到返回语句中的拷贝进行优化:

T f( T value ) { return value; } // Cannot be optimized away --but can be converted into
                                 // a move operation if available.

最好更正维基百科文章及其引用的参考资料,包括 Hinnant 文章。 - Lightness Races in Orbit
维基百科的文章并没有“错误”,示例中的注释明确说明了“RVO可能不会被应用”。我只是想指出,取决于编译器,甚至可以避免非平凡的复制。 - David Rodríguez - dribeas
3
请注意,RVO / NRVO仅适用于返回临时对象的情况,即通常在复制后会被销毁的对象。例如,在从成员函数返回成员变量时,必须进行复制。这对一些人来说可能很明显,但是我不得不向多个人解释这一点,因为他们刚学习关于RVO的知识。 - JDiMatteo
1
@DavidRodríguez-dribeas,您能解释一下您的最终示例吗?value不是通过传值方式临时生成的吗?因此完全符合RVO的条件,对吗? - Owen
1
@Owen:value不会超出函数的生命周期,但它也不是临时的(虽然这并不重要)。该语言没有为省略该复制提供规定,这就是注释的原因。至于为什么,复制省略是通过直接在复制将要占用的内存上创建复制来完成的,然后就不需要复制了。常见的ABIs让调用者保留返回对象的空间,并且调用者也会复制参数。但在一个具有分离编译的语言中,调用者不知道函数是否会返回该参数[...] - David Rodríguez - dribeas
即使知道了,ABI 也可能不允许省略它,因为它可能会确定参数的固定位置。 - David Rodríguez - dribeas

5

在gcc编译器中,是否保证所有对象都能使用RVO(返回值优化)?

没有任何优化是保证的(尽管RVO相当可靠,但确实存在某些情况会影响它)。

如果答案是“否”,那么类/对象进行此优化的条件是什么?

这是一个故意从您抽象出来的实现细节。

请不要关心或了解此问题。


7
你最后的观点有些像Java语言的评论.. ;) 知道并没有坏处,虽然这可能是实现特定的,但如果倾向于知道的话,为什么不呢?不过,只有在分析时才需要关心它... - Nim
1
@Nim:想要了解正在进行哪些优化是“错误”的。好吧,如果你真的在为某个关键任务的系统做紧缩优化,那么分析可能会引导你进入黑暗艺术的领域,让你具体了解编译器的操作,但这不应该是默认的方法。 - Lightness Races in Orbit
@Nim:因为这些条件随时可能会改变。它们实际上是不可靠的。 - Puppy
2
RVO 可以总是应用,NRVO 有时则不能应用。 - David Rodríguez - dribeas
1
对于许多(非关键任务)应用程序而言,“在大多数情况下应该进行优化”已经足够好了,因此需要知道何时通常执行此操作的原因。如果通过轻微修改程序可以在大多数情况下获得显着的性能提升,为什么不呢? - Piyush Soni
显示剩余7条评论

3

移动语义(C++11的新特性)是您问题的解决方案,它允许您显式地使用Type(Type &&r);移动构造函数)而不是Type(const Type &r)复制构造函数)。

例如:

class String {
  public:    
    char *buffer;

    String(const char *s) { 
      int n = strlen(s) + 1;
      buffer = new char[n];
      memcpy(buffer, s, n);
    }

    ~String() { delete [] buffer; }
    
    String(const String &r) { 
      // traditional copy ...
    }

    String(String &&r) {
      buffer = r.buffer; // O(1), No copying, saves time.
      r.buffer = 0;
    }
};

String hello(bool world) {
  if (world) {
    return String("Hello, world.");
  } else {
    return String("Hello.");
  }
}

int main() {
  String foo = hello();
  std::cout <<foo.buffer <<std::endl;
}

这不会触发复制构���函数


返回-1,因为如果你使用return std::move(temp_object),对象可能会超出范围(简单示例,T t; return std::move(t);),并且您将得到无效引用。在该示例中使用String可能会起作用,因为String保存在池中,即使超出范围也可能不会被删除,但一般情况下不要相信它。 - WorldSEnder
@WorldSEnder 我在这里没有看到无效引用。return一个对象将使其构造一个新对象(通过复制或移动),而不是临时对象的引用。只有当返回类型为引用或指针时,你所说的才会发生。 - RnMss
如果你不返回String&&,那么你是正确的。谢谢,我以为它会有所不同。 - WorldSEnder
对于仍然遇到这个问题的人,请注意在返回语句中不应该使用std::move。这样做没有任何好处,实际上可能会阻止编译器利用RVO。具有讽刺意味的是,这回答了“是否适用于所有对象的RVO?”的问题,因为它产生了一个不允许使用RVO的情况。 - Brian61354270
@Brian 我稍微编辑了一下答案。嗯...,那是几年前的事了,我不理解像pr-values、x-values这样的机制。现在我们有了C++17(很快就会有C++20),其中复制省略已经完全标准化,我可能不会提到移动语义。然而,这个答案是为了处理无法保证RVO的情况。 - RnMss
@Brian,“你不应该在返回语句中使用std::move”,这是不正确的,如果值是一个右值引用。例如:T foo(T&& x) { return x; } 这将执行复制构造而不是移动构造。为了避免复制构造,你必须写成 return std::move(x);。所以,如果你不想要一份拷贝但不确定是否有拷贝省略,就使用move,它不会有任何问题。 - RnMss

3

给Jesper:如果要构建的对象很大,避免复制可能是必要的(或者至少非常可取)。

如果发生RVO,则避免了复制,您无需再编写任何代码。

如果没有发生RVO,则您将不得不手动执行此操作,自己编写额外的脚手架。这可能涉及事先指定缓冲区,强制您为此空(可能无效,您可以看到这不干净)对象编写构造函数和“构造”此无效对象的方法。

因此,“如果保证可以减少我的代码行数。这不是吗?”并不意味着Masoud是个白痴。但不幸的是,RVO不能保证。您必须测试它是否发生,如果没有,编写脚手架并污染设计。这是无法避免的。


0

我没有一个肯定或否定的答案,但你说如果你要寻找的优化是有保证的话,你可以写更少的代码。

如果你写了你需要写的代码,程序总是会工作的,如果优化存在,它会更快地工作。如果确实存在这样一种情况,即优化“填补”逻辑而不是代码机制并使其工作,或者直接改变逻辑,那么这似乎是一个错误,我希望修复它,而不是依赖或利用实现细节。


我不介意被踩,但请解释原因。我不明白说“请不要让你的代码依赖于优化”有什么问题。 - Jesper
我没有给你点踩,但我不确定这个回答是否真正解答了问题。它有点偏离了主题。 :) - Lightness Races in Orbit
这很公平。有时候答案真的是“求你别这么做”,尽管我认为这个问题远未达到那种程度。 - Jesper
1
@MasoudM。别说我疯了,但我仍然坚持认为,如果某个优化缺失时代码无法正常工作,那么这段代码就是有问题的。进行优化的原因是为了加速逻辑运行,而不是为了“修复”它。这与知道优化发生的位置并简单地编写代码以针对它们是不同的——即使该优化消失或被禁用,您的代码仍将正常工作。 - Jesper

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接