在代码优化期间,C++ 11编译器能否将局部变量转换为右值?

Mic*_*zyk 26 c++ compiler-optimization rvalue-reference visual-c++ c++11

有时将复杂或长表达式分成多个步骤是明智的(例如,第二个版本不是更清楚,但它只是一个例子):

return object1(object2(object3(x)));
Run Code Online (Sandbox Code Playgroud)

可以写成:

object3 a(x);
object2 b(a);
object1 c(b);
return c;
Run Code Online (Sandbox Code Playgroud)

假设所有3个类都实现了以rvalue作为参数的构造函数,第一个版本可能更快,因为临时对象被传递并可以移动.我假设在第二个版本中,局部变量被认为是左值.但是如果以后没有使用变量,那么C++ 11编译器是否会优化代码,因此变量被认为是rvalues,两个版本的工作方式完全相同?我最感兴趣的是Visual Studio 2013的C++编译器,但我也很高兴知道GCC编译器在这个问题上的行为.

谢谢,米哈尔

jua*_*nza 24

在这种情况下,编译器不能破坏"as-if"规则.但是你可以std::move用来达到预期的效果:

object3 a(x);
object2 b(std::move(a));
object1 c(std::move(b));
return c;
Run Code Online (Sandbox Code Playgroud)

  • @MichałFronczyk为了将左值作为rvalues处理,编译器需要选择一个不同的重载(rvalue ref与lvalue ref),这将要求它知道在选择一个或另一个时没有可观察到的差异.这听起来很复杂,而且b)不太可能(但并非不可能). (4认同)
  • @juanchopanza:我希望编译器最好内联lvalue构造函数然后优化,忽略rvalue构造函数的存在.假设优化器确实关注了rvalue ctors - 要么它能够证明它们与这个代码是等价的(在这种情况下,它原则上可以产生与lvalue ctor中的rvalue构造函数一样好的代码而不需要rvalue ctor)或它不能(在这种情况下,它必须按照标准使用左值构造函数).因此,我认为rvalue构造函数的存在是无关紧要的. (4认同)
  • @juanchopanza:问题告诉我有一个技巧,那就是如果rvalue ctor存在并且编译器可以证明它对于这个代码是等价的,那么它可能是一个很好的策略来假设它是高效,因此更愿意使用它而不是优化器可能发明的其他东西,作为所有进一步优化步骤的基础.我完全同意你的意见,但这听起来不太可能.优化器通常*不关心程序员的意见什么是有效的;-) (2认同)

Mat*_* M. 14

正如juanchopanza所说,编译器不能(在C++级别)违反"as-if"规则; 这就是所有转换应该产生一个语义上等效的代码.

但是,超出C++级别,当代码优化时,可能会出现更多机会.

因此,它实际上取决于对象本身:如果move-constructors/destructors有副作用,并且(de)分配内存是副作用,那么优化就不会发生.如果仅使用POD,使用默认的move-constructors/destructors,那么它可能会自动优化.


Ali*_*Ali 9

但是如果以后没有使用变量,那么C++ 11编译器是否会优化代码,因此变量被认为是rvalues,两个版本的工作方式完全相同?

这是可能的,但它很大程度上取决于您的类型.请考虑以下带有POD类型的示例point:

#include <cstdio>

struct point {
  int x;
  int y;
};

static point translate(point p, int dx, int dy) {
  return { p.x + dx, p.y + dy };
}

static point mirror(point p) {
  return { -p.x, -p.y };
}

static point make_point(int x, int y) {
  return { x, y };
}

int main() {
  point a = make_point(1, 2);
  point b = translate(a, 3, 3);
  point c = mirror(b);

  std::printf("(x,y) = (%d,%d)\n", c.x, c.y);
}
Run Code Online (Sandbox Code Playgroud)

我查看了汇编代码,这里是整个程序(!)基本编译成的内容(所以下面的代码是生成的汇编代码的C近似值):

int main() {
  std::printf("(x,y) = (-4,-5)\n");
}
Run Code Online (Sandbox Code Playgroud)

它不仅摆脱了所有的局部变量,而且还在编译时进行了计算!我尝试过gcc和clang但不是msvc.

好吧,让我们让程序变得更复杂,以便它不能进行计算:

int main(int argc, char* argv[]) {

  int x = *argv[1]-'0';
  int y = *argv[2]-'0';
  point a = make_point(x,y);
  point b = translate(a, 3, 3);
  point c = mirror(b);

  std::printf("(x,y) = (%d,%d)\n", c.x, c.y);
}
Run Code Online (Sandbox Code Playgroud)

要运行此代码,您必须将其称为./a.out 1 2.

整个程序在优化后简化为此程序(在C中重写的程序集):

int main(int argc, char* argv[]) {
  int x = *argv[1]-'0';
  int y = *argv[2]-'0';
  std::printf("(x,y) = (%d,%d)\n", -(x+3), -(y+3));
}
Run Code Online (Sandbox Code Playgroud)

所以它摆脱了a, b, c所有的功能make_point(),translate()并且mirror()在编译时尽可能多地完成了计算.

由于Matthieu M.的回答中提到的原因,不要期望在更复杂的类型(特别是非POD)中进行如此好的优化.

根据我的经验,内联是至关重要的.努力工作,以便您的功能可以轻松内联.使用链接时优化.


mar*_*inj 7

请注意,除了可以大大加速代码的移动语义之外,编译器还在执行(N)RVO - (命名)返回值优化,这实际上可以为代码提供更高的效率.我已经测试了你的例子,在g ++ 4.8中看来你的第二个例子实际上可能是更优的:

object3 a(x);
object2 b(a);
object1 c(b);
return c;
Run Code Online (Sandbox Code Playgroud)

从我的实验看起来它会调用构造函数/析构函数8次(1 ctr + 2 copy ctrs + 1 move ctr + 4 dtrs),相比之下调用它10次的其他方法(1 ctr + 4 move ctors + 5 dtors) .但是正如user2079303所评论的那样,移动构造函数仍然应该优于复制构造函数,在这个例子中,所有调用都将被内联,因此不会发生函数调用开销.

复制/移动省略实际上是"as-if"规则的一个例外,这意味着有时你可能会惊讶你的构造函数/析构函数甚至没有副作用的东西也不会被调用.

http://coliru.stacked-crooked.com/a/1ca7ebec0567e48f

(您可以使用-fno-elide-constructors参数禁用(N)RVO)

#include <iostream>
#include <memory>

template<int S>
struct A {
    A() { std::cout<<"A::A"<<std::endl; }    
    template<int S2>
    A(const A<S2>&) { std::cout<<"A::A&"<<std::endl; }
    template<int S2>
    A(const A<S2>&&) { std::cout<<"A::A&&"<<std::endl; }    
    ~A() { std::cout<<"~A::A"<<std::endl;}        
};
A<0> foo () {    
    A<2> a; A<1> b(a); A<0> c(b); return c;   // calls dtor/ctor 8 times
    //return A<0>(A<1>(A<2>()));  // calls dtor/ctor 10 times
}
int main()
{
   A<0> a=foo();
   return 0;
}
Run Code Online (Sandbox Code Playgroud)