编译器返回值优化
返回值优化
返回值优化 (Return Value Optimization, RVO) 是编译器一种抑制拷贝 (Copy Elision) 的优化机制,避免代码发生不必要的拷贝。特别对于返回一些局部创建的大对象来说,有助于提高性能。虽然这是编译器的行为,但是并非所有情况下,编译器都会对返回值进行优化。因此,开发者需要搞清楚何种情况,才会触发此机制。
返回值优化包括两种形式:RVO 和 NRVO (Named RVO)。
RVO:它作用于右值对象,即临时对象。例如在函数中直接创建并返回的对象。RVO 在C++11之前就有了。从c++17以及之后,RVO是编译器的标准原则。
NRVO:它作用于有名字的局部变量。例如在函数中创建的对象然后返回。NRVO是C++11标准引入的。
这两种优化技术都可以在不改变程序语义的前提下提高程序的性能,通过减少数据拷贝和临时变量的创建,可以有效地优化函数的返回值处理。
举例说明
看如下代码:
class Test {
public:
Test() { std::cout << "Test()" << std::endl; }
~Test() { std::cout << "~Test() " << std::endl; }
Test(const Test &t) { std::cout << "Test(const Test &t)" << std::endl; }
Test &operator=(const Test &t) {
std::cout << "Test &operator=(const Test &t)" << std::endl;
return *this;
}
};
Test GetTest() {
Test t;
return t;
}
int main() {
Test t = GetTest();
return 0;
}
上面的代码先不说编译器优化,单纯从代码角度分析,main从开始运行到退出main,整个过程中Test的类成员函数调用顺序应该是:
- 调用Test构造函数,生成对象;
- 调用Test拷贝构造函数,生成临时对象;
- 析构第1步生成的对象;
- 调用Test拷贝构造函数,将第2步生成的临时变量拷贝到main()函数中的局部对象t中;
- 调用Test析构函数,将第2步生成的临时对象释放;
- 调用Test析构函数,释放main()函数中的t局部对象。
好了,现在来看下运行结果:
Test()
~Test()
看这个运行结果,傻眼了吧,和上面预测的完全不一致,这里就是返回值优化起作用了。
我们可以通过编译参数-fno-elide-constructors禁用返回值优化,看下效果:
g++ -std=c++11 -fno-elide-constructors -g ./src/test6.cc -o test6
Test()
Test(const Test &t)
~Test()
Test(const Test &t)
~Test()
~Test()
这次禁用返回值优化后,运行结果和上面我们预测的完全一致了。
返回值优化失效场景
编译器并非万能,在某些场景下,返回值优化也会失效。
不同条件分支,返回不同变量
Test GetTest(bool flag) {
Test t1;
Test t2;
if (flag) {
return t1;
}
return t2;
}
int main() {
Test t = GetTest(true);
return 0;
}
返回全局变量
Test g_t;
Test GetTest() { return g_t; }
int main() {
Test t = GetTest();
return 0;
}
直接返回函数参数
Test GetTest(Test t) {
return t;
}
int main() {
Test t;
Test t1 = GetTest(t);
return 0;
}
返回成员变量
class Test1 {
public:
Test t;
};
Test GetTest() {
Test1 t1;
return t1.t;
}
int main() {
Test t = GetTest();
return 0;
}
赋值operator=
Test GetTest() { return Test(); }
int main() {
Test t;
t = GetTest();
return 0;
}
使用std::move返回
Test GetTest() {
Test t;
return std::move(t);
}
int main() {
Test t = GetTest();
return 0;
}
运行结果:
Test()
Test(Test &&t)
~Test()
~Test()
场景比较多,也很难记全,所以大道至简,最合适的还是以下面的方式来写:
bool GetTest(Test* output)
以入参的方式来操作。