前言

大家都知道“过早的优化是万恶之源”这句话,然而我相信其中的大多数人都不知道自己是不是在做过早的优化。我也无法准确的定义什么叫做“过早的优化”,但我相信这“过早的优化”要么是得不偿失的,要么干脆是有害无利的。今天我就想举个我认为是“过早的优化”的例子。

从函数返回值

为了从一个函数得到运行结果,常规的途径有两个:通过返回值和通过传入函数的引用或指针(当然还可以通过全局变量或成员变量,但我觉得这算不上是什么好主意)。

通过传给函数一个引用或指针来承载返回值在很多情况下是无可厚非的,毕竟有时函数需要将多个值返回给用户。除了这种情况之外,我觉得应当尽量做到参数作为函数输入,返回值作为函数输出(这不是很自然的事情吗?)。然而,我们总能看到一些“突破常规”的做法:

首先定义Message类:

struct Message
{int a;int b;int c;int d;int e;int f;
};

为了从某个地方(比如一个队列)得到一个特定Message对象,有些人喜欢写一个这样的getMessage:

void getMessage(Message &msg); // 形式1

虽然只有一个返回值,但仍然是通过传入函数的引用返回给调用者的。

为什么要这样呢?“嗯,为了提高性能。你知道,要是这样定义函数,返回Message对象时必须要构造一个临时对象,这对性能有影响。”

Message getMessage(); // 形式2

我们先不讨论这带来了多少性能提升,先看看形式1相对形式2带来了哪些弊端。我认为有两点:

1. 可读性变差

略(我希望你能和我一样认为这是显而易见的)。

2. 将对象的初始化划分成了两个步骤

调用形式1时,你必然要这样:

Message msg;     // S1
getMessage(msg); // S2

这给维护者带来了犯错的机会:一些需要在S2语句后面对msg进行的操作有可能会被错误的放在S1和S2之间。
如果是形式2,维护者就不可能犯这种错误:

Message msg = getMessage();

好,现在我们来看性能,形式2真的相对形式1性能更差吗?对于下面的代码:

#include <stdio.h>struct Message
{Message(){ printf("Message::Message() is called\n"); }Message(const Message &){printf("Message::Message(const Message &msg) is called\n");}Message& operator=(const Message &){printf("Message::operator=(const Message &) is called\n");}~Message(){printf("Message::~Message() is called\n");}int a;int b;int c;int d;int e;int f;
};Message getMessage()
{Message result;result.a = 0x11111111;return result;
}int main()
{Message msg = getMessage();return 0;
}

你认为运行时会输出什么呢?是不是这样:

Message::Message() is called
Message::Message(const Message &msg) is called
Message::~Message() is called
Message::~Message() is called

其中,第一行是临时对象result构造时打印,第二行是将临时对象赋给msg时打印,第三行是临时对象result析构时打印,第四行是msg析构时打印。

然而使用GCC 7.3.0版本使用O0(即关闭优化)编译上述代码后,运行结果为:

Message::Message() is called
Message::~Message() is called

并没有像预期的输出那样。

如果使用MSVC2017编译,且关闭优化(/Od),确实可以得到预期输入,但是一旦打开优化(/O2),输出就和GCC的一样了。

我们看看实际上生成了什么代码(使用GCC编译):

(gdb) disassemble main
Dump of assembler code for function main():0x0000000000000776 <+0>:  push   %rbp0x0000000000000777 <+1>:  mov    %rsp,%rbp0x000000000000077a <+4>: push   %rbx0x000000000000077b <+5>:  sub    $0x28,%rsp0x000000000000077f <+9>:    mov    %fs:0x28,%rax0x0000000000000788 <+18>:    mov    %rax,-0x18(%rbp)0x000000000000078c <+22>: xor    %eax,%eax0x000000000000078e <+24>:    lea    -0x30(%rbp),%rax             #将栈上地址-0x30(%rbp)传给getMessage函数0x0000000000000792 <+28>: mov    %rax,%rdi0x0000000000000795 <+31>:    callq  0x72a <getMessage()>0x000000000000079a <+36>:   mov    $0x0,%ebx0x000000000000079f <+41>:    lea    -0x30(%rbp),%rax0x00000000000007a3 <+45>: mov    %rax,%rdi0x00000000000007a6 <+48>:    callq  0x7e4 <Message::~Message()>0x00000000000007ab <+53>:    mov    %ebx,%eax0x00000000000007ad <+55>:    mov    -0x18(%rbp),%rdx0x00000000000007b1 <+59>: xor    %fs:0x28,%rdx0x00000000000007ba <+68>:    je     0x7c1 <main()+75>0x00000000000007bc <+70>: callq  0x5f0 <__stack_chk_fail@plt>0x00000000000007c1 <+75>:  add    $0x28,%rsp0x00000000000007c5 <+79>:   pop    %rbx0x00000000000007c6 <+80>: pop    %rbp0x00000000000007c7 <+81>: retq
End of assembler dump.
(gdb) disassemble getMessage
Dump of assembler code for function getMessage():0x000000000000072a <+0>:    push   %rbp0x000000000000072b <+1>:  mov    %rsp,%rbp0x000000000000072e <+4>: sub    $0x20,%rsp0x0000000000000732 <+8>:    mov    %rdi,-0x18(%rbp)                 #将main函数传入的栈上地址保存到-0x18(%rbp)处0x0000000000000736 <+12>:  mov    %fs:0x28,%rax0x000000000000073f <+21>:    mov    %rax,-0x8(%rbp)0x0000000000000743 <+25>:  xor    %eax,%eax0x0000000000000745 <+27>:    mov    -0x18(%rbp),%rax             #将main函数传入的栈上地址传给Message::Message()函数0x0000000000000749 <+31>:   mov    %rax,%rdi0x000000000000074c <+34>:    callq  0x7c8 <Message::Message()>0x0000000000000751 <+39>: mov    -0x18(%rbp),%rax0x0000000000000755 <+43>: movl   $0x11111111,(%rax)0x000000000000075b <+49>:   nop0x000000000000075c <+50>: mov    -0x18(%rbp),%rax0x0000000000000760 <+54>: mov    -0x8(%rbp),%rdx0x0000000000000764 <+58>:  xor    %fs:0x28,%rdx0x000000000000076d <+67>:    je     0x774 <getMessage()+74>0x000000000000076f <+69>:   callq  0x5f0 <__stack_chk_fail@plt>0x0000000000000774 <+74>:  leaveq 0x0000000000000775 <+75>: retq
End of assembler dump.

可以看出来,在getMessage函数中构造的对象实际上位于main函数的栈帧上,并没有额外构造一个Message对象。这是因为开启了所谓的返回值优化(RVO,Return Value Optimization)的缘故。你想得到的效果编译器已经自动帮你完成了,你不必再牺牲什么。

RVO

对于我们这些用户来说,RVO并不是什么特别复杂的机制,主流的GCC和MSVC均支持,也没什么特别需要注意的地方。它存在的目的是优化掉不必要的拷贝复制函数的调用,即使拷贝复制函数有什么副作用,例如上面代码中的打印语句,这可能是唯一需要注意的地方了。从上面的汇编代码中可以看出来,在GCC中,其基本手段是直接将返回的对象构造在调用者栈帧上,这样调用者就可以直接访问这个对象而不必复制。

RVO是有限制条件的,在某些情况下无法进行优化,在一篇关于MSVC2005的RVO技术的文章中,提到了3点导致无法优化的情况:

1. 函数抛异常

关于这点,我是有疑问的。文章中说如果函数抛异常,开不开RVO结果都一样。如果函数抛异常,无法正常的返回,我当然不会要求编译器去做RVO了。

2. 函数可能返回具有不同变量名的对象

例如:

Message getMessage_NoRVO1(int in)
{Message msg1;msg1.a = 1;Message msg2;msg2.a = 2;if (in % 2){return msg1;}else{return msg2;}
}

经过验证,在GCC上确实也是这样的,拷贝构造函数被调用了。但这种情况在很多时候应该都是可以通过重构避免的。

Message::Message() is called
Message::Message() is called
Message::Message(const Message &msg) is called
Message::~Message() is called
Message::~Message() is called
Message::~Message() is called

3. 函数有多个出口

例如:

Message getMessage_NoRVO2(int in)
{Message msg;if (in % 2){return msg;}msg.a = 1;return msg;
}

这个在GCC上验证发现RVO仍然生效,查看汇编发现只有一个retq指令,多个出口被优化成一个了。
————————————————

C++ 返回值优化(RVO,Return Value Optimization)相关推荐

  1. C++ 返回值优化 RVO

    C++ 返回值优化 RVO 引子 返回值优化 RVO RVO 限制 参考 最近在调试代码时,发现拷贝和移动系列构造函数的调用和预期不太一样,经过查阅相关资料,发现是返回值优化(RVO)从中做梗.了解了 ...

  2. C++高阶 返回值优化--RVO和NRVO介绍

    RVO即返回值优化(return value optimize),可以少做一次拷贝构造. NRVO是具名返回值的意思,起初RVO技术仅支持匿名变量的优化,后期才加入对具名变量的优化. RVO: Big ...

  3. c++中返回值优化(RVO)和命名返回值优化(NRVO)介绍

    RVO和NRVO介绍 前言 半年前就想写一篇关于RVO和NRVO的介绍,但碍于没什么时间去写博客.在跟身边人进行学术探讨的时候,会发现部分人可能尝到了编译器给它做返回值优化的好处,知道这段代码被优化了 ...

  4. 什么是复制省略和返回值优化?

    本文翻译自:What are copy elision and return value optimization? What is copy elision? 什么是复制省略? What is (n ...

  5. 提高C++性能的编程技术笔记:虚函数、返回值优化+测试代码

    虚函数:在以下几个方面,虚函数可能会造成性能损失:构造函数必须初始化vptr(虚函数表):虚函数是通过指针间接调用的,所以必须先得到指向虚函数表的指针,然后再获得正确的函数偏移量:内联是在编译时决定的 ...

  6. C++的返回值优化(RVO,Return Value Optimization)

    前言 大家都知道"过早的优化是万恶之源"这句话,然而我相信其中的大多数人都不知道自己是不是在做过早的优化.我也无法准确的定义什么叫做"过早的优化",但我相信这& ...

  7. 浅谈C++11标准中的复制省略(copy elision,也叫RVO返回值优化)

    严正声明:本文系作者davidhopper原创,未经许可,不得转载. C++11以及之后的C++14.17标准均提出一项编译优化技术:复制省略(copy elision,也称复制消除),另外还有RVO ...

  8. C++之RVO返回值优化

    什么是RVO优化 RVO的全称是Return Value Optimization.RVO是一种编译器优化技术,可以把通过函数返回创建的临时对象给"去掉",然后可以达到少调用拷贝构 ...

  9. C++编程法则365条一天一条(358)copy elision(返回值优化NVO和具名返回值优化NRVO)

    文章目录 强制编译器实现的优化 非强制实现优化 参考:https://en.cppreference.com/w/cpp/language/copy_elision Elision 是省略.删节或者忽 ...

  10. java中return返回值_Java中return的用法

    展开全部 一.return语句总是用在方法中,有两个作用. 一个是返回方法指定类型的值(这个值总62616964757a686964616fe59b9ee7ad9431333366306434是确定的 ...

最新文章

  1. fastjson过滤属性或函数
  2. 重启服务器之home下文件全没,小白宝典——树莓派实用工具分享(大神绕路)
  3. javascript学习系列(13):数组中的concat方法
  4. 前端学习(2452):封装数据接口
  5. 编译原理——实验叁预习报告——基于YACC的TINY语法分析器的构建
  6. 散点图 横纵坐标_厉害了我的Python!散点图还能这么画
  7. trufflesuite/truffle-hdwallet-provider
  8. 2019中国软件百强榜:华为、阿里、百度、腾讯位列前四
  9. YYUC01——Windows本地环境搭建
  10. android 开机动画xp,XP下更改开机动画
  11. html选择日期的组件,怎样实现一个datePicker(日期选择)组件
  12. Matlab plot画图线型、符号及颜色
  13. 高数 | 旋转体体积计算方法汇总、二重积分计算旋转体体积
  14. hd6630m可以玩lol吗_《LOL》流畅玩!Intel HD620核显性能实测
  15. linux开机自动执行脚本、运行程序
  16. coreldraw x7 分布_CDR X7新增功能有哪些,CDR X7新功能介绍
  17. 下面不属于电子计算机外存储器的是,2013年计算机专转本模拟题三答案
  18. python中如何引入π_python如何计算π
  19. chrom,firefox,ie不能上网,百度浏览器却可以。。。
  20. 【Golang实战】——XPath解析网页

热门文章

  1. selenium自动化测试框架之PO设计模式
  2. MySQL开启命令自动补全功能(auto-rehash)
  3. Codevs 均分纸牌(贪心)
  4. IDEA中如何配置Tomcat和项目?
  5. 详解 Spring 3.0 基于 Annotation 的依赖注入实现(转)
  6. SQL Server 中位数、标准差、平均数
  7. About MS Reporting Service
  8. 3.软件架构设计:大型网站技术架构与业务架构融合之道 --- 语言
  9. 23. jQuery 遍历 - 过滤
  10. css中利用margin来隐藏元素