RVO 在失败时强制编译错误

RVO force compilation error on failure

本文关键字:编译 错误 失败 RVO      更新时间:2023-10-16

这里有很多关于何时可以完成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这样的构造函数帮助程序就是一个例子。请参阅此示例,其中优化实际上是由标准强制执行的。

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

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

#include <cstdio>
using namespace std;
namespace {
struct x {
    //int dummy[1024];
    x() { printf("original x address %pn", this); }
};
__attribute__((noinline)) x make_x() {
    return x();
}
} // namespace
int main() {
    auto x1 = make_x();
    printf("copy  of x address %pn", &x1);
}

我和我的一位同事一起分析了生成的汇编代码,因为我对 gcc 生成的汇编的理解非常有限。今天晚些时候,我使用带有-S -emit-llvm标志的clang来生成LLVM程序集,我个人认为它比X86程序集/GAS语法更好,更容易阅读。使用哪个编译器并不重要,结论是相同的。

我在 C++ 年重写了生成的程序集,如果x为空,它大致如下所示:

#include <cstdio>
using namespace std;
struct x { };
void make_x() {
    x tmp;
    printf("original x address %pn", &tmp);
}
int main() {
    x x1;
    make_x();
    printf("copy  of x address %pn", &x1);
}

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

#include <cstdio>
using namespace std;
struct x { int dummy[1024]; };
void make_x(x* x1) {
    printf("original x address %pn", x1);
}
int main() {
    x x1;
    make_x(&x1);
    printf("copy  of x address %pn", &x1);
}

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

如果使对象变大(例如添加int dummy[1024];成员(,则会就地构造该对象,以便 RVO 启动,并且仅将对象的地址传递给要打印make_x()。没有对象被复制,任何东西都不会被移动。

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

RVO 看起来在那些重要的情况下会持续发生。而且,正如我之前的困惑所显示的那样,即使是整个make_x()函数也可以内联,因此首先没有要优化的返回值。

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

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

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

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

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

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

因为标准是这样说的:

C++14, 12.8/31:

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

C++14, 12.8/32

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

您必须记住,RVO(和其他副本省略(是可选的。

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

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