C++ 返回值优化RVO

一、原理

C++的返回值优化(Return Value Optimization,也被称为RVO)是一个重要的优化技术,可以提高程序的性能。返回值优化(RVO)是指编译器对临时变量的处理机制。对于函数返回一个对象的情况,编译器在生成代码时会按照以下规则进行优化:

1.如果返回值是一个非静态局部变量,并且该变量是通过复制返回(Return by Value)方式返回的,编译器会尝试将该返回值直接构造在调用方的返回值处,而不是返回一个副本。

2.如果返回值是一个匿名临时变量,编译器会优化掉这个临时变量的构造和析构函数,直接将其值传递给函数返回值的目标对象。

其中,第一种情况的优化被称为NRVO(Named Return Value Optimization,命名返回值优化或者具名返回值优化)

RVO的核心思想是:在函数内部创建的对象,在函数调用结束后会立即被销毁,这些临时对象的生命周期比较短暂,因此可以直接将它们构造在调用方的返回值处,避免产生额外的开销。

二、示例分析

static int counter = 0; // counter to identify instances
class Data {
 public:
  Data() : id(++counter) {
    std::cout << "ctor " << id << std::endl;
  }
  Data(const Data& other) : id(++counter) {
    std::cout << "copy ctor " << id << std::endl;
  }
  Data& operator=(const Data& other) {
    std::cout << "copy assign " << other.id << " to " << id << std::endl;
    return *this;
  }
  ~Data() {
    std::cout << "dtor " << id << std::endl;
  }
  int i = 0;
 private:
  int id = 0;
};

2.1、不具名返回值优化

不具名返回值优化发生在返回一个无名对象或者临时对象, 一般是 Return 语句中直接创建并返回的对象。由于编译器默认开启该优化选项, 因此需要在编译时关闭该选项,使用-fno-elide-constructors 该编译选项进行关闭编译优化。

Data GetData() {
  return Data{};
}

int main() {
  Data d = GetData();    
  return 0;
}

无优化运行结果

ctor 1
copy ctor 2
dtor 1
copy ctor 3
dtor 2
dtor 3

优化运行结果

ctor 1
dtor 1

可以看出, 在 RVO 时, 对象只被构造了一次,而未 RVO 时, 对象则发生了多次拷贝。

2.2、具名返回值优化 RVO

具名返回值优化一般发生在返回一个已经创建的对象。

Data GetData() {
  Data d;
  d.i = 10;
  return d;
}

int main() {
  Data d = GetData();    
  return 0;
}

无优化运行结果

ctor 1
copy ctor 2
dtor 1
copy ctor 3
dtor 2
dtor 3

优化运行结果

ctor 1
dtor 1

结果和不具名返回值优化一样。

三、返回值优化的特殊情况

对于存在分支的函数, 若所有分支都返回同一个具名对象, 才会开启返回值优化。

3.1、所有分支返回同一具名对象(有优化)

若分支返回全是同一具名对象, 发生返回值优化

Data GetData(int param) {
  Data d;                    
  if (param % 2 == 0) {
    d.i = 1;
    return d;
  }
  else if (param % 2 == 1) {
    d.i = 2;
    return d;
  }
  return d;
}

int main() {
  Data d = GetData(0);    
  return 0;
} 

优化运行结果

ctor 1
dtor 1

3.2、所有分支返回非同一对象(无优化)

若分支返回不全是同一具名对象, 则无返回值优化。因为返回的对象在运行时确定, 编译器无法在编译期决定。

Data GetData(int param) {
  Data d;                    
  if (param % 2 == 0) {
    d.i = 1;
    return d;
  }
  else if (param % 2 == 1) {
    Data d2;
    d2.i = 2;
    return d2;
  }
  return d;
}                              

int main() {
  Data d = GetData(0);    
  return 0;
}   

运行结果

ctor 1
copy ctor 2
dtor 1
dtor 2

3.3、函数返回结果用于赋值(无优化)

需要注意的另一种情况是, 如果调用函数时, 造成的是拷贝赋值, 而不是拷贝构造, 即使是不具名的情况, 也不会发生返回值优化 (注: 换个思路理解, 编译器不清楚赋值左侧的值从创建到赋值之间, 将处于何种状态, 或者进行何种操作, 所以不会对这种形式做返回值优化. 为避免这种情况的拷贝赋值, 可以通过移动赋值来消除)。

Data GetData(int param) {
  Data d;                    
  if (param % 2 == 0) {
    d.i = 1;
    return d;
  }
  else if (param % 2 == 1) {
    d.i = 2;
    return d;
  }
  return d;
}

int main() {
  Data d;
  d = GetData(0);    
  return 0;
} 

运行结果

ctor 1
ctor 2
copy assign 2 to 1
dtor 2
dtor 1

3.4、函数返回成员对象 (无优化)

函数返回的是局部对象的成员变量, 也无法作用返回值优化, 即使是匿名变量。

struct DataWrap {
  Data d;
};

Data GetData() {
  DataWrap dw;
  return dw.d;        
}                               

int main() {
  Data d = GetData();    
  return 0;
}  

运行结果

ctor 1
copy ctor 2
dtor 1
dtor 2

3.5、函数返回参数或者全局变量 (无优化)

函数返回的是输入参数或者全局变量, 也无返回值优化。

Data GetData(Data d) {
  return d;        
}                              

int main() {
  Data d1;                  
  Data d2 = GetData(d1);      
  return 0;
}

运行结果

ctor 1
copy ctor 2
copy ctor 3
dtor 2
dtor 3
dtor 1

3.6、由 std::move 返回 (无优化)

通过显式调用 std::move 返回函数结果往往是错误的,即使如此, 这试图使对象显式调用移动构造函数, 导致返回值优化被抑制。

Data GetData() {
  Data d;                 
  return std::move(d);                  
}                           

int main() {
  Data d = GetData();    
  return 0;
}   

运行结果

ctor 1
copy ctor 2
dtor 1
dtor 2

四、参考资料

https://juejin.cn/post/7145704295353516063

https://21xrx.com/Articles/read_article/85173