【问题标题】:Return value optimizations and side-effects返回值优化和副作用
【发布时间】:2013-11-16 12:18:16
【问题描述】:

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

该标准在this working draft(强调我的)第 32 段第 12.8 节中对此做了以下说明。

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

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


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

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

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

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

编辑

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

【问题讨论】:

  • 嗯,可能不是重复的。你最好重新提出这个问题。
  • @RedX 我会更改标题。它并不是真正的重复,但我明白你为什么怀疑它。
  • auto x = foo(42); 有两种可能的优化 1) 从 quux 复制/移动到临时返回值。 2) 从返回值临时复制/移动到x。第一个是 NRVO,只有当返回语句中的表达式是 name 时才会发生(即move(quux) 禁止该优化)。第二个仍然可以应用。

标签: c++ c++11 compiler-optimization


【解决方案1】:

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

这是正确的。作为一般规则 - 称为 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 所指出的(参见 cmets),以下段落不正确。但是,我将它们留在这里,因为它们提出了一个可能适用于没有的类的想法采用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{}};
}

看看postR. Martinho Fernandes 的精彩answer

小心!返回类型是否为std::vector&lt;int&gt; 上面的最后一个代码将具有与原始代码不同的行为。 (这是另一个故事。)

【讨论】:

  • 我对另一个故事很好奇。我知道如果它是 vector, { bar, 0 } 会创建一个包含 2 个元素的向量,但是如何强制多个参数调用而不是初始化列表调用?
  • 对于std::vector&lt;double&gt;,我没有让它在 GCC 4.8.1 上运行。它创建一个包含两个元素 bar 和 0 的向量,除非添加第三个参数 std::vector&lt;double&gt;::allocator_type()
  • @haohaolee:你是对的。它不像我预期的那样工作,因为采用std::initializer_list 的构造函数优先。您更改此优先级的解决方法有效。但是,使用allocator_type 来控制重载解决方案有点奇怪。我不是在责怪你的做法,它更像是一个 C++(核心和库)问题。我将更新帖子以反映这些新发现。谢谢。
  • 嗨,不幸的是它仍然有问题,int bar 应该是std::vector&lt;double&gt;::size_type bar,因为braced-init-list 不允许缩小转换(gcc 对此给出警告)
  • @haohaolee 我知道,但我犹豫要不要使用 std::vector&lt;double&gt;::size_type,以免与 OP 有太多分歧。 :-(
【解决方案2】:

我强烈推荐阅读 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 引入嵌套的本地块 悄悄关闭优化。

【讨论】:

  • 我同意它令人困惑,并添加了引号。
【解决方案3】:

它说得很清楚,不是吗?它允许省略具有副作用的 ctor。因此,您应该永远对 ctor 产生副作用,或者如果您坚持,您应该使用消除 (N)RVO 的技术。 至于第二个,我认为它禁止 NRVO 因为 std::move 产生 T&amp;&amp; 而不是 T 这将成为 NRVO(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::move 添加到foo(),您将消除NRVO。

【讨论】:

  • 与 g++ 4.7 的行为相同 ... 符合预期。
  • 问题是关于标准的保证。虽然来自流行供应商的样本很有用,但它似乎并不能表明可以依赖的行为。
  • “至于第二个,我认为它禁止 NRVO,因为 std::move 产生 T&amp;&amp; 而不是 T,这将成为 NRVO(RVO) 的候选者”在分析表达式 [expr]/5 之前删除。禁止它的原因是NRVO需要对象的名称
  • @BrianCain,OP 自己引用了标准,我只是把它放到了例子中。
【解决方案4】:
  1. 这可能很明显,但是如果您避免编写具有副作用的复制/移动构造函数(大多数都不需要它们),那么问题就完全没有实际意义了。即使在简单的副作用情况下,例如构造/破坏计数,它仍然应该没问题。唯一可能担心的情况是复杂的副作用,这是重新检查代码的强烈设计气味。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-06-09
    • 2015-07-12
    • 2014-09-08
    • 1970-01-01
    • 2013-02-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多