深度解读《深度探索C++对象模型》之返回值优化
接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,自动获得推文和全部的文章列表。
没有启用返回值优化时,怎么从函数内部返回对象
当在函数的内部中返回一个局部的类对象时,是怎么返回对象的值的?请看下面的代码片段:
class Object {}
Object foo() {
Object b;
// ...
return b;
}
Object a = foo();
对于上面的代码,是否一定会从foo函数中拷贝对象到对象a中,如果Object类中定义了拷贝构造函数的话,拷贝构造函数是否一定会被调用?答案是要看Object类的定义和编译器的实现策略有关。我们细化一下代码来进一步分析具体的表现行为,请看下面的代码:
#include <cstdio>
class Object {
public:
Object() {
printf("Default constructor\n");
a = b = c = d = 0;
}
int a;
int b;
int c;
int d;
};
Object foo() {
Object p;
p.a = 1;
p.b = 2;
p.c = 3;
p.d = 4;
return p;
}
int main() {
Object obj = foo();
printf("%d, %d, %d, %d\n", obj.a, obj.b, obj.c, obj.d);
return 0;
}
编译成对应的汇编代码,看一下是怎么从foo函数中返回一个对象的,下面节选main和foo函数的汇编代码:
foo(): # @foo()
push rbp
mov rbp, rsp
sub rsp, 16
lea rdi, [rbp - 16]
call Object::Object() [base object constructor]
mov dword ptr [rbp - 16], 1
mov dword ptr [rbp - 12], 2
mov dword ptr [rbp - 8], 3
mov dword ptr [rbp - 4], 4
mov rax, qword ptr [rbp - 16]
mov rdx, qword ptr [rbp - 8]
add rsp, 16
pop rbp
ret
main: # @main
push rbp
mov rbp, rsp
sub rsp, 32
mov dword ptr [rbp - 4], 0
call foo()
mov qword ptr [rbp - 24], rax
mov qword ptr [rbp - 16], rdx
mov esi, dword ptr [rbp - 24]
mov edx, dword ptr [rbp - 20]
mov ecx, dword ptr [rbp - 16]
mov r8d, dword ptr [rbp - 12]
lea rdi, [rip + .L.str]
mov al, 0
call printf@PLT
xor eax, eax
add rsp, 32
pop rbp
ret
从汇编代码中看到,在foo函数内部构造了一个Object类的对象(第5、6行),然后对它的成员进行赋值(第7行到第10行),最后通过将对象的值拷贝到rax和rdx寄存器中作为返回值返回(第11、12行)。在main函数中的第22、23代码,将返回值从rax和rdx寄存器中拷贝到栈空间中,这里没有构造对象,直接采用拷贝的方式拷贝内容,可见在这种情况下编译器是直接拷贝对象内容的方式来返回一个局部对象的。
启用返回值优化的条件和编译器的实现分析
如果Object类中有定义了一个拷贝构造函数,在这种情况下表现行为又是怎样的?在上面从C++代码中加入拷贝构造函数:
Object(const Object& rhs) {
printf("Copy constructor\n");
memcpy(this, &rhs, sizeof(Object));
}
编译运行,输出结果如下:
Default constructor
1, 2, 3, 4
神奇的是拷贝构造函数被没有如预期地被调用,甚至查看汇编代码都没有生成拷贝构造函数的代码(因为没有调用,编译器优化掉了)。我们再来看看foo和main函数的汇编代码,看看和上面的汇编代码有什么区别。
foo(): # @foo()
push rbp
mov rbp, rsp
sub rsp, 32
mov qword ptr [rbp - 24], rdi # 8-byte Spill
mov rax, rdi
mov qword ptr [rbp - 16], rax # 8-byte Spill
mov qword ptr [rbp - 8], rdi
call Object::Object() [base object constructor]
mov rdi, qword ptr [rbp - 24] # 8-byte Reload
mov rax, qword ptr [rbp - 16] # 8-byte Reload
mov dword ptr [rdi], 1
mov dword ptr [rdi + 4], 2
mov dword ptr [rdi + 8], 3
mov dword ptr [rdi + 12], 4
add rsp, 32
pop rbp
ret
main: # @main
push rbp
mov rbp, rsp
sub rsp, 32
mov dword ptr [rbp - 4], 0
lea rdi, [rbp - 24]
call foo()
mov esi, dword ptr [rbp - 24]
mov edx, dword ptr [rbp - 20]
mov ecx, dword ptr [rbp - 16]
mov r8d, dword ptr [rbp - 12]
lea rdi, [rip + .L.str]
mov al, 0
call printf@PLT
xor eax, eax
add rsp, 32
pop rbp
ret
从汇编代码中看到,foo函数内部中不再构造一个局部对象然后初始化后再将这个对象拷贝返回,而是传递了一个对象的地址给foo函数(第24、25行),foo函数对传递过来的这个对象进行构造(第5到第9行),然后对对象的成员进行赋值(第12到15行),foo函数结束之后,在main函数中就可以直接使用这个被构造和赋值后的对象了,第26到29行就是取各成员的值然后调用printf函数打印出来。也就是说原先的代码被编译器改写了,如下面的伪代码所示:
Object obj = foo();
// 将被改成:
Object obj; // 这里不需要调用默认构造函数
foo(obj);
// 相应地foo函数将被改写定义:
void foo(Object& obj) {
obj.Object::Object(); // 调用Object的默认构造函数
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
return;
}
看起来像是拷贝构造函数的加入激活了编译器NRV(Named Return Value)优化,为什么有拷贝构造函数的存在就会触发NRV优化呢?原因就是既然程序中定义了拷贝构造函数,根据我们之前的分析,说明是要处理拷贝大块的内存空间等之类的操作,不仅仅是普通的数据成员的拷贝,如果只是拷贝数据成员可以不必定义拷贝构造函数,编译器会采用更高效的逐成员拷贝的方法,编译器内部就可以帮程序员做好了,所以有拷贝构造函数的存在就说明有需要低效的拷贝动作,那么就要想办法消除掉拷贝的操作,那么启用NRV优化就是一项提高效率的做法了。
那么是不是只有存在拷贝构造函数编译器才会启用NRV优化呢?我们继续来修改代码,类中加入一个大数组,同时把拷贝构造函数去掉:
class Object {
public:
Object() {
printf("Default constructor\n");
a = b = c = d = 0;
}
int a;
int b;
int c;
int d;
int buf[100];
};
这样修改之后的汇编代码跟之前的基本一样(汇编代码跟上面基本一样就没贴了),有区别的地方就是对象占用的内存空间变大了,这说明没有定义拷贝构造函数的情况下编译器也有可能启用了NRV优化,在对象占用的内存空间较大的时候,这时不再适合使用寄存器来传送对象的内容了,如果采用栈空间来返回结果的话,会涉及到内存的拷贝,效率较低,所以启用NRV优化则有效率上的提升。
启用返回值优化后的效率提升
那么启用NRV优化与不启用优化,两者之间的效率对比究竟差了多少?我们还是以上面的例子来测试,默认情况下编译器是开启了这个优化的,如果想要禁用这个优化,可以在编译时加入-fno-elide-constructors选项关闭它。为了不影响效率,把打印都去掉,在main函数中加入时间计时,下面是完整的代码:
#include <cstdio>
#include<chrono>
using namespace std::chrono;
class Object {
public:
Object() {}
int a;
int b;
int c;
int d;
int buf[100];
};
Object foo(int i) {
Object p;
p.a = 1;
p.b = 2;
p.c = 3;
p.d = 4;
p.buf[0] = i;
p.buf[99] = i;
return p;
}
int main() {
auto start = system_clock::now();
for (auto i = 0; i < 10000000; ++i) {
Object a = foo(i);
}
auto end = system_clock::now();
auto duration = duration_cast<milliseconds>(end-start);
printf("spend %lldms\n", duration.count());
return 0;
}
下面是在我的Apple M1机器上的测试结果,每种情况都是取测试10次然后取平均值。
| 启用NRV优化 | 未启用NRV优化 |
|---|---|
| 56.3ms | 186.7 |
未优化的时间多花了130.4ms,时间上是启用优化后的时间的3倍多。
返回值优化的缺点
从测试结果来看,NRV优化看起来很美好,那么NRV优化是否一切都完美无缺呢?其实NRV优化也存在一些不足或者说不尽如人意的地方:
- 是否开启了NRV优化的问题,NRV优化并不是C++标准中规定的东西,各家编译器的实现未必一定支持它,或者说启用它的条件和规则也不尽相同,例如clang或者g++,像我上面提到的那两种情况下就会开启优化,微软的Visual Studio编译器则默认不会启用,需要设置优化选项之后才会启用。所以写一些跨平台的代码的时候需要注意一下,做到心中有数。
- 未能启用NRV优化的情况,NRV优化并非在所有的情况下、所有的代码中都能够启用,可能在某些条件限制下编译器不能够启用优化,比如代码逻辑太复杂的情况下。
- 优化不是预期的需求,优化可能在无声无息中完成了,但是却有可能不是你想要的结果,比如你期待在拷贝构造函数中做一些事情,然后在析构函数中做相反的一些事情,但是拷贝构造函数并未如预期中的被调用了,导致了程序运行的错误。
总之,需要做到对编译器背后的行为有深入的理解,就能做到心中有数,写出既高效又安全的代码。
如果您感兴趣这方面的内容,请在微信上搜索公众号iShare爱分享并关注,以便在内容更新时直接向您推送。
深度解读《深度探索C++对象模型》之返回值优化的更多相关文章
- 返回值优化(RVO)
C++的函数中,如果返回值是一个对象,那么理论上它不可避免的会调用对象的构造函数和析构函数,从而导致一定的效率损耗.如下函数所示: A test() { A a; return a; } 在test函 ...
- 【M20】协助完成“返回值优化(RVO)”
1.方法返回对象,会导致临时对象的产生,这降低了效率,const Rational operator* (const Rational& lhs,Rational& rhs).有没有什 ...
- [转] C++中临时对象及返回值优化
http://www.cnblogs.com/xkfz007/articles/2506022.html 什么是临时对象? C++真正的临时对象是不可见的匿名对象,不会出现在你的源码中,但是程序在运行 ...
- [More Effective C++]条款22有关返回值优化的验证结果
(这里的验证结果是针对返回值优化的,其实和条款22本身所说的,考虑以操作符复合形式(op=)取代其独身形式(op),关系不大.书生注) 在[More Effective C++]条款22的最后,在返回 ...
- C++返回值优化RVO
返回值优化,是一种属于编译器的技术,它通过转换源代码和对象的创建来加快源代码的执行速度.RVO = return value optimization. 测试平台:STM32F103VG + Keil ...
- 转:C++中临时对象及返回值优化
http://www.cnblogs.com/xkfz007/articles/2506022.html 什么是临时对象? C++真正的临时对象是不可见的匿名对象,不会出现在你的源码中,但是程序在运行 ...
- C++返回值优化
返回值优化(Return Value Optimization,简称RVO)是一种编译器优化机制:当函数需要返回一个对象的时候,如果自己创建一个临时对象用于返回,那么这个临时对象会消耗一个构造函数(C ...
- 一段小代码秒懂C++右值引用和RVO(返回值优化)的误区
关于C++右值引用的参考文档里面有明确提到,右值引用可以延长临时变量的周期.如: std::string&& r3 = s1 + s1; // okay: rvalue referen ...
- 返回值优化 RVO
<深度探索C++对象模型>-- 2.3 返回值的初始化 & 在编译器层面做优化
- 《深度探索C++对象模型》第二章 | 构造函数语意学
默认构造函数的构建操作 默认构造函数在需要的时候被编译器合成出来.这里"在需要的时候"指的是编译器需要的时候. 带有默认构造函数的成员对象 如果一个类没有任何构造函数,但是它包含一 ...
随机推荐
- springboot+kaptcha生成数学运算验证码和字符验证码
使用以下代码只需要复制粘贴,修改一处文本生成器路径即可,文中有交代. 1.添加kaptcha依赖 <dependency> <groupId>com.github.penggl ...
- get 加 header 下载文件 函数,虽然最后没用。
export const apiDown = (url, data = {}) => { let data2 = secretFilter(data) axiosDown({ url, para ...
- Debian打开架构支持
第一步检查内核有没有 AMD和i386 dpkg --list | grep linux-image 会出现现在电脑上的内核,可以看到支持的架构 dpkg --print-foreign-archi ...
- 你的DDPG/RDPG为何不收敛?
园子好多年没有更过了,草长了不少.上次更还是读博之前,这次再更已是博士毕业2年有余,真是令人唏嘘.盗链我博客的人又见长,身边的师弟也问我挖的几个系列坑什么时候添上.这些着实令我欣喜,看来我写的东西也是 ...
- win10注册表各种配置
注册表教程 lesson combination of images step: 1_注册右键特定类型文件指令 step: 2_注册新建文件类型指令 step: 3_新建文件夹右键菜单 step: 4 ...
- FPGA 原语之一位全加器
FPGA原语之一位全加器 1.实验原理 一位全加器,三个输入,两个输出.进位输出Cout=AB+BC+CA,本位输出S=A异或B异或C.实验中采用三个与门.一个三输入或门(另外一个是两个或门,功能一致 ...
- java实战字符串3:反转每对括号间的子串,多个括号嵌套时,逐层反转
题目描述 给出一个字符串 s(仅含有小写英文字母和括号).请你按照从括号内到外的顺序,逐层反转每对匹配括号中的字符串,并返回最终的结果. 注意,您的结果中 不应 包含任何括号. 解答要求时间限制:10 ...
- Unicode编码解码的全面介绍
1. Unicode的起源和发展 Unicode是一个国际标准,旨在统一世界上所有文字的表示方式.它最初由Unicode协会创立,解决了不同字符集之间的兼容性问题.Unicode的发展经历了多个版本, ...
- AndroidStudio--app是如何运行的
#实用快捷键# Ctrl+alt+F 快速自动把类方法内部的变量声明为类属性变量,以方便全局使用! Ctrl+O 快速显示所有类方法以及field属性结构 今天发现了一个非常好的博主----litt ...
- #排序,去重#洛谷 5682 [CSPJX2019]次大值
题目 分析 首先,显然要排序去重,考虑最大值应该是\(a_{n-1}\)(排序后) 那次大值只有可能出现在\(a_{n-2}\)或者\(a_n\bmod a_{n-1}\)中 当然去重后如果只有一个数 ...