返回值优化是一个很经典的问题,很多面试官也会针对返回值优化和拷贝复制和移动复制问一些问题。
当一个未命名且未绑定到任何引用的临时变量被移动或复制到一个相同的对象时,拷贝和移动构造可以被省略。当这个临时对象在被构造的时候,他会直接被构造在将要拷贝/移动到的对象。当未命名临时对象是函数返回值时,发生的省略拷贝的行为被称为RVO,”返回值优化”。
他的作用是可以优化栈上的临时对象,并且也可以减少复制的开销。
禁止返回值优化
分析
如果我们禁止了返回值优化,那么从函数中返回对象,一种实现办法是在函数调用语句前在stack frame上声明一个隐藏对象,把该对象的地址隐蔽传入被调用函数,函数的返回对象直接构造或者复制构造到该地址上。
struct BigObject {};
BigObject foo() {
BigObject ret;
// generate ret
return ret;
}
int main() {
BigObject o = foo();
}
可能产生的代码如下:
struct BigObject {};
BigObject * foo(BigObject * _hiddenAddress) {
BigObject ret = {};
// copy result into hidden object
*_hiddenAddress = ret;
return _hiddenAddress;
}
int main() {
BigObject _hidden; // create hidden object
BigObject o = *foo(&_hidden); // copy the result into d
}
这引起了BigObject对象被复制两次,也就是上图中左侧图片描述的过程。
优化之后可能会产生如下的代码:
struct BigObject {};
void f(BigObject& ret_value) {
BigObject localObj;
return ret_value.BigObject::BigObject(std::move(localObj));//显式构造
}
int main() {
BigObject o; ///这里没有使用默认构造,定义而不构造
f(&o);
}
返回的类对象直接被构造在将要拷贝/移动到的对象栈空间上,只会产生一次构造/析构,优化掉了栈上的临时对象。
下面我就使用这个代码来分别看下这个代码在11和17下的汇编,来分析具体的实现。
#include <bits/stdc++.h>
using namespace std;
static int counter; // counter to identify instances
struct Data {
int i{ 0 };
int id;
Data() : id{ ++counter } {
std::cout << "ctor " << id << "\n";
}
Data(const Data& s) : i{ s.i }, id{ ++counter } {
std::cout << "copy ctor " << id << "\n";
}
Data& operator=(const Data& data) {
i = data.i;
std::cout << "copy assign " << data.id << " to " << id << "\n";
return *this;
}
~Data() {
std::cout << "dtor " << id << "\n";
}
};
Data GetData() {
return Data{};
}
int main() {
Data d = GetData();
return 0;
}
C++11-14
在C++11到14的标准中,并没有明确的规定返回值优化必须要做,但是大部分的编译器都是执行这个优化。
使用-fno-elide-constructors
这个标志就可以了。
.LC0:
.string "ctor "
.LC1:
.string "\n"
Data::Data() [base object constructor]:
push rbp
mov rbp, rsp
push rbx
sub rsp, 24
mov QWORD PTR [rbp-24], rdi
mov rax, QWORD PTR [rbp-24]
mov DWORD PTR [rax], 0
mov eax, DWORD PTR counter[rip]
add eax, 1
mov DWORD PTR counter[rip], eax
mov edx, DWORD PTR counter[rip]
mov rax, QWORD PTR [rbp-24]
mov DWORD PTR [rax+4], edx
mov rax, QWORD PTR [rbp-24]
mov ebx, DWORD PTR [rax+4]
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
mov esi, ebx
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov esi, OFFSET FLAT:.LC1
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
nop
add rsp, 24
pop rbx
pop rbp
ret
.LC2:
.string "copy ctor "
Data::Data(Data const&) [base object constructor]:
push rbp
mov rbp, rsp
push rbx
sub rsp, 24
mov QWORD PTR [rbp-24], rdi
mov QWORD PTR [rbp-32], rsi
mov rax, QWORD PTR [rbp-32]
mov edx, DWORD PTR [rax]
mov rax, QWORD PTR [rbp-24]
mov DWORD PTR [rax], edx
mov eax, DWORD PTR counter[rip]
add eax, 1
mov DWORD PTR counter[rip], eax
mov edx, DWORD PTR counter[rip]
mov rax, QWORD PTR [rbp-24]
mov DWORD PTR [rax+4], edx
mov rax, QWORD PTR [rbp-24]
mov ebx, DWORD PTR [rax+4]
mov esi, OFFSET FLAT:.LC2
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
mov esi, ebx
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov esi, OFFSET FLAT:.LC1
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
nop
add rsp, 24
pop rbx
pop rbp
ret
.LC3:
.string "dtor "
Data::~Data() [base object destructor]:
push rbp
mov rbp, rsp
push rbx
sub rsp, 24
mov QWORD PTR [rbp-24], rdi
mov rax, QWORD PTR [rbp-24]
mov ebx, DWORD PTR [rax+4]
mov esi, OFFSET FLAT:.LC3
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
mov esi, ebx
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov esi, OFFSET FLAT:.LC1
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
nop
add rsp, 24
pop rbx
pop rbp
ret
GetData():
push rbp
mov rbp, rsp
push rbx
sub rsp, 40
mov QWORD PTR [rbp-40], rdi
lea rax, [rbp-24]
mov rdi, rax
call Data::Data() [complete object constructor]
lea rdx, [rbp-24]
mov rax, QWORD PTR [rbp-40]
mov rsi, rdx
mov rdi, rax
call Data::Data(Data const&) [complete object constructor]
lea rax, [rbp-24]
mov rdi, rax
call Data::~Data() [complete object destructor]
jmp .L8
mov rbx, rax
lea rax, [rbp-24]
mov rdi, rax
call Data::~Data() [complete object destructor]
mov rax, rbx
mov rdi, rax
call _Unwind_Resume
.L8:
mov rax, QWORD PTR [rbp-40]
add rsp, 40
pop rbx
pop rbp
ret
main:
push rbp
mov rbp, rsp
push rbx
sub rsp, 24
lea rax, [rbp-24]
mov rdi, rax
call GetData()
lea rdx, [rbp-24]
lea rax, [rbp-32]
mov rsi, rdx
mov rdi, rax
call Data::Data(Data const&) [complete object constructor]
lea rax, [rbp-24]
mov rdi, rax
call Data::~Data() [complete object destructor]
mov ebx, 0
lea rax, [rbp-32]
mov rdi, rax
call Data::~Data() [complete object destructor]
mov eax, ebx
jmp .L13
mov rbx, rax
lea rax, [rbp-24]
mov rdi, rax
call Data::~Data() [complete object destructor]
mov rax, rbx
mov rdi, rax
call _Unwind_Resume
.L13:
add rsp, 24
pop rbx
pop rbp
ret
__static_initialization_and_destruction_0(int, int):
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
cmp DWORD PTR [rbp-4], 1
jne .L16
cmp DWORD PTR [rbp-8], 65535
jne .L16
mov edi, OFFSET FLAT:std::__ioinit
call std::ios_base::Init::Init() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:std::__ioinit
mov edi, OFFSET FLAT:std::ios_base::Init::~Init() [complete object destructor]
call __cxa_atexit
.L16:
nop
leave
ret
_GLOBAL__sub_I_GetData():
push rbp
mov rbp, rsp
mov esi, 65535
mov edi, 1
call __static_initialization_and_destruction_0(int, int)
pop rbp
ret
结果
ctor 1
copy ctor 2
dtor 1
copy ctor 3
dtor 2
dtor 3
我们来分析一下,main函数在他的栈帧上分配了一块地址,把他赋值给rdi,虽然GetData
没有参数,但是还是要传递进去。
然后在函数的栈帧中我们在申请一块内存,用于构建新的对象。然后
lea rdx, [rbp-24];这个是GetData栈帧上创建的对象,地址
mov rax, QWORD PTR [rbp-40];这个是main栈帧上的那么
mov rsi, rdx;第二个参数,拷贝构造函数
mov rdi, rax;第一个参数,this
call Data::Data(Data const&) [complete object constructor];这个就非常明确了,传递进去,进行拷贝赋值
然后就是调用这个析构函数。然后ret返回main的栈帧。
main的栈帧我们再次进行拷贝初始化,最后就是析构了。
C++17
现在我们使用C++17,在C++17中,确定了标准,也就是返回值必须要优化,即使使用参数也没有用。
.LC0:
.string "ctor "
.LC1:
.string "\n"
Data::Data() [base object constructor]:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax], 0
mov eax, DWORD PTR counter[rip]
add eax, 1
mov DWORD PTR counter[rip], eax
mov edx, DWORD PTR counter[rip]
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax+4], edx
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
mov rdx, rax
mov rax, QWORD PTR [rbp-8]
mov eax, DWORD PTR [rax+4]
mov esi, eax
mov rdi, rdx
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov esi, OFFSET FLAT:.LC1
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
nop
leave
ret
.LC2:
.string "copy ctor "
Data::Data(Data const&) [base object constructor]:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov QWORD PTR [rbp-16], rsi
mov rax, QWORD PTR [rbp-16]
mov edx, DWORD PTR [rax]
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax], edx
mov eax, DWORD PTR counter[rip]
add eax, 1
mov DWORD PTR counter[rip], eax
mov edx, DWORD PTR counter[rip]
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax+4], edx
mov esi, OFFSET FLAT:.LC2
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
mov rdx, rax
mov rax, QWORD PTR [rbp-8]
mov eax, DWORD PTR [rax+4]
mov esi, eax
mov rdi, rdx
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov esi, OFFSET FLAT:.LC1
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
nop
leave
ret
.LC3:
.string "dtor "
Data::~Data() [base object destructor]:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov esi, OFFSET FLAT:.LC3
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
mov rdx, rax
mov rax, QWORD PTR [rbp-8]
mov eax, DWORD PTR [rax+4]
mov esi, eax
mov rdi, rdx
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov esi, OFFSET FLAT:.LC1
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
nop
leave
ret
GetData():
push rbp
mov rbp, rsp
push rbx
sub rsp, 40
mov QWORD PTR [rbp-40], rdi
lea rax, [rbp-24]
mov rdi, rax
call Data::Data() [complete object constructor]
lea rdx, [rbp-24]
mov rax, QWORD PTR [rbp-40]
mov rsi, rdx
mov rdi, rax
call Data::Data(Data const&) [complete object constructor]
nop
lea rax, [rbp-24]
mov rdi, rax
call Data::~Data() [complete object destructor]
jmp .L8
mov rbx, rax
lea rax, [rbp-24]
mov rdi, rax
call Data::~Data() [complete object destructor]
mov rax, rbx
mov rdi, rax
call _Unwind_Resume
.L8:
mov rax, QWORD PTR [rbp-40]
mov rbx, QWORD PTR [rbp-8]
leave
ret
main:
push rbp
mov rbp, rsp
push rbx
sub rsp, 24
lea rax, [rbp-24]
mov rdi, rax
call GetData()
lea rdx, [rbp-24]
lea rax, [rbp-32]
mov rsi, rdx
mov rdi, rax
call Data::Data(Data const&) [complete object constructor]
lea rax, [rbp-24]
mov rdi, rax
call Data::~Data() [complete object destructor]
mov ebx, 0
lea rax, [rbp-32]
mov rdi, rax
call Data::~Data() [complete object destructor]
mov eax, ebx
jmp .L13
mov rbx, rax
lea rax, [rbp-24]
mov rdi, rax
call Data::~Data() [complete object destructor]
mov rax, rbx
mov rdi, rax
call _Unwind_Resume
.L13:
mov rbx, QWORD PTR [rbp-8]
leave
ret
结果
ctor 1
dtor 1
可以很清楚的看到,只进行了一次初始化。
其余特殊的优化
所有分支返回同一个具名对象
若分支返回全是同一具名对象, 发生返回值优化. 运行代码
#include <iostream>
#include <cstddef>
#include <cstring>
#include <utility>
static int counter; // counter to identify instances of S
struct Data {
int i{ 0 };
int id;
Data() : id{ ++counter } {
std::cout << "ctor " << id << "\n";
}
Data(const Data& s) : i{ s.i }, id{ ++counter } {
std::cout << "copy ctor " << id << "\n";
}
Data& operator=(const Data& data) {
i = data.i;
std::cout << "copy assign " << data.id << " to " << id << "\n";
return *this;
}
~Data() {
std::cout << "dtor " << id << "\n";
}
};
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;
}
这个返回值全部都是d,所以可以使用优化,所以可以进行优化。
所有分支返回非同一对象
若分支返回不全是同一具名对象, 则无返回值优化. 因为返回的对象在运行时确定, 编译器无法在编译期决定.
Data GetData(int param) {
Data d; // ctor 1
if (param % 2 == 0) {
d.i = 1;
return d;
}
else if (param % 2 == 1) {
Data d2;
d2.i = 2;
return d2;
}
return d;
} // copy ctor 2, dtor 1
int main() {
Data d = GetData(0);
return 0;
} // dtor 2
函数返回结果用于赋值
如果调用函数时, 造成的是拷贝赋值, 而不是拷贝构造, 即使是不具名的情况, 也不会发生返回值优化 (注: 换个思路理解, 编译器不清楚赋值左侧的值从创建到赋值之间, 将处于何种状态, 或者进行何种操作, 所以不会对这种形式做返回值优化. 为避免这种情况的拷贝赋值, 可以通过移动赋值来消除).
Data GetData(int param) {
//!!! sub case 1
// Data d{}; // ctor 2
// return d;
//!!! sub case 2
// return Data{}; // ctor 2
//!!! sub case 3
Data d{}; // ctor 2
if (param % 2 == 0) {
d.i = 1;
return d;
} else {
d.i = 2;
return d;
}
}
int main() {
Data d; // ctor 1
d = GetData(0); // copy assign 2 to 1
return 0;
} // dtor 2, dtor1
这个也可以理解,毕竟之前都是拷贝构造函数,因为这个对象还没有初始化,我们可以把栈上的地址传递进去。
但是如果说这个对象已经初始化了,我们怎么进行初始化,所以就没有办法来原来的地址上进行初始化了,就只能重新申请地址,把东西放到这里,然后再进行拷贝。
返回一个对象的成员
struct DataWrap {
Data d;
};
Data GetData() {
return DataWrap{}.d; // ctor 1
} // copy ctor 2, dtor 1
int main() {
Data d = GetData();
return 0;
} // dtor 2
这种情况下,他其实需要生成一个匿名变量,所以这个变量必须生成,所以也就无法初始化。
返回值是全局的变量或是其他变量
Data sd{}; // ctor 1
Data GetData() {
return sd;
} // copy ctor 2, dtor 1
int main() {
Data d = GetData();
return 0;
}
对于这种嘛,也可以理解,毕竟这个对象已经生成了,只能进行拷贝赋值了。
其实上面的这些例子,当我们了解了他的原理就可以很容器的想到。我们之所以能够进行返回值优化,就是因为我们在上一个栈帧中找了一块内存,作为参数传递过去。这样才可以返回值优化,如果不能根据这样,就不能进行优化,必须需要拷贝。
还有就是cpp标准规定,return 一个表达式,如果这个表达式是一个自动生命周期变量(栈上变量),并且不是volite,那么他就是可移动的,优先使用移动构造函数。