27

返回值优化 (RVO) 是一种涉及复制省略的优化技术,它消除了在某些情况下为保存函数返回值而创建的临时对象。我总体上了解 RVO 的好处,但我有几个问题。

该标准在本工作草案的第 12.8 节第 32 段(强调我的)中对此进行了如下说明。

当满足某些标准时,允许实现省略类对象的复制/移动构造,即使对象的复制/移动构造函数和/或析构函数具有副作用。在这种情况下,实现将省略的复制/移动操作的源和目标简单地视为引用同一对象的两种不同方式,并且该对象的销毁发生在两个对象本应被删除的较晚时间。没有优化就被破坏了。

然后它列出了当实现可以执行此优化时的许多标准。


关于这种潜在的优化,我有几个问题:

  1. 我习惯于优化受到限制,以至于它们无法改变可观察到的行为。此限制似乎不适用于 RVO。我是否需要担心标准中提到的副作用?是否存在可能导致麻烦的极端情况?

  2. 作为程序员,我需要做什么(或不做什么)才能执行此优化?例如,以下是否禁止使用复制省略(由于move):

std::vector<double> foo(int bar){
    std::vector<double> quux(bar,0);
    return std::move(quux);
}

编辑

我将此作为一个新问题发布,因为我提到的具体问题没有在其他相关问题中直接回答。

4

4 回答 4

14

我习惯于优化受到限制,以至于它们无法改变可观察到的行为。

这是对的。作为一般规则 - 称为as-if规则 - 如果更改不可观察,编译器可以更改代码。

此限制似乎不适用于 RVO。

是的。OP 中引用的子句为 as-if规则提供了一个例外,并允许省略复制构造,即使它有副作用。请注意,RVO 只是复制省略的一种情况(C++11 12.8/31 中的第一个要点)。

我是否需要担心标准中提到的副作用?

如果复制构造函数具有副作用,例如执行复制省略会导致问题,那么您应该重新考虑设计。如果这不是您的代码,您可能应该考虑一个更好的选择。

作为程序员,我需要做什么(或不做什么)才能执行此优化?

基本上,如果可能,返回一个与函数返回类型具有相同 cv 非限定类型的局部变量(或临时变量)。这允许 RVO,但不强制执行(编译器可能不执行 RVO)。

例如,以下是否禁止使用复制省略(由于移动):

// 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);
}

是的,它确实是因为您没有返回局部变量的名称。这个

std::vector<double> foo(int bar){
    std::vector<double> quux(bar,0);
    return quux;
}

允许 RVO。有人可能会担心,如果不执行 RVO,那么移动比应对更好(这将解释std::move上面的用法)。别担心。所有主要编译器都将在此处执行 RVO(至少在发布版本中)。即使编译器不执行 RVO 但满足 RVO 的条件,它也会尝试执行移动而不是复制。综上所述,使用std::move上面肯定会有所动作。不使用它可能既不会复制也不会移动任何东西,并且在最坏(不太可能)的情况下会移动。

更新:正如 haohaolee 指出的(见评论),以下段落是不正确的。但是,我把它们留在这里是因为它们提出了一个可能适用于没有构造函数的类的想法std::initializer_list(参见参考资料底部)。对于std::vector,haohaolee 找到了解决方法。)

在这个例子中,你可以通过返回一个可以创建返回类型的braced-init-list来强制RVO(严格来说这不再是RVO,但为了简单起见让我们继续这样调用) :

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{}};
}

请参阅这篇文章R. Martinho Fernandes的精彩回答

小心点!如果返回类型是std::vector<int>上面的最后一个代码,则会有与原始代码不同的行为。(这是另一个故事。)

于 2013-11-05T15:42:49.777 回答
5

我强烈推荐阅读 Stanely B. Lippman 的“Inside the C++ Object Model”,了解详细信息和一些关于命名返回值优化如何工作的历史背景。

例如,在第 2.1 章中,他谈到了命名返回值优化:

在诸如 bar() 之类的函数中,所有 return 语句都返回相同的命名值,编译器本身可以通过将结果参数替换命名返回值来优化函数。例如,给定 bar() 的原始定义:

X bar() 
{ 
   X xx; 
   // ... process xx 
   return xx; 
} 

__result 被编译器替换为 xx:

void 
bar( X &__result ) 
{ 
   // default constructor invocation 
   // Pseudo C++ Code 
   __result.X::X(); 

   // ... process in __result directly 

   return; 
}

(……)

尽管 NRV 优化提供了显着的性能改进,但这种方法存在一些批评。一是因为优化是由编译器静默完成的,它是否实际执行并不总是很清楚(特别是因为很少有编译器记录它的实现程度或它是否被实现)。第二个是随着功能变得更加复杂,优化变得更加难以应用。例如,在 cfront 中,仅当所有命名的 return 语句都出现在函数的顶层时才应用优化。引入带有 return 语句的嵌套本地块,cfront 悄悄地关闭优化。

于 2013-11-05T15:31:05.830 回答
4

它说得很清楚,不是吗?它允许省略具有副作用的 ctor。所以你不应该ctors中产生副作用,或者如果你坚持,你应该使用消除(N)RVO的技术。至于第二个,我认为它禁止 NRVO ,因为它是std::move生产的T&&,而不是TNRVO(RVO) 的候选者,因为std::move删除了名称并且 NRVO 需要它(感谢@DyP 评论)。

刚刚在 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;
}

它会产生Ctor,所以我们已经失去了 move ctor 的副作用。如果您添加std::movefoo()您将消除 NRVO。

于 2013-11-05T15:14:59.283 回答
0
  1. 这可能很明显,但是如果您避免编写具有副作用的复制/移动构造函数(大多数都不需要它们),那么问题就完全没有意义了。即使在简单的副作用情况下,例如构造/破坏计数,它仍然应该没问题。唯一可能担心的情况是复杂的副作用,这是重新检查代码的强烈设计气味。

  2. 这对我来说听起来像是过早的优化。只需编写明显的、易于维护的代码,然后让编译器进行优化。只有当分析显示某些区域表现不佳时,您才应考虑采用更改来提高性能。

于 2013-11-05T16:18:07.117 回答