【问题标题】:Why is clang not optimizing this with NRVO?为什么 clang 不使用 NRVO 优化这个?
【发布时间】:2013-12-18 04:58:11
【问题描述】:

我试图解释为什么一个相当好的 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

这显然不是那么好。所以问题是:

为什么我没有看到在“SLOW”情况下应用了 NRVO 优化?我知道编译器不需要应用 NRVO,但这似乎是一个很常见的简单案例。

一般来说,我尝试鼓励使用“SLOW”风格的代码,因为我发现它更容易调试。

【问题讨论】:

  • 优化-s?如果 Clang 上的 -s 与 GCC 上的功能相同,我认为这不是您需要的。 -O2-O3 是合适的。
  • @jogojapan:虽然-s 没有优化,但实际上并不重要,因为复制省略不是一种优化:它会改变行为而不允许优化改变行为。 “NRVO”是用词不当。 Sane 编译器独立于优化设置应用复制省略。可悲的是,有一种流行的编译器会改变行为。
  • @DietmarKühl 我明白了;我没有意识到这一点。谢谢!
  • @DietmarKühl:从我的角度来看,这无疑是一种优化,因为它提高了性能,问题是是否需要在所有优化级别(包括名义上/其他方面未优化的任何内容)执行此优化build) 或 none,因此行为不会改变。问题在于可移植性最终需要强制或禁止优化跨编译器。我个人认为这是 C++ 的一个角落,程序员必须承担一些责任并对此感到满意。
  • 根据 Straustrup 的定义,它确实是一种优化 - 优化器 - 编译器的一部分,消除代码中的冗余操作并调整代码以执行在给定的计算机上更好。

标签: c++ optimization c++11 nrvo


【解决方案1】:

简单的答案是:因为在这种情况下不允许应用复制省略。编译器只允许在极少数和特定情况下应用复制省略。标准中的引用是 12.8 [class.copy] 第 31 段:

...这种复制/移动操作的省略,称为复制省略,在以下情况下是允许的(可以结合起来消除多个副本):

  • 在具有类返回类型的函数的 return 语句中,当表达式是具有与函数返回相同的 cv 非限定类型的非易失性自动对象(函数或 catch 子句参数除外)的名称时类型,可以通过将自动对象直接构造到函数的返回值中来省略复制/移动操作
  • [...]

显然B(a) 的类型不是A,即不允许复制省略。同一段落中的其他项目符号指的是 throw 表达式、临时删除副本和异常声明等内容。这些都不适用。

【讨论】:

  • 谢谢@DietmarKühl。对我来说仍然很奇怪,不允许优化这种情况。这似乎是一个非常常见的情况,并且会优化一大块 C++ 代码。我讨厌这些类型的模式需要基本上记住才能编写高效的 C++ 代码。
  • @dmaclach 不是真的。如果不记住标准,a 不是代码中的返回值,因此 NRVO 没有任何意义。当您有一个未命名的临时创建对象并且a 有一个名称时,通常会发生省略。所以a 如果有副作用就不会被忽略。隐式右值也不适用,因为return 并不简单return var;(诚然,隐式右值是你需要了解的东西)
  • @Yakk:隐式右值是同一节的第 32 段,基本上说隐式右值适用于与复制省略扩展到所有局部变量相同的情况,即,还包括参数。
【解决方案2】:

您在慢速路径中看到的副本不是由于缺少 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));

【讨论】:

    猜你喜欢
    • 2017-03-05
    • 2014-07-13
    • 2013-06-16
    • 2018-11-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多