一道面试题:你了解哪些编译器优化行为?知道Copy elision 、RVO吗?
C++11以后,g++ 编译器默认开启复制省略(copy elision)选项,可以在以值语义传递对象时避免触发复制、移动构造函数。copy elision 主要发生在两个场景:
- 函数返回的是值语义时
- 函数参数是值语义时
返回值优化
返回值优化RVO(Return Value Optimization,RVO),即避免返回过程触发复制 / 移动构造函数。根据返回的值是否是匿名对象,可以分为两类:
- 具名返回值优化
NRVO
(Named Return Value Optimization,NRVO) - 匿名返回值优化
URVO
(Unknown Return Value Optimization,URVO )
二者的区别在于返回值是具名的局部变量(NRVO)还是无名的临时对象(URVO)。
假定现在有类Foo
,实现了复制构造函数(ctor
)、 移动构造函数(mtor
)。
class Foo { public:Foo() { std::cout<<"default"<<std::endl; }Foo(const Foo& rhs) { std::cout<<"ctor"<<std::endl; }Foo(Foo&& rhs) { std::cout<<"mtor"<<std::endl; }};
现在,有返回类型是Foo
的 两个函数:return_urvo_value
和 return_nrvo_value
,实现如下:
Foo return_urvo_value() { return Foo{}; }Foo return_nrvo_value() { Foo local_obj;return local_obj; }
按照常规,return_urvo_value
函数返回Foo{}
应该触发mtor
, return_nrvo_value
函数返回local_obj
应该触发ctor
。真的如此吗?
int main(int argc, char const *argv[]) {auto x = return_urvo_value();auto y = return_nrvo_value(); return 0;}
输出如下:
g++ rvo.cc -o rvo && ./rvodefaultdefault
输出结果,令人惊讶!竟然都只调用了一次默认构造函数。这是因为编译器默认开启了RVO,为了禁止这个优化策略,需要为编译加上 -fno-elide-constructors
选项,此时输出如下:
$ g++ -fno-elide-constructors rvo.cc -o rvo && ./rvodefaultmtormtordefaultmtormtor
下面对输出结果,逐个分析。
URVO
首先,return_urvo_value
函数,触发两次移动构造函数,这很好理解:
- 基于return的
Foo{}
构造return_urvo_value
函数的返回值,触发一次; - 基于
return_urvo_value
函数返回的右值构造x
,触发一次。
return_urvo_value
函数return的Foo{}
,中间经过两次mtor
,才将Foo{}
的内部数据转移到了x
。但是,这中间的两次mtor
是可以避免的:由于return之后Foo{}
就结束生命周期,那为什么不直接将Foo{}
用于x
呢?
因此,编译器默认开启RVO,省略中间两次调用mtor
的过程,直接基于return_urvo_value
函数中return的Foo{}
构造x
。此时,整个过程简化如下:
Foo x{};
NRVO
但是!!!,return_nrvo_value
函数,怎么也触发了两次mtor
,而不是ctor
+ mtor
?
这是因为
local_obj
是局部变量,return_nrvo_value
函数执行return语句的同时,local_obj
的生命周期也即将结束。既然如此,与其返回local_obj
的副本,不如直接将local_obj
返回回去,既避免了析构local_obj
,也避免了重新分配Foo
对象。编译器默认开启RVO时,则可以完成上述优化。当编译加上
-fno-elide-constructors
标志禁止RVO优化时,那么编译器也会优先选择mtor
,将local_obj
的内部数据转移到return_nrvo_value
的返回值中,最后用于构造y
,避免重新为local_obj
中的数据分配内存。
因此,return_nrvo_value
函数,即使禁止了RVO优化,也是触发两次移动构造函数,而不是一次复制构造、一次移动构造。为了验证确实是将local_obj
的内部数据转移到了y
,对return_nrvo_value
函数修改如下:
std::vector<int> return_nrvo_value() {std::vector<int> local_vec{1,2,3,4};std::cout<<"object address: "<< std::addressof(local_vec)<<" |data address:" << std::addressof(local_vec[0])<<std::endl;return local_vec;}int main(int argc, char const *argv[]) {auto y = return_nrvo_value(); std::cout<<"object address: "<< std::addressof(y)<<" |data address:" << std::addressof(y[0])<<std::endl;return 0; }
分别开启rvo
优化、禁止rvo
优化,输出如下:
$ g++ rvo.cc -o rvo && ./rvoobject address: 0x7ffffc262da0 |data address:0x7ffff55e9eb0object address: 0x7ffffc262da0 |data address:0x7ffff55e9eb0$ g++ -fno-elide-constructors rvo.cc -o rvo && ./rvoobject address: 0x7fffc9b6ee80 |data address:0x7fffc2969eb0object address: 0x7fffc9b6ef00 |data address:0x7fffc2969eb0
从输出,可以看出:
- 当开启RVO时,不仅
y
和local_vec
指向的数据内存一致,y
和local_vec
对象本身地址都是一致,即y
就是local_vec
; - 当使用
-fno-elide-constructors
禁止RVO时,y
和local_vec
仍指向同一片内存区,但是此时y
的地址不是local_vec
的地址,说明local_vec
将数据转移到了y
后,local_Vec
本身还是析构了,而y
是基于移动构造函数重新创建的对象。
C++17强制编译器实现 URVO
在上面的demo中,Foo
的mtor
必须是可访问的,即移动构造函数没有加上=delete
标志,也没有设置为private
属性。到了C++17,时代变了,强制编译器实现RVO,就是即便你禁止了移动构造函数,对象也能具有URVO能力。比如,将上面的类Foo
修改如下:
class Foo { public:Foo() { std::cout<<"default"<<std::endl; }// 禁止复制、移动构造函数Foo(const Foo& rhs) = delete;Foo(Foo&& rhs) =delete;};int main(int argc, char const *argv[]) {auto x = return_urvo_value();auto y = return_nrvo_value(); return 0;}
下面分别在C++14、17的编译输出:
C++14编译输出如下:
$ g++ -std=c++14 rvo.cc -o rvo && ./rvorvo.cc: In function ‘Foo return_urvo_value()’:rvo.cc:15:14: error: use of deleted function ‘Foo::Foo(Foo&&)’15 | return Foo{};| ^rvo.cc:10:3: note: declared here10 | Foo(Foo&& rhs) =delete;| ^~~rvo.cc: In function ‘Foo return_nrvo_value()’:rvo.cc:21:10: error: use of deleted function ‘Foo::Foo(const Foo&)’21 | return local_obj;| ^~~~~~~~~rvo.cc:9:3: note: declared here9 | Foo(const Foo& rhs) = delete;| ^~~
C++17编译输出如下:
$ g++ -std=c++17 rvo.cc -o rvo && ./rvorvo.cc: In function ‘Foo return_nrvo_value()’:rvo.cc:21:10: error: use of deleted function ‘Foo::Foo(const Foo&)’21 | return local_obj;| ^~~~~~~~~rvo.cc:9:3: note: declared here9 | Foo(const Foo& rhs) = delete;| ^~~
从两编译输出可以看出,即使在Foo
同时禁止复制、移动构造函数时,C++17编译器仍然能强实现NRVO,但是都不支持NRVO。但是如果仅禁止Foo
的复制构造函数呢?注意,在禁止复制构造函数时,要主动实现移动构函数,否则效果和同时禁止ctor
和mtor
一样。
class Foo { public:Foo() { std::cout<<"default"<<std::endl; }// 禁止复制、移动构造函数Foo(const Foo& rhs) = delete;Foo(Foo&& rhs) { std::cout<<"mtor"<<std::endl;}};
此时输出如下:
$ g++ -std=c++17 rvo.cc -o rvo && ./rvodefaultdefault$ g++ -std=c++14 rvo.cc -o rvo && ./rvodefaultdefault
因此,可总结如下:当函数的返回类型是值类型时,
URVO:在C++17之前,对象的
motor
必须是可访问的,才能开启URVO。// return_urvo_value 导致编译失败class Foo { public:Foo() =default;Foo(const Foo& rhs) =default;Foo(Foo&& rhs) =delete; // mtor 不可访问};// 编译通过class Foo { public:Foo() =default;Foo(const Foo& rhs) =default;};
C++17开始,即使完全禁止了对象的
ctor
、motr
,编译器一样可以实现URVO。NRVO:对象的
mtor
必须可访问的,才能开启。
URVO 应用
根据URVO特性,我么可以为 std::unique_ptr
、 std::atomic
等提供一个工厂函数 make_instance
。
template <typename T, typename... Args>T make_instance(Args&& ... args) {return T {std::forward<Args>(args)...};}int main(int argc, const char* argv[]) {// 普通类型int i = make_instance<int>(42);// std::unique_ptr 实现了 移动构造函数,因此可以编译成功 auto up = make_instance<std::unique_ptr<int>>(new int{ 42 }); // 禁止了复制构造函数,但是也没有实现移动构造函数,因此要到 C++17 才能编译过auto ai = make_instance<std::atomic<int>>(42); return 0;}
在上面的make_instance
对于std::unique_ptr
、std::atomic
要求不同:
std::unique_ptr
:虽然禁止了ctor
,但实现mtor
,因此它在C++11中可以开启NRVO。注意,在C++14中已经为std::unique_ptr
提供了工厂函数std::make_unique
,实现如下:// 和 make_instance 如出一辙template <typename _Tp, typename... _Args>unique_ptr<_Tp> make_unique(_Args && ...__args){return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...));}
std::atomic
:同时禁止了ctor
、mtor
,因此必须等到 C++17,make_instance
函数才能为std::atmoic
创建对象。
函数值传递
在 函数模板之值传递与引用传递的不同类型推导规则辨析 一文中,深度讲解了函数模板基于值传递和引用传递的优劣。在讲值传递时,未必总是发生复制行为:pass_by_value
函数传入右值时,也会发生copy elision 行为,即使禁止编译器的copy elision 行为,也是优先调用对象的mtor
。
void pass_by_value(Foo foo) { // ...}int main(int argc, char const *argv[]) {auto x = return_urvo_value();auto y = return_nrvo_value(); pass_by_value(Foo{});pass_by_value(std::move(x));return 0;}
最终的输出也是调用默认三次构造函数:
$ g++ -std=c++11 rvo.cc -o rvo && ./rvodefaultdefaultdefault
到此,copy elision 的两个主要应用场景基本分析结束。
感谢你的观看,你的点赞、关注与分享就是对我最大的支持。
更多硬核知识,微信搜一搜:【
look_code_art
】,欢迎关注!!!
一道面试题:你了解哪些编译器优化行为?知道Copy elision 、RVO吗?相关推荐
- 从一道笔试题谈算法优化(下)
因为受到经济危机的影响,我在 bokee.com 的博客可能随时出现无法访问的情况:因此将2005年到2006年间在 bokee.com 撰写的博客文章全部迁移到 csdn 博客中来,本文正是其中一篇 ...
- 从一道笔试题谈算法优化(上)
因为受到经济危机的影响,我在 bokee.com 的博客可能随时出现无法访问的情况:因此将2005年到2006年间在 bokee.com 撰写的博客文章全部迁移到 csdn 博客中来,本文正是其中一篇 ...
- 从一道面试题谈谈一线大厂码农应该具备的基本能力
作者:Yura Shevchenko 来源:skypixel.com 关于一线码农的面试,我想说 求职面试在绝大部分人来说都是必不可少的,自己作为求职者也参与了不少面试(无论成功或者失败),作为技术面 ...
- 最近刷爆朋友圈的一道面试题
前言: 最近在网上有一道面试题掀起了劲爆的浪潮,好多家公司都模仿提问了这么一道面试题,而且好多人也都在讨论这道面试题要是自己回答的话该怎么回答!这道题也是在个网站上刷爆了. 面试题 如果不用Sprin ...
- 从一道面试题谈起,大厂到底看重程序员的什么能力?
唐磊,他谦逊的自我介绍,是"在阿里云打工的清华学渣". 上周的一篇<字符串比较,居然暗藏玄机>,我最早是在唐磊<这10行比较字符串相等的代码给我整懵了>里看 ...
- PHP递归创建多级目录(一道面试题的解题过程)
今天看到一道面试题,要写出一个可以创建多级目录的函数: 我的第一个感觉就是用递归创建,具体思路如下: function Directory($dir){ if(is_dir($dir) || @mkd ...
- C#编译器优化那点事
使用C#编写程序,给最终用户的程序,是需要使用release配置的,而release配置和debug配置,有一个关键区别,就是release的编译器优化默认是启用的. 优化代码开关即optimize开 ...
- 一道面试题(限流,幂等key)
一道面试题[限流,幂等key] 题目介绍 关键代码 使用令牌桶进行限流 幂等性校验 新版校验幂等 简易的使用日志框架 简单的redis功能实现 bean转换工具 日期工具类 json工具类 返回值 题 ...
- java yang模型_一道面试题引发的对Java内存模型的一点疑问
一道面试题引发的对Java内存模型的一点疑问 问题描述如上图所示程序,按道理,子线程会通过 num++ 操作破坏 while 循环的条件,从而终止循环,执行最后的输出操作.但在我的多次运行中,偶尔会出 ...
最新文章
- 敏捷研发落地之持续集成
- #JS:this的指向及函数调用对this的影响
- mysql 造1亿条记录的单表--大数据表
- (转)[Android] 利用 ant 脚本修改项目包名
- 云炬Android开发笔记 9主界面-通用底部导航设计与一键式封装
- python使用opencv_教你快速使用OpenCV/Python/dlib進行眨眼檢測識別!
- Qt文档阅读笔记-QWebEngineView及QML WebEngineView
- devops工作流程_DevOps会偷我的工作吗?
- java 调用dll内存泄露_对 精致码农大佬 说的 Task.Run 会存在 内存泄漏 的思考
- Lesson 3.5 - Maya Commands: getAttr
- PHP丢失依赖文件libssl.so libcrypto.so
- 计算机识别图像的原理,什么是图像识别技术?图像识别技术原理介绍
- 【Typecho插件】Typecho百度主动推送插件
- 按键精灵 手机 oracle,按键精灵Android版:软件使用
- linux的tmp分区,在Linux系统中的单独分区上挂载/tmp的方法
- Linux 打开文件显示: No such file or directory
- Android新浪微博分页加载,Android仿新浪微博自定义ListView下拉刷新(4)
- 正则表达式提取html内容
- 程序员在网吧敲代码,这波操作真的太秀了!
- B1031. 查验身份证
热门文章
- 秒解UTF-8带来的烦恼
- 家庭中的交换机如何选择?几种常见的交换机选择避坑方法需要知道
- 网站服务器坏了要修多久,大学服务器电脑坏了,一分钟修好收500,朋友:有钱不挣是傻子!...
- Oracle Newsletter闪亮人物推介--Joel Perez
- windows ios良心软件推荐
- 普通用户申请微软的OneDrive免费网盘,容量5T、5T、5T,重要事情说三遍!!!!!
- ADB常用命令(adb常用命令)
- idm下载器(Internet Download Manager)
- 安徒生---海的女儿
- 阿里云国际站实名认证上传材料填写样例(域名持有者为组织)