RVO和NRVO介绍
前言
半年前就想写一篇关于RVO
和NRVO
的介绍,但碍于没什么时间去写博客。在跟身边人进行学术探讨的时候,会发现部分人可能尝到了编译器给它做返回值优化的好处,知道这段代码被优化了,但为什么 如何去做,却不知道。
因此,为了尝试说明RVO
和NRVO
的好处、使用场景、局限性等,在看了数个StackOverflow的讨论之后,有了这篇博客。
RVO(返回值优化(Return Value Optimization))
- 如果函数返回一个无名的临时对象,该对象将被编译器移动或拷贝到目标中,在此时,编译器可以优化代码以减少对象的构造,即省略拷贝或移动。
T f() {
return T();
}
f(); // 只有一次对T的默认构造函数的调用
NRVO(命名返回值优化(Named Return Value Optimization))
- 命名返回值优化(指的是省略了命名对象的拷贝这一事实)
- 如果函数按值返回一个类的类型,并且返回语句的表达式是一个具有自动存储期限的非易失性对象的名称(即不是一个函数参数),那么可以省略优化编译器所要进行的拷贝/移动。如果是这样的话,返回值就会直接构建在函数的返回值所要移动或拷贝的存储器(即被拷贝省略优化掉的拷贝或移动的存储器)中。
注意,NRVO
不会应用于函数参数;它仅适用不是函数参数的局部变量,拿以下代码进行分析
#include <stdio.h>
class Data {
public:
Data() { printf("I am in constructor\n"); }
Data(const Data &data) { printf("I am in copy constructor\n"); }
~Data() { printf("I am in destructor\n"); }
int mem_var;
};
Data process(int i) {
Data data;
data.mem_var = i;
return data;
}
Data process1(Data data){
return data;
}
int main() {
printf("process\n");
Data data;
data = process(5);
printf("process1\n");
Data data2;
data = process1(data2);
}
其结果(MSVC Debug
模式下)是
process
I am in constructor
I am in constructor
I am in copy constructor
I am in destructor
I am in destructor
process1
I am in constructor
I am in copy constructor
I am in copy constructor
I am in destructor
I am in destructor
I am in destructor
I am in destructor
其中最为核心的代码部分
Data process(int i) {
Data data; // 2 constructor
data.mem_var = i;
return data; // 3 copy constructor
}
int main() {
Data data; // 1 constructor
data = process(5); // 5 destructor
}
process
函数内部在编译器看来,可能是以下的行为过程
Data process(Data &_hiddenArg, int i){
Data data;
data.Data::Data(); // constructor for data
data.mem_var = i;
_hiddenArg.Data::Data(data); // copy constructor for data
return;
data.Data::~Data(); // destructor for data
}
{
Data data; // constructor for data
data = process(5);
// destructor for temp val
// destructor for data
}
在经过NRVO
优化后,代码如下,基本思想是消除临时基于堆栈的值并使用_hiddenArg
(隐藏参数)。因此,这将消除基于堆栈的值的拷贝构造函数和析构函数
Data process(Data &_hiddenArg, int i){
_hiddenArg.Data::Data();
_hiddenArg.mem_var = i;
Return
}
其结果(MSVC Release
模式)是
process
I am in constructor
I am in constructor
I am in destructor
process1
I am in constructor
I am in copy constructor
I am in copy constructor
I am in destructor
I am in destructor
I am in destructor
I am in destructor
没有启用NRVO
,在process
函数中,构造了三次,析构了三次,有NRVO
之后,拷贝构造函数操作就被优化省略掉了,可以看到优化是非常明显的
例子来自微软:Named Return Value Optimization in Visual C++ 2005
关于NRVO的限制
函数return不同的命名对象
Data MyData(int i)
{
Data data;
data.mem_var = i;
if (data.mem_var == 10)
return (Data());
return rvo;
}
其开不开NRVO
结果都是
I am in constructor
I am in constructor
I am in copy constructor
当把所有返回路径改为data
时,优化将消除拷贝构造函数
Data MyData(int i)
{
Data data;
data.mem_var = i;
if (data.mem_var == 10)
return (rvo);
return rvo;
}
其结果是
I am in constructor
I am in constructor
Tips
若Data里有析构函数,则无法优化。拥有多个返回路径并引入析构函数会在函数中创建EH(Exception Handling)
状态
NRVO的副作用
因为开NRVO的优化在某些情况下可以消除拷贝构造函数,但如果你的代码依赖于拷贝构造函数的副作用,那么你的代码就写的很烂。你编写的拷贝构造函数应该保证这样的优化时安全的(源自StackOverflow)。
最后的最后再附上一个cppreference
的example
把
#include <iostream>
struct Noisy {
Noisy() { std::cout << "constructed at " << this << '\n'; }
Noisy(const Noisy&) { std::cout << "copy-constructed\n"; }
Noisy(Noisy&&) { std::cout << "move-constructed\n"; }
~Noisy() { std::cout << "destructed at " << this << '\n'; }
};
Noisy f() {
Noisy v = Noisy(); // copy elision when initializing v
// from a temporary (until C++17)
// from a prvalue (since C++17)
return v; // NRVO from v to the result object (not guaranteed, even in C++17)
} // if optimization is disabled, the move constructor is called
void g(Noisy arg) {
std::cout << "&arg = " << &arg << '\n';
}
int main() {
Noisy v = f(); // copy elision in initialization of v
// from the temporary returned by f() (until C++17)
// from the prvalue f() (since C++17)
std::cout << "&v = " << &v << '\n';
g(f()); // copy elision in initialization of the parameter of g()
// from the temporary returned by f() (until C++17)
// from the prvalue f() (since C++17)
}
其关闭NRVO
可能的结果是
constructed at 008FF793
move-constructed
destructed at 008FF793
&v = 008FF7C7
constructed at 008FF78F
move-constructed
destructed at 008FF78F
&arg = 008FF7AC
destructed at 008FF7AC
destructed at 008FF7C7
开了之后是(c++11后,会尝试把局部变量通过Move-constructor
出去(如未显式定义(将Noisy(Noisy&&
那行代码注释)),则此处是Copy-constructor
),NRVO
则消除了在此过程中移动构造函数和析构函数的调用)
constructed at 00DEFA9E
&v = 00DEFA9E
constructed at 00DEFA9F
&arg = 00DEFA9F
destructed at 00DEFA9F
destructed at 00DEFA9E
RVO和NRVO的一些讨论思考
在c++17
之前,编译器允许使用RVO
或NRVO
进行优化,但这并不保证。从c++17
开始,copy elision
得到保证,强制使用RVO,但并不强制使用NRVO
gcc下提供标志-fno-elide-constructors
,意味着你可以通过使用编译器标志来关闭NRVO
// 可关闭NRVO,RVO无法关闭 强制存在
g++ -std=c++17 -fno-elide-constructors main.cpp
RVO
总是可以应用的,不能普遍应用的是NRVO
。为了进行优化,编译器必须知道在构造对象的地方将返回什么对象。
- 在
RVO
的情况下(返回一个临时值),该条件很容易满足:对象是在return语句中构造的。(从上文RVO
较NRVO
篇幅短很多便可知) - 在
NRVO
的情况下,必须分析代码以了解编译器是否可以知道该信息。例如在函数返回对象前,抛出异常,可能会使得NRVO
没有效果
RVO和NRVO的区别
RVO
在函数返回临时对象时生效,NRVO
适用于函数返回命名对象时生效
// RVO
Data DefaultData(){
return Data(); // return temporary
}
Data data = DefaultData();
// NRVO
Data DefaultData(){
Data data;
data.mem_val = 10;
return data; // return named object
}
Data data = DefaultData();