【问题标题】:RVO force compilation error on failure失败时 RVO 强制编译错误
【发布时间】:2013-10-16 05:17:34
【问题描述】:

这里有很多关于何时可以完成 RVO 的讨论,但关于何时实际完成的讨论不多。如前所述,RVO 无法根据标准得到保证,但有没有办法保证 RVO 优化成功或相应代码编译失败?

到目前为止,当 RVO 失败时,我部分成功地使代码问题链接错误。为此,我声明了复制构造函数而不定义它们。显然,在我需要实现一个或两个复制构造函数的非罕见情况下,这既不可靠也不可行,即x(x&&)x(x const&)

这就引出了我的第二个问题:为什么编译器编写者选择在用户定义的复制构造函数就位时启用 RVO,而不是在只有默认的复制构造函数存在时?

第三个问题:还有其他方法可以为纯数据结构启用 RVO 吗?

最后一个问题(承诺):你知道任何编译器使我的测试代码表现得与我在 gcc 和 clang 中观察到的不同吗?

这里是 gcc 4.6、gcc 4.8 和 clang 3.3 的一些示例代码,显示了该问题。该行为不依赖于一般优化或调试设置。当然,选项 --no-elide-constructors 会按照它所说的去做,即关闭 RVO。

#include <iostream>
using namespace std;

struct x
{
    x () { cout << "original x address" << this << endl; }
};
x make_x ()
{
    return x();
}

struct y
{
    y () { cout << "original y address" << this << endl; }
    // Any of the next two constructors will enable RVO even if only
    // declared but not defined. Default constructors will not do!
    y(y const & rhs);
    y(y && rhs);
};
y make_y ()
{
    return y();
}

int main ()
{
    auto x1 = make_x();
    cout << "copy of  x address" << &x1 << endl;
    auto y1 = make_y();
    cout << "copy of  y address" << &y1 << endl;
}

输出:

original x address0x7fff8ef01dff
copy of  x address0x7fff8ef01e2e
original y address0x7fff8ef01e2f
copy of  y address0x7fff8ef01e2f

RVO 似乎也不适用于普通数据结构:

#include <iostream>

using namespace std;

struct x
{
    int a;
};

x make_x ()
{
    x tmp;
    cout << "original x address" << &tmp << endl;
    return tmp;
}

int main ()
{
    auto x1 = make_x();
    cout << "copy of  x address" << &x1 << endl;
}

输出:

original x address0x7fffe7bb2320
copy of  x address0x7fffe7bb2350

更新:请注意,一些优化很容易与 RVO 混淆。像make_x 这样的构造函数助手就是一个例子。请参阅this example,其中优化实际上是由标准强制执行的。

【问题讨论】:

  • 我认为 make_x 不使用 RVO,因为 x 太小了,只复制它会更有效。如果你让x 包含一个更大的数组,它会在我的机器上执行 RVO。
  • 既然 C++ 有移动语义,你为什么还要关心执行 RVO?
  • @DanielKO,在那些 RVO/NRVO 比移动对象性能更高的情况下,编译器通常使用 RVO 而不是移动。
  • @DavidBrown,你说得对。我更新了我的帖子并添加了对我相信您喜欢的另一篇帖子的引用。

标签: c++ c++11 inline compiler-optimization copy-elision


【解决方案1】:

问题是编译器做了太多优化:)

首先,我禁用了 make_x() 的内联,否则我们无法区分 RVO 和内联。但是,我确实将其余部分放入了匿名命名空间,以便外部链接不会干扰任何其他编译器优化。 (有证据表明,例如,外部链接可以防止内联,谁知道还有什么......)我重写了输入输出,现在它使用printf();否则生成的汇编代码会因为所有iostream 的东西而变得杂乱无章。所以代码:

#include <cstdio>
using namespace std;

namespace {

struct x {
    //int dummy[1024];
    x() { printf("original x address %p\n", this); }
};

__attribute__((noinline)) x make_x() {
    return x();
}

} // namespace

int main() {
    auto x1 = make_x();
    printf("copy  of x address %p\n", &x1);
}

我和我的一个同事分析了生成的汇编代码,因为我对 gcc 生成的汇编的理解非常有限。今天晚些时候,我使用带有-S -emit-llvm 标志的clang 来生成LLVM assembly,我个人发现它比X86 Assembly/GAS Syntax 更好、更容易阅读。用哪个编译器无所谓,结论都是一样的。

我用 C++ 重写了生成的程序集,如果x 为空,它大致是这样的:

#include <cstdio>
using namespace std;

struct x { };

void make_x() {
    x tmp;
    printf("original x address %p\n", &tmp);
}

int main() {
    x x1;
    make_x();
    printf("copy  of x address %p\n", &x1);
}

如果x 很大(int dummy[1024]; 成员未注释):

#include <cstdio>
using namespace std;

struct x { int dummy[1024]; };

void make_x(x* x1) {

    printf("original x address %p\n", x1);
}

int main() {
    x x1;
    make_x(&x1);
    printf("copy  of x address %p\n", &x1);
}

事实证明,make_x() 只需要在对象为空时打印一些有效的、唯一的地址。如果对象为空,make_x() 有权打印一些指向其自己的堆栈的有效地址。也没什么可复制的,make_x()的也没什么可回的。

如果您使对象更大(例如添加 int dummy[1024]; 成员),它会在适当的位置构造,因此 RVO 会启动,并且只有对象的地址会传递给 make_x() 以进行打印。没有对象被复制,没有任何东西被移动。

如果对象为空,编译器可以决定不将地址传递给make_x()(这会浪费多少资源?:))但让make_x() 自己组成一个唯一的、有效的地址堆。当这种优化发生时有些模糊且难以推理(这就是您在y 中看到的),但这并不重要。

RVO 似乎在重要的情况下始终如一地发生。而且,正如我之前的困惑所表明的那样,即使是整个 make_x() 函数也可以内联,因此一开始就没有需要优化的返回值。

【讨论】:

  • 好点。在我的测试代码中,我什至看不到成功的 RVO 和成功的内联之间的区别。奇怪地使用编译器选项 -finline-small-functions 也不会省略副本。 make_x 函数应该足够小。
  • 在这种情况下,我认为最好先将此问题标记为comiler-error,以吸引编译器编写向导并查看他们的建议。我现在就这样做。
  • 您能否引用标准指出外部链接函数在某些地方不能内联而在其他地方作为函数调用的地方?
  • @MarkB 不幸的是,不,您可能是正确的。我会修改这个答案。今天,以更新鲜的眼光,我看到我需要进一步检查这个问题。很抱歉误报。
  • @Ali,回答了我的问题,提供了额外的见解并展示了一些技巧 8-)
【解决方案2】:
  1. 我不相信有任何方法可以做出这样的保证。 RVO 是一种优化,因此编译器可能会在特定情况下确定使用它实际上是一种反优化并选择不这样做。

  2. 我假设您指的是您的第一个代码 sn-p。在 32 位编译中,即使根本没有启用优化,我也无法在 g++ 4.4、4.5 或 4.8(通过ideone.com)上重现您的断言。在 64 位编译中,我可以重现您的无 RVO 行为。这闻起来像 g++ 中的 64 位代码生成错误。

  3. 如果事实上我在(2) 中观察到的是一个错误,那么一旦修复了错误,它就会正常工作。

  4. 我可以确认,即使在 32 位编译中,Sun CC 也不会 RVO 您的具体示例。

但我确实想知道您打印地址的自省代码是否会导致编译器禁止优化(例如,它可能需要禁止优化以防止可能出现的别名问题)。

【讨论】:

    【解决方案3】:

    为什么编译器编写者选择在用户定义的复制构造函数就位时启用 RVO,但在仅存在默认复制构造函数时不选择启用 RVO?

    因为标准是这样说的:

    C++14、12.8/31:

    当满足某些条件时,允许实现省略类对象的复制/移动构造,即使为复制/移动操作选择了构造函数和/或为对象有副作用。

    C++14、12.8/32

    当满足或将满足省略复制操作的条件时,除了源对象是函数参数的事实,并且要复制的对象由左值指定时,重载决策选择构造函数首先执行复制,就好像对象由右值指定一样。如果重载决议失败,或者如果所选构造函数的第一个参数的类型不是对对象类型的右值引用(可能是 cv 限定的),则再次执行重载决议,将对象视为左值。 [ 注意:无论是否发生复制省略,都必须执行此两阶段重载解决方案。它确定如果不执行省略则要调用的构造函数,并且即使调用被省略,所选构造函数也必须是可访问的。 ——尾注]

    您必须记住,RVO(和其他复制省略)是可选的。

    想象一下,由于 RVO 启动,删除了复制/移动构造函数/赋值的代码在您的编译器上编译。然后,您将完美编译的代码移动到另一个编译器,它在法律上无法编译。这是不可接受的。

    这意味着即使编译器出于某种原因决定不进行 RVO 优化,代码也必须始终有效。

    【讨论】:

      猜你喜欢
      • 2012-10-28
      • 1970-01-01
      • 2016-05-24
      • 1970-01-01
      • 2012-08-25
      • 2015-09-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多