返回值优化(RVO)是一种涉及复制省略的优化技术,它消除了在某些情况下为保存函数返回值而创建的临时对象。我一般都了解RVO的好处,但我有几个问题。
该标准在this working draft的第12段第32段(强调我的)中说明了以下内容。
当满足某些条件时,允许实现省略类对象的复制/移动构造,即使对象的复制/移动构造函数和/或析构函数具有副作用。在这种情况下,实现将省略的复制/移动操作的源和目标视为仅仅两种不同的引用同一对象的方式,并且该对象的销毁发生在两个对象的后期时间。在没有优化的情况下销毁。
然后,当实现可以执行此优化时,它会列出许多条件。
关于这种潜在的优化,我有几个问题:
我习惯于约束优化,使得它们无法改变可观察的行为。此限制似乎不适用于RVO。 我是否需要担心标准中提到的副作用?是否存在可能导致问题的极端情况?
我作为程序员需要做什么(或不做)才能执行此优化?例如,以下是否禁止使用复制省略(由于move
):
std::vector<double> foo(int bar){
std::vector<double> quux(bar,0);
return std::move(quux);
}
我将此作为一个新问题发布,因为我提到的具体问题并未在其他相关问题中直接回答。
答案 0 :(得分:14)
我习惯于约束优化,以至于无法改变可观察的行为。
这是对的。作为一般规则 - 称为 as-if 规则 - 如果无法观察到更改,编译器可以更改代码。
此限制似乎不适用于RVO。
是。 OP中引用的子句给出了 as-if 规则的一个例外,并允许省略复制构造,即使它有副作用。请注意,RVO只是一个copy-elision案例(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找到了解决方法。)
在这个示例中,您可以强制执行RVO(严格来说,这不再是RVO,但为了简单起见,让我们继续这样调用),方法是返回一个 braced-init-list ,返回类型可以是创建:
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{}};
}
请参阅此post和R. Martinho Fernandes精彩的answer。
小心!如果返回类型为std::vector<int>
,则上面的最后一个代码将具有与原始代码不同的行为。 (这是另一个故事。)
答案 1 :(得分:5)
我强烈建议阅读&#34;在C ++对象模型中#34;由Stanely B. Lippman提供有关命名返回值优化如何工作的详细信息和一些历史背景。
例如,在第2.1章中,他有关于命名返回值优化的说法:
在诸如bar()之类的函数中,所有返回语句都返回 相同的命名值,编译器本身可以进行优化 通过将结果参数替换为命名返回来实现该函数 值。例如,给定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的嵌套本地块 悄悄关闭优化。
答案 2 :(得分:4)
它说得很清楚,不是吗?它允许省略具有副作用的ctor。所以你应该从不在ctors中有副作用,或者如果你坚持,你应该使用消除(N)RVO的技术。
至于第二个我认为它禁止NRVO ,因为的候选者删除名称,NRVO需要它(感谢@DyP评论)。 std::move
产生T&&
而不是T
,因为std::move
会产生NRVO(RVO)
刚在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
,所以我们失去了移动ctor的副作用。如果您将std::move
添加到foo()
,则会删除NRVO。
答案 3 :(得分:0)
这可能是显而易见的,但是如果你避免编写带副作用的复制/移动构造函数(大多数不需要它们)那么问题就完全没有实际意义了。即使在简单的副作用情况下,例如施工/破坏计数,它仍然应该没问题。可能令人担心的唯一情况是复杂的副作用,这是重新检查代码的强烈设计气味。
这听起来像是对我不成熟的优化。只需编写明显易于维护的代码,然后让编译器进行优化。只有在分析显示某些区域表现不佳时,您才应考虑采用变更来提高绩效。