失败时RVO强制编译错误

时间:2013-10-09 02:27:59

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

这里有很多关于何时可以完成RVO的讨论,但关于何时实际完成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

3 个答案:

答案 0 :(得分:5)

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

首先,我禁用了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()函数也可以内联,因此首先没有返回值进行优化。

答案 1 :(得分:2)

  1. 我不相信有任何方法可以作出这样的保证。 RVO是优化,因此编译器可能会在特定情况下确定使用它实际上是一种去优化,并选择不这样做。

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

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

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

  5. 我确实想知道,如果打印出地址的内省代码以某种方式导致编译器禁止优化(例如,它可能需要禁止优化以防止可能的别名问题)。

答案 2 :(得分:0)

  

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

因为标准是这样说的:

C ++ 14,12.8 / 31:

  

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

C ++ 14,12.8 / 32

  

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

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

想象一下带有已删除的复制/移动构造函数/赋值的代码,这些代码会在您的编译器上编译,因为RVO会启动。然后您将完美编译代码转移到另一个编译器中,在那里它合法地无法编译。这是不可接受的。

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