跟我学C++中级篇——返回值优化

235 篇文章 94 订阅

一、前景介绍

在开发者编写代码的过程中,很多函数需要返回值的。如果只是简单的返回一个基础类型值,即使有一些浪费,这些浪费在整体的程序运行中其实影响还是相当小的。但当开发者自定义一些数据结构或者字符串等的返回时,就有可能出现多次拷贝的现象。这样的运行成本可能就相当大了。
开发者在早期的编译期环境中,可能需要一些具体的技巧来处理这些返回值,但在越来越优秀的编译器加持下,返回值优化的手段从开发者不断的向编译期转移。这和前文(跟我学C++中级篇——临时对象)分析过的临时对象有着很大的联系,可一并参考学习分析。

二、编译器的处理

编译器对返回值有两种,一种是基础类型(一般都小于8字节),通过寄存器(EAX or EDX)返回,速度快;第二个是自定义的数据类型或大于8字节的返回值,都通过栈返回。这里面的问题,其实就在后一种身上,通过栈空间返回,是需要进行内存拷贝的。而拷贝又分成两类,一种是有构造函数的,需要调用构造函数;另外一种是没有构造函数的(通常为基础类型或其复合体),则直接拷贝。

三、代码优化控制

在早期的编程技巧中,会对返回值做一些特别的处理,最简单的比如不返回大对象的值而通过引用来实现,返回指针等等。目的当然只有一个,就是尽量少的产生大对象的
复制。特别是在一些图像处理或者数据处理中,这些方法非常重要。但针对一些不太大的数据时,其实返回值优化的真正优势才展现出来。看一下最基础的返回值处理的代码:

#include <iostream>

class Object{
    public:
      Object(){std::cerr<<"call Object constructor function!"<<std::endl;}
      ~Object(){std::cerr<<"call Object destructor function!"<<std::endl;}
      Object(const Object&obj){std::cerr<<"call Object copy constructor function!"<<std::endl;}
      Object operator=(const Object&obj){
          std::cerr<<"call Object Assignment constructor ! "<<std::endl;
          return *this;
      }
};

Object getObj(){
Object obj;
return obj;
}
int main(){
Object obj;
obj = getObj();
return 0;
}

运行结果:

:~/test$ g++ -o rvo rvo.cpp
:~/test$ ./rvo
call Object constructor function!
call Object constructor function!
call Object Assignment constructor !
call Object copy constructor function!
call Object destructor function!
call Object destructor function!
call Object destructor function!

如果增加禁止优化(vs编译器请自行查看相关编译选项,不过似乎一直在启用)的选项再进行编译:

:~/test$ g++ -fno-elide-constructors -g -o rvo rvo.cpp
:~/test$ ./rvo
call Object constructor function!
call Object constructor function!
call Object copy constructor function!
call Object destructor function!
call Object Assignment constructor !
call Object copy constructor function!
call Object destructor function!
call Object destructor function!
call Object destructor function!

发现了什么不同呢?多了一次拷贝构造函数的调用和一次析构函数的调用。也就是说,在不优化的情况下,多生成了一个Object的对象实例。不要小看这一次,在很多时候儿可能就是性能的瓶颈。
从上面的代码可以看到返回值优化的实际情况,那么在编译器中是如何对返回值进行优化的呢?它分为两种情况:
1、RVO
返回值优化(Return Value Optimization),其实就是编译器对函数返回值通过某种判断来减少临时对象生成的数量从而达到对代码进行优化的技术。
2、NRVO
具名返回值优化(Named Return Value Optimization),象上面的例子,其实就是一个NRVO的例子,NRVO是一个RVO的更具体或者说更特定的情况。

上面的代码还能不能进一步优化?试着把代码中的Object的定义从声名定义改为直接创建:

Object getObj(){
//Object obj;
return Object();
}
int main(){
//Object obj;
Object obj = getObj();
return 0;
}

编译并运行,运行结果:

g++  -o rvo rvo.cpp
call Object constructor function!
call Object destructor function!

在编译器的眼中,代码已经变成了类似下面的代码:

int main(){
  Object();//其实就是把最终结果直接绑定到了临时对象上
  return 0;
}

在c++98到c++17之间,或者说较老编译器中,可能还存在着一个过渡的优化过程,即还是有一次赋值或者拷贝函数的调用。这个需要根据实际的环境来确定。编译器也是要跟着标准走的,首先需要明确这个前提。
从上面的分析可以看出,返回值优化的本质,其实就是对对象的构造(拷贝构造)函数和析构函数的调用开销的优化(或者说临时对象的控制和处理)。最简单的方法就是不要调用或者尽量少的调用。如果开发者本身或者编译器本身能够发现优化的代码那么这就是最大的成功。
另外,虽然在有些情况下不可以使用RVO,但可以通过一些手段来辅助实现,比如“计算性构造函数”,但这种方法属于一种不太优雅的实现方式,可能在某些确实需要性能的情况下,还是可以借鉴一下。相关的知识可以参考一些资料和书籍,此处就不再赘述。

四、代码优化失效

世界的事情从来都不是一面的,至少还有另外一个面。那么既然编译器会对返回值优化,是不是所有的返回值都会自动优化呢?答案肯定是否定的。在下面的几种情况下,就不会再进行RVO(NRVO)优化。
1、无法静态分析出返回结果情况
学习模板知识可以清楚,编译器其实对静态的把握程度远高于动态。而返回值也是如此,如果返回值返回的具体值需要由条件来进行限定,那么编译器就无法进行优化。这此条件包括:条件语句、多态和内联汇编引用返回值等。这里举一个最简单的条件语句限定。假设有三个相同类型的变量,不知道条件的情况下,编译器根本不知道返回哪个,那么就得老老实实的按情况来走,不能直接把某个变量直接替换成最终结果。比如下面的代码中,在函数中再增加几个相同类型变量,但需要根据具体的情况来返回相应的变量,此时就无法如上面那样的优化了。

Object getObj(){
Object obj;
Object obj1;
if (1){return obj1;}
return obj;
}

其执行结果:

call Object constructor function!
call Object constructor function!
call Object copy constructor function!
call Object destructor function!
call Object destructor function!
call Object destructor function!

2、返回全局变量
这个不是失效,是无法优化,因为全局变量就一个,没有可优化的余地。
3、返回成员变量
这种更类似于条件限制无法进行返回优化,因为编译器无法对内部参数进行详细的把控,这个成本太高了。
4、返回函数参数
其实函数参数意味着传入位置的不确定,这种动态性东西本身就不是编译器擅长的。所以其不进行优化正常。
5、赋值动作
即下面这种情况:

int main(){
Object obj;
obj = getObj();
return 0;
}

6、C++11后的移动语义
使用移动构造当然就不会再有什么多余的临时对象了,看下面的代码:

Object getObj(){
Object obj;
return std::move(obj);
}

换句话说,优化与不优化之于编译器来说,需要一个确定的手段,明确可确定的便有可能优化。从上面的分析还可以品出一点味道,为什么会出现移动语义和右值引用,就是为了提高资源的利用效率和性能。

五、总结

经过上面的分析,那么开发者到底是需要自己掌握一些优化的技巧和编程风格来控制优化还是依赖编译器来被动优化呢?其实二者并不矛盾。“在家靠父母,出门靠朋友”。但靠来靠去,都不如自己可靠 。外在条件和内在条件是相辅相成辩证统一的,但是内因决定外因。
故而,还是要对编译器处理有一定的了解,对优化自己心中有数。从根本上掌握优化的各个层次和角度,既要不断跟踪新的编译技术和编译工具,又要明白语言标准的发展方向。内外兼修,这才是根本。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值