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