接下来我将持续更新“深度解读《深度探索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++对象模型》之返回值优化的更多相关文章

  1. 返回值优化(RVO)

    C++的函数中,如果返回值是一个对象,那么理论上它不可避免的会调用对象的构造函数和析构函数,从而导致一定的效率损耗.如下函数所示: A test() { A a; return a; } 在test函 ...

  2. 【M20】协助完成“返回值优化(RVO)”

    1.方法返回对象,会导致临时对象的产生,这降低了效率,const Rational operator* (const Rational& lhs,Rational& rhs).有没有什 ...

  3. [转] C++中临时对象及返回值优化

    http://www.cnblogs.com/xkfz007/articles/2506022.html 什么是临时对象? C++真正的临时对象是不可见的匿名对象,不会出现在你的源码中,但是程序在运行 ...

  4. [More Effective C++]条款22有关返回值优化的验证结果

    (这里的验证结果是针对返回值优化的,其实和条款22本身所说的,考虑以操作符复合形式(op=)取代其独身形式(op),关系不大.书生注) 在[More Effective C++]条款22的最后,在返回 ...

  5. C++返回值优化RVO

    返回值优化,是一种属于编译器的技术,它通过转换源代码和对象的创建来加快源代码的执行速度.RVO = return value optimization. 测试平台:STM32F103VG + Keil ...

  6. 转:C++中临时对象及返回值优化

    http://www.cnblogs.com/xkfz007/articles/2506022.html 什么是临时对象? C++真正的临时对象是不可见的匿名对象,不会出现在你的源码中,但是程序在运行 ...

  7. C++返回值优化

    返回值优化(Return Value Optimization,简称RVO)是一种编译器优化机制:当函数需要返回一个对象的时候,如果自己创建一个临时对象用于返回,那么这个临时对象会消耗一个构造函数(C ...

  8. 一段小代码秒懂C++右值引用和RVO(返回值优化)的误区

    关于C++右值引用的参考文档里面有明确提到,右值引用可以延长临时变量的周期.如: std::string&& r3 = s1 + s1; // okay: rvalue referen ...

  9. 返回值优化 RVO

    <深度探索C++对象模型>-- 2.3 返回值的初始化 & 在编译器层面做优化

  10. 《深度探索C++对象模型》第二章 | 构造函数语意学

    默认构造函数的构建操作 默认构造函数在需要的时候被编译器合成出来.这里"在需要的时候"指的是编译器需要的时候. 带有默认构造函数的成员对象 如果一个类没有任何构造函数,但是它包含一 ...

随机推荐

  1. br 词根 分支 broad bread branch brother broom 看到abroad后查到的

    br-分支 cl-集合 pater-父亲 br/other 兄弟-br/oom笤帚-br/eed繁殖-br/ood一窝-br/eak打破-br/anch分枝 cl/ash碰撞-class/ify分类- ...

  2. 解决linux平台无法使用getch()的问题

    参考https://www.cnblogs.com/jiangxinnju/p/5516906.html#:~:text=%E5%8F%A6%E5%A4%96%E5%A4%A7%E5%AE%B6%E5 ...

  3. Cloud XR面临的问题以及Cloud XR主要应用场景

    cloud xr面临的问题 带宽要求高:cloud xr需要实时把一个高码率的视频流,从云端传输到终端,这需要一个非常大的带宽. 延迟要求低:在传输的过程中,它需要一个非常低的时延,XR每进行一个新动 ...

  4. 浅析三维模型3DTile格式轻量化处理常见问题与处理措施

    浅析三维模型3DTile格式轻量化处理常见问题与处理措施 三维模型3DTile格式的轻量化处理是大规模三维地理空间数据可视化的关键环节,但在实际操作过程中,往往会遇到一些问题.下面我们来看一下这些常见 ...

  5. 关于三维模型OBJ格式轻量化压缩必要性探讨

    关于三维模型OBJ格式轻量化压缩必要性探讨 三维模型的OBJ格式轻量化压缩在当前的计算机图形学和虚拟现实应用中具有重要的必要性.以下是对三维模型OBJ格式轻量化压缩必要性的分析: 1.提高加载和传输效 ...

  6. .NET Emit 入门教程:第四部分:构建类型(Type)

    前言: 在动态生成代码的过程中,构建类型(Type)是至关重要的一步. 通过使用 Emit 中的 TypeBuilder,我们可以定义和创建各种类型,包括类.结构体和接口. 本节将深入探讨如何使用 T ...

  7. Dynamic ReLU:微软推出提点神器,可能是最好的ReLU改进 | ECCV 2020

    论文提出了动态ReLU,能够根据输入动态地调整对应的分段激活函数,与ReLU及其变种对比,仅需额外的少量计算即可带来大幅的性能提升,能无缝嵌入到当前的主流模型中   来源:晓飞的算法工程笔记 公众号 ...

  8. SPEL表达式注入分析

    环境依赖 <dependencies> <dependency> <groupId>org.springframework</groupId> < ...

  9. 学习Python前要了解的tips

    学习Python前要了解的tips 对后续的学习来说很重要,否则后续会出现一些奇奇怪怪的问题,而且很难找到解决方法.嘿嘿,就不要问我怎么知道的了吧,好多都是我踩过的坑 卸载电脑内软件 之前我一直用电脑 ...

  10. [Linux]将ArchLinux安装到U盘

    将ArchLinux安装到U盘 几个月前入门Arch的时候上网搜了不少安装教程,同时由于当时硬盘空间不太够于是就打算安装到U盘上,也因此踩了不少坑. 但128G的U盘都拿来装Arch的话未免也太浪费了 ...