为什么clang没有使用NRVO进行优化?

8

我正在思考一个相当不错的C++11编译器(clang)为何不能优化这段代码,并想知道这里是否有人有意见。

#include <iostream>
#define SLOW

struct A {
  A() {}
  ~A() { std::cout << "A d'tor\n"; }
  A(const A&) { std::cout << "A copy\n"; }
  A(A&&) { std::cout << "A move\n"; }
  A &operator =(A) { std::cout << "A copy assignment\n"; return *this; }
};

struct B {
  // Using move on a sink. 
  // Nice talk at Going Native 2013 by Sean Parent.
  B(A foo) : a_(std::move(foo)) {}  
  A a_;
};

A MakeA() {
  return A();
}

B MakeB() {  
 // The key bits are in here
#ifdef SLOW
  A a(MakeA());
  return B(a);
#else
  return B(MakeA());
#endif
}

int main() {
  std::cout << "Hello World!\n";
  B obj = MakeB();
  std::cout << &obj << "\n";
  return 0;
}

如果我注释掉#define SLOW并使用-s进行优化,那么我会得到:
Hello World!
A move
A d'tor
0x7fff5fbff9f0
A d'tor

预计会出现这种情况。

如果我启用#define SLOW并使用-s进行优化,则会得到以下结果:

Hello World!
A copy
A move
A d'tor
A d'tor
0x7fff5fbff9e8
A d'tor

很明显这种情况并不好。所以问题是:

为什么"慢速"案例中没有应用NRVO优化?我知道编译器不一定需要应用NRVO,但这似乎是一个非常普遍而简单的情况。

总的来说,我尝试鼓励采用"慢速"风格的代码,因为我发现这样更容易调试。


优化使用-s吗?如果Clang上的-s与GCC上的相同,我认为那不是你所需要的。-O2-O3会更合适。 - jogojapan
1
@jogojapan:尽管-s不会进行优化,但实际上并不重要,因为复制省略不是一种优化:它改变了行为,而优化不允许改变行为。 "NRVO"是一个误称。理智的编译器独立于优化设置应用复制省略。可悲的是,有一个流行的编译器会改变行为。 - Dietmar Kühl
@TonyD:说得好。我想我真的应该提出一个缺陷……标准并没有定义这个术语,也就是说,它是非正式使用的。如果一些输出由于复制省略在调试模式下出现而在发布模式下没有出现,那么这会让人感到困惑,因为编译器不会将其视为优化(我见过很多由于这个特定问题而浪费的时间)。因此,最好不要将其视为优化。 - Dietmar Kühl
1
@DietmarKühl:从我的角度来看,这无疑是一种优化,因为它可以提高性能。问题在于是否应该要求在所有优化级别(包括名义上/否则未优化的构建)中执行此优化或不执行此优化,以便行为不会发生变化。问题在于,可移植性最终需要在编译器之间强制实施或禁止优化。我个人认为,这是C++的一个角落,程序员必须承担一些责任,并且对此感到舒适。 - Tony Delroy
1
从Straustrup的定义来看,它肯定是一种优化 - 优化器 - 编译器的一部分,可以从代码中消除冗余操作并调整代码以在给定计算机上更好地执行。 - SChepurin
显示剩余4条评论
2个回答

13
简单的回答是:因为在这种情况下不允许应用拷贝省略。编译器只允许在非常少数和特定的情况下应用拷贝省略。标准中引用的语句是12.8 [class.copy]第31段:
“......这种称为拷贝省略的拷贝/移动操作,在以下情况下是允许的(可以合并以消除多个副本):”
  • 在返回类型为类的返回语句中,当表达式是与函数返回类型有相同cv限定符类型的自动对象的名称(而不是函数或catch参数),可以通过直接构造自动对象到函数的返回值中省略复制/移动操作
  • [...]
显然,B(a) 的类型不是 A,即拷贝省略不被允许。同一段落中的其他子弹点涉及像throw表达式、从临时值省略复制和异常声明等事项,但均不适用于此情况。

感谢 @DietmarKühl。我仍觉得这种情况不能被优化很奇怪。这似乎是一个非常常见的情况,并且可以优化大量的C++代码。我讨厌这些类型的模式需要基本上被记住,才能编写高效的C++代码。 - dmaclach
@dmaclach 不完全是这样。如果没有记住标准,a 在您的代码中不是返回值,因此 NRVO 没有意义。省略通常发生在创建对象的未命名临时对象时,而 a 有一个名称。因此,如果它具有副作用,则不会省略 a。隐式右值也不适用,因为 return 不是简单的 return var;(必须承认,隐式右值是您需要了解的内容)。 - Yakk - Adam Nevraumont
@Yakk:隐式 rvalue 在同一节的第32段基本上是说,隐式 rvalue 适用于与复制省略相同的情况,扩展到所有局部变量,即参数也包括在内。 - Dietmar Kühl

3
你在慢路径中看到的副本不是由于缺乏RVO引起的,而是因为在B(MakeA())中,“MakeA()”是一个右值,但在B(a)中,“a”是一个左值。
为了让这一点更清楚,让我们修改慢路径,以指示MakeA()何时完成:
#ifdef SLOW
  A a(MakeA());
  std::cout << "---- after call \n";
  return B(a);
#else

输出结果是:
Hello World!
---- after call 
A copy
A move
A d'tor
A d'tor
0x7fff5a831b28
A d'tor

这表明没有进行复制。
A a(MakeA());

因此,RVO确实发生了。
修复方法是删除所有副本:
return B(std::move(a));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接