前言

考虑存在这样一个类如HeavyObject,其拷贝赋值操作比较耗时,通常你在使用函数返回这个类的一个对象时会习惯使用哪一种方式?或者会根据具体场景选择某一种方式?

// style 1
HeavyObject func(Args param); // style 2
bool func(HeavyObject* ptr, Args param);

上面的两种方式都能过到同样的目的,但直观上的使用体验的差别也是非常明显的:

style 1只需要一行代码,而style 2需要两行代码

// style 1
HeavyObject obj = func(params); // style 2
HeavyObject obj;
func(&obj, params);

但是,能达到同样的目的,消耗的成本却未必是一样的,这取决于多个因素,比如编译器支持的特性、C++语言标准的规范强制性、多团队多环境开发等等。

看起来style 2虽然使用时需要写两行代码,但函数内部的成本却是确定的,只会取决于你当前的编译器,外部即使采用不同的编译器进行函数调用,也并不会有多余的时间开销和稳定性问题。比如func内部使用clang+libc++编译,外部调用的编译环境为gcc+gnustl或者vc++,除了函数调用开销,不用担心其它性能开销以及由于编译环境不同会崩溃问题。

因此这里我主要剖析一下style 1背后开发者需要关注的点。

RVO

RVO是Return Value Optimization的缩写,即返回值优化,NRVO就是具名的返回值优化,为RVO的一个变种,此特性从C++11开始支持,也就是说C++98、C++03都是没有将此优化特性写到标准中的,不过少量编译器在开发过程中也会支持RVO优化(如IBM Compiler?),比如微软是从Visual Studio 2010才开始支持的。

仍然以上述的HeavyObject类为例,为了更清晰的了解编译器的行为,这里实现了构造/析构及拷贝构造、赋值操作、右值构造函数,如下

class HeavyObject
{
public:
HeavyObject() { cout << "Constructor\n"; }
~HeavyObject() { cout << "Destructor\n"; }
HeavyObject(HeavyObject const&) { cout << "Copy Constructor\n"; }
HeavyObject& operator=(HeavyObject const&) { cout << "Assignment Operator\n"; return *this; }
HeavyObject(HeavyObject&&) { cout << "Move Constructor\n"; }
private:
// many members omitted...
};

编译环境:
AppleClang 10.0.1.10010046

* 第一种使用方式

HeavyObject func()
{
return HeavyObject();
} // call
HeavyObject o = func();

按照以往对C++的理解,HeavyObject类的构造析构顺序应该为

Constructor

Copy Constructor
Destructor
Destructor

但是实际运行后的输出结果却为

Constructor

Destructor

实际运行中少了一次拷贝构造和析构的开销,编译器帮助我们作了优化。

于是我反汇编了一下:

0000000100000f60 <__Z4funcv>:
100000f60: 55 push %rbp
100000f61: 48 89 e5 mov %rsp,%rbp
100000f64: 48 83 ec 10 sub $0x10,%rsp
100000f68: 48 89 f8 mov %rdi,%rax
100000f6b: 48 89 45 f8 mov %rax,-0x8(%rbp)
100000f6f: e8 0c 00 00 00 callq 100000f80 <__ZN11HeavyObjectC1Ev>
100000f74: 48 8b 45 f8 mov -0x8(%rbp),%rax
100000f78: 48 83 c4 10 add $0x10,%rsp
100000f7c: 5d pop %rbp
100000f7d: c3 retq
100000f7e: 66 90 xchg %ax,%ax

上述汇编代码中的__Z4funcv即func()函数,__ZN11HeavyObjectC1Ev即HeavyObject::HeavyObject()。
不同编译器的C++修饰规则略有不同。

实际上这里就是先创建外部的对象,再将外部对象的地址作为参数传给函数func,类似style 2方式。

* 第二种使用方式

HeavyObject func()
{
HeavyObject o;
return o;
} // call
HeavyObject o = func();

运行上述调用代码的结果为

Constructor

Destructor

与第一种使用方式的结果相同,这里编译器实际做了NRVO,来看一下反汇编

0000000100000f40 <__Z4funcv>: // func()
100000f40: 55 push %rbp
100000f41: 48 89 e5 mov %rsp,%rbp
100000f44: 48 83 ec 20 sub $0x20,%rsp
100000f48: 48 89 f8 mov %rdi,%rax
100000f4b: c6 45 ff 00 movb $0x0,-0x1(%rbp)
100000f4f: 48 89 7d f0 mov %rdi,-0x10(%rbp)
100000f53: 48 89 45 e8 mov %rax,-0x18(%rbp)
100000f57: e8 24 00 00 00 callq 100000f80 <__ZN11HeavyObjectC1Ev> // HeavyObject::HeavyObject()
100000f5c: c6 45 ff 01 movb $0x1,-0x1(%rbp)
100000f60: f6 45 ff 01 testb $0x1,-0x1(%rbp)
100000f64: 0f 85 09 00 00 00 jne 100000f73 <__Z4funcv+0x33>
100000f6a: 48 8b 7d f0 mov -0x10(%rbp),%rdi
100000f6e: e8 2d 00 00 00 callq 100000fa0 <__ZN11HeavyObjectD1Ev> // HeavyObject::~HeavyObject()
100000f73: 48 8b 45 e8 mov -0x18(%rbp),%rax
100000f77: 48 83 c4 20 add $0x20,%rsp
100000f7b: 5d pop %rbp
100000f7c: c3 retq
100000f7d: 0f 1f 00 nopl (%rax)

从上面的汇编代码可以看到返回一个具名的本地对象时,编译器优化操作如第一种使用方式一样直接在外部对象的指针上执行构造函数,只是如果构造失败时还会再调用析构函数。

以上两种使用方式编译器所做的优化非常相近,两种方式的共同点都是返回本地的一个对象,那么当本地存在多个对象且需要根据条件选择返回某个对象时结果会是如何呢?

* 第三种使用方式

HeavyObject dummy(int index)
{
HeavyObject o[2];
return o[index];
} // call
HeavyObject o = dummy(1);

运行后的结果为

Constructor

Constructor
Copy Constructor
Destructor
Destructor
Destructor

从运行的结果可以看到没有做RVO优化,此时调用了拷贝构造函数。

从上述三种实现方式可以看到,如果你的函数实现功能比较单一,比如只会对一个对象进行操作并返回时,编译器会进行RVO优化;如果函数实现比较复杂,可能会涉及操作多个对象并不确定返回哪个对象时,编译器将不做RVO优化,此时函数返回时会调用类的拷贝构造函数。

但是,当只存在一个本地对象时,编译器一定会做RVO优化吗?

* 第四种使用方式

HeavyObject func()
{
return std::move(HeavyObject());
} // call
HeavyObject o = func();

实际运行输出的结果是

Constructor

Move Constructor
Destructor
Destructor

上述的函数实现直接返回临时对象的右值引用,从实际的运行结果来看调用了Move构造函数,与第一种使用方式运行的结果明显不同,并不是我期望的只调用一次构造函数和析构函数,也就是说编译器没有做RVO。

* 第五种使用方式

HeavyObject func()
{
HeavyObject o;
return static_cast<HeavyObject&>(o);
} // call
HeavyObject o = func();

实际运行输出的结果是

Constructor

Copy Constructor
Destructor
Destructor

上述的函数实现直接返回本地对象的引用,实际运行结果仍然调用了拷贝构造函数,并不是期望的只调用一次构造和析构函数,也就是说编译器并没有做RVO。

从上述两种使用方式可以看到,当返回一个对象时且对象类型与返回类型不一致时,编译器将不做RVO。实际上C++标准文档中有如下描述:

in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

总结

  • 两种style代码的性能可能会不一样,当你非常确定你的代码的开发环境及编译器的支持特性如RVO,以及使用者的接入环境时,建议使用style 1,否则建议使用style 2
  • RVO的编译器优化特性需要相对比较严格的限制,使用style 1时,较复杂的函数实现可能并不会如你期望的使用RVO优化

作者:lifesider

原文链接

本文为阿里云原创内容,未经允许不得转载

深入理解C++中的RVO的更多相关文章

  1. 如何理解javaSript中函数的参数是按值传递

    本文是我基于红宝书<Javascript高级程序设计>中的第四章,4.1.3传递参数小节P70,进一步理解javaSript中函数的参数,当传递的参数是对象时的传递方式. (结合资料的个人 ...

  2. 怎么理解js中的事件委托

    怎么理解js中的事件委托 时间 2015-01-15 00:59:59  SegmentFault 原文  http://segmentfault.com/blog/sunchengli/119000 ...

  3. 如何理解T-SQL中Merge语句(二)

    写在前面的话:上一篇写了如何理解T-SQL中Merge语句,基本把Merge语句要讲的给讲了,在文章的后面,抛出了几个结,当时没有想明白怎么去用文字表达,这一篇就来解答一下这几个结,又是一篇“天马行空 ...

  4. 如何理解T-SQL中Merge语句

    写在前面的话:之前看过Merge语句,感觉没什么用,完全可以用其他的方式来替代,最近又看了看Merge语句,确实挺好用,可以少写很多代码,看起来也很紧凑,当然也有别的优点. ====正文开始===== ...

  5. 深入理解JDK中的I/O

    深入理解JDK中的I/O 目 录 java内存模型GCHTTP协议事务隔离级并发多线程设计模式清楚redis.memcache并且知道区别mysql分表分库有接口幂等性了解jdk8稍微了解一下特性 j ...

  6. 深度理解Jquery 中 offset() 方法

    参考原文:深度理解Jquery 中 offset() 方法

  7. 简单理解Struts2中拦截器与过滤器的区别及执行顺序

    简单理解Struts2中拦截器与过滤器的区别及执行顺序 当接收到一个httprequest , a) 当外部的httpservletrequest到来时 b) 初始到了servlet容器 传递给一个标 ...

  8. 深入理解CSS中的层叠上下文和层叠顺序(转)

    by zhangxinxu from http://www.zhangxinxu.com 本文地址:http://www.zhangxinxu.com/wordpress/?p=5115 零.世间的道 ...

  9. 理解JavaScript中的原型继承(2)

    两年前在我学习JavaScript的时候我就写过两篇关于原型继承的博客: 理解JavaScript中原型继承 JavaScript中的原型继承 这两篇博客讲的都是原型的使用,其中一篇还有我学习时的错误 ...

  10. 理解Java中的弱引用(Weak Reference)

    本篇文章尝试从What.Why.How这三个角度来探索Java中的弱引用,理解Java中弱引用的定义.基本使用场景和使用方法.由于个人水平有限,叙述中难免存在不准确或是不清晰的地方,希望大家可以指出, ...

随机推荐

  1. JSF之Action 与ActionListener的区别

     事件  检验  参数  事件产生  页面跳转  Action  有 无参数,不传入当前控件,有返回值    当铵钮被单击时产生事件.提交表单   返回页面---根据配置文件跳转  ActionLis ...

  2. thttpd 2.27(最新)移植指南(官方安装脚本好多坑,我只想说)

    PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明   本文作为本人csdn blog的主站的备份.(Bl ...

  3. TTS 擂台: 文本转语音模型的自由搏击场

    对文本转语音 (text-to-speech, TTS) 模型的质量进行自动度量非常困难.虽然评估声音的自然度和语调变化对人类来说是一项微不足道的任务,但对人工智能来说要困难得多.为了推进这一领域的发 ...

  4. 从 Linux 内核角度探秘 JDK MappedByteBuffer

    本文涉及到的内核源码版本为: 5.4 ,JVM 源码为:OpenJDK17,RocketMQ 源码版本为:5.1.1 在之前的文章<一步一图带你深入剖析 JDK NIO ByteBuffer 在 ...

  5. elasticsearch使用painless的一些简单例子

    目录 1.背景 2.准备数据 2.1 mapping 2.2 插入数据 3.例子 3.1 (update)更新文档 id=1 的文档,将 age 加 2岁 3.2 (update_by_query)如 ...

  6. 01-【HAL库】STM32实现串口打印

    一.什么是串口 串口通讯(Serial Communication)是一种设备间非常常用的串行通讯方式,因为它简单便捷,因此大部分电子设备都支持该通讯方式,电子工程师在调试设备时也经常使用该通讯方式输 ...

  7. KafkaConsumerDemo

    pom <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>sp ...

  8. 无监督学习-K-means算法

    无监督学习-K-means算法 1. 什么是无监督学习 一家广告平台需要根据相似的人口学特征和购买习惯将美国人口分成不同的小组,以便广告客户可以通过有关联的广告接触到他们的目标客户. Airbnb 需 ...

  9. 【已解决】解决Python打开文件---路径报错问题(SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position 2-3: truncated \UXXXXXXXX escape)

    原因分析: 在windows系统当中读取文件路径可以使用\,但是在python字符串中\有转义的含义, 如\t可代表TAB,\n代表换行, 所以我们需要采取一些方式使得\不被解读为转义字符.目前有3个 ...

  10. 强烈推荐:2024 年12款 Visual Studio 亲测、好用、优秀的工具,AI插件等

    工具类扩展 1. ILSpy 2022 (免费) ILSpy 是 ILSpy 开源反编译器的 Visual Studio 扩展. 是一款开源.免费的.且适用于.NET平台反编译[C#语言编写的程序和库 ...