您的位置:首页 > 编程语言 > C语言/C++

C++编译器优化-返回值优化

2015-04-25 19:57 99 查看
本文转自http://www.cnblogs.com/Azhu/archive/2012/07/14/2591489.html、http://bbs.csdn.net/topics/10268360、http://www.programlife.net/cpp-return-value-optimization.html和http://blog.sina.com.cn/s/blog_5dbb2c470100xapn.html,转载至此仅为方便学习参考。

(一)返回值优化(Return Value Optimization,简称RVO),是这么一种优化机制:当函数需要返回一个对象的时候,如果自己创建一个临时对象用户返回,那么这个临时对象会消耗一个构造函数(Constructor)的调用、一个复制构造函数的调用(Copy Constructor)以及一个析构函数(Destructor)的调用的代价。而如果稍微做一点优化,就可以将成本降低到一个构造函数的代价,下面是在Visual Studio 2008的Debug模式下做的一个测试:(在GCC下测试的时候可能编译器自己进行了RVO优化,看不到两种代码的区别)

// C++ Return Value Optimization
// 作者:代码疯子
// 博客:http://www.programlife.net/
#include <iostream>
using namespace std;

class Rational
{
public:
Rational(int numerator = 0, int denominator = 1) :
n(numerator), d(denominator)
{
cout << "Constructor Called..." << endl;
}
~Rational()
{
cout << "Destructor Called..." << endl;
}
Rational(const Rational& rhs)
{
this->d = rhs.d;
this->n = rhs.n;
cout << "Copy Constructor Called..." << endl;
}
int numerator() const { return n; }
int denominator() const { return d; }
private:
int n, d;
};

//const Rational operator*(const Rational& lhs,
//						 const Rational& rhs)
//{
//	return Rational(lhs.numerator() * rhs.numerator(),
//					lhs.denominator() * rhs.denominator());
//}

const Rational operator*(const Rational& lhs,
const Rational& rhs)
{
cout << "----------- Enter operator* -----------" << endl;
Rational tmp(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
cout << "----------- Leave operator* -----------" << endl;
return tmp;
}

int main(int argc, char **argv)
{
Rational x(1, 5), y(2, 9);
Rational z = x * y;
cout << "calc result: " << z.numerator()
<< "/" << z.denominator() << endl;

return 0;
}
Copyed From 程序人生
Home Page:http://www.programlife.net
Source URL:http://www.programlife.net/cpp-return-value-optimization.html
函数输出截图如下:

Return Value Optimization

可以看到消耗一个构造函数(Constructor)的调用、一个复制构造函数的调用(Copy Constructor)以及一个析构函数(Destructor)的调用的代价。

而如果把operator*换成另一种形式:

const Rational operator*(const Rational& lhs,
const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
Copyed From 程序人生
Home Page:http://www.programlife.net
Source URL:http://www.programlife.net/cpp-return-value-optimization.html
就只会消耗一个构造函数的成本了:

返回值优化

--------------------------------------------------------------------------------

以上参考链接:

Copyed From 程序人生

Home Page:http://www.programlife.net

Source URL:http://www.programlife.net/cpp-return-value-optimization.html

(二)返回值优化,是一种属于编译器的技术,它通过转换源代码和对象的创建来加快源代码的执行速度。

RVO = return value optimization。

class Complex//复数
{
friendd Complex operator + (const Complex & , const Complex&);
public:
Conplex(double r=0.0,double i= 0.0): real(r),imag(i){}
Complex(const Complex& a):real(a.real),imag(a.imag){};
Complex operator = (const Complex &a);
~Complex();
private:
double real;
double imag;
};


对于执行 A=B+C;

的时候,编译器在原函数创建一个临时变量,作为第三个参数传给 operator +(),使用引用传递,然后再将值赋给 A。

很多的编译器都实现了这样的优化,不过在程序编写的时候需要注意某些细节,才能让编译器执行这一技术。如:

//不能使用RVO
Complex operator +(const Complex & a, const Complex &b)
{
Complex retVal;
retVal.real=a.real +b.real;
retVal.imag=a.imag +b.imag;
return retVal;
}

//能够使用RVO
Complex operator +(const Complex & a, const Complex &b)
{
double r=a.real +b.real;
double i=a.imag +b.imag;
return Complex(r,i);
}


//不能使用RVO
Complex operator +(const Complex & a, const Complex &b)
{
Complex C(a.real +b.real,a.imag +b.imag);
return C;
}

//能够使用RVO
Complex operator +(const Complex & a, const Complex &b)
{
return C(a.real +b.real,a.imag +b.imag);
}


另外,必须定义拷贝构造函数来“打开”RVO

另外还有一种是通过 够着函数实现的,称 计算性构造函数

//计算性构造函数
Complex operator +(const Complex& a, const Complex &b)
{
return Complex(a,b);
}

Complex::Complex(const Complex &a ,const Complex &b):real(a.real +b.real),imag(a.imag +b.imag){}


要点:

1.如果必须按值返回函数,通过RVO可以省去创建和销毁局部对象的步骤。

2.RVO 的应用要遵照编译器的实现而定。

3.通过编写计算性函数可以更好的使用RVO。

(三)在[More Effective C++]条款22有关返回值优化的验证结果

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

在[More Effective C++]条款22的最后,在返回值的返回方式上,大师Meyers推荐使用表达式[return T(lhs)+=rhs;]这种使用匿名临时变量的方式,理由是“自古以来未具名对象总是比具名对象更容易被消除”,这种写法将更好地帮助编译器实现返回值优化(Return Value Optimization,简写RVO)。

针对上述说法,我在两款编译器上验证了一下(g++ 4.1.2,以下简称g++,及MS Visual C++ 2008,以下简称vc),结果并不如此。验证分三个阶段,具体结果如下。

(1)(不做加法计算,纯为RVO验证)

return T(lhs);

结果:g++和vc在不开编译优化选项的情况下,都进行了优化。即只会发生一次copy ctor。

(2)

return T(lhs) += rhs;

或者

T temp(lhs);

return temp += rhs;

结果:g++和vc即使开编译优化选项,也不会优化。即发生两次copy ctor,分别在temp对象的构造时与return时。

(3)

T temp(lhs);

temp += rhs;

return temp;

结果:g++无论开不开编译优化选项,都会优化,也就是说针对具名对象temp做了优化。结果只发生一次copy ctor。

vc在不开编译优化选项的情况下,不会优化,打开优化选项的情况下(比如release模式下)会做优化。

乍看(2)的第一种写法和(3)的写法对比,会产生奇怪的错觉:(3)的具名对象都可以优化,为什么(2)的匿名临时对象反而不会优化?其实看看(2)的第二种写法就明白了,问题不在于具名对象还是匿名对象,问题在于return语句上附带的操作。

为了搞清楚这个问题,我们需要先来了解一下返回值优化的原理。根据C++标准规定,具名对象也可以经由RVO被优化去除,那么编译器如何优化呢?按照(3)的流程,正常情况下(不优化)应该这样做:

T temp(lhs); //经由copy ctor创建一个temp对象

temp += rhs; //在temp上进行操作

return temp; //返回temp,即经由copy ctor将temp对象复制给外层的一个匿名临时对象(或某具名对象)

当编译器判断出函数的返回值是通过temp返回时,就可以将temp对象优化掉,而把在temp身上做的所有操作直接操做于外层的那个匿名临时变量身上,这样最后return所产生的copy操作就可以省掉了。

而在(2)的第二种写法中,temp就没办法被优化了,因为在return语句中,需要在temp对象上来做一个操作,编译器就只能老老实实的将temp构造出来,再以temp为参数做这个操作(调用相应函数),并将该函数返回结果再做一次copy ctor复制出去。(2)的第一种写法原理相同。

这个验证结果带给我们的结论就是,如果你定义了一个专用于存储返回值的临时对象(具名或者匿名),那么在返回的时候不要再做多余的操作。另外,如果存在返回不同具名对象的多个路径,编译器将没办法在编译期确定哪个对象会被作为返回值返回,那么编译器也无法完成返回值优化。还有一件事情需要小心,那就是在某些编译器上(比如vc)根据编译选项的不同程序的行为会不同(除了少调用一次copy ctor,还少一次临时变量的析构函数调用)。

当然,在优化这件事上,还需要慎重。为了完成返回值优化而硬写出难于理解的代码,甚至可能是效率更差的代码,就违反了我们的本意了。就像Meyers大师所说的,掌握好80-20原则,以及讲求证据的原则。

(四)其实我看<<深度探索>>的时候也没搞清楚这个问题,在那个帖子里我胡言乱语一番,

现在想把这个问题彻底弄清楚,现在把我对书上部分内容的理解写出来,抛砖引玉,

欢迎指正.有书的朋友见书上60-75页.

问题介绍:

拷贝构造函数:以一个class object作为另一个class object的初值时调用的构造函数.

本问题就是讨论编译器调用拷贝构造函数时的策略(如何优化以提高效率),侯捷称之为"程序转化的语义学"(PROGRAM TRANSFORMATION SEMANTICS).

讨论的例子,看下面的程序段:

X bar()//显然这个函数里的bar_x的作用是返回其值,用来为函数外部的一个对象赋值.

{

X bar_x ; // 构造函数bar_x

.... //处理bar_x

return bar_x; // 析够函数 bar_x

}

void foo( )

{

//这里希望有一个copy constructor xx=bar()

X xx=bar();

// ...

// 这里调用destructor xx

}

为什么要对这个程序段优化?怎么优化?

因为语句

X xx=bar();

中下面两个地方存在可改进的地方:

1.构造bar_x的作用仅仅是返回其值,用来为函数外部的对象赋值.

2.调用bar()函数,返回时存在临时对象构造,以及拷贝构造函数的调用.

为了说明这个问题,请看bar()函数的返回值是如何从局部对象bar_x中拷贝过来的,

(Stroustrup的cfront中的双阶段转化):

1.首先加上一个额外参数,类型是class object的一个reference,这个参数将用来放置被拷贝构造而得的返回值;

2.在return指令之前安插一个copy constructor的调用操作,以便将欲传回之object的内容当作上述新增参数的初值.

这样,后一个转化操作会重新改写函数,使他不传回任何值.

这样:

X bar()

{

X bar_x ; // 构造函数bar_x

.... //处理bar_x

return bar_x; // 析够函数 bar_x

}

被转化成了:

//函数转化的c++伪码;

//以反映copy constructor的调用.

void bar (X &_result) //_result就是加上的额外参数

{X bar_x;

bar_x.X::X();//编译器所产生的default constructor的调用操作

...//处理bar_x

_result.X::X(bar_x);//编译器所产生的copy constructor调用操作

return;

}

所以语句

X xx=bar();

编译时被转换成下列两个指令句:

X xx;//注意,不实行default constructor,xx是在bar()函数中构造的

bar(xx);

看到了吧,上面说bar()中存在的两个问题 :

1.构造bar_x的作用仅仅是返回其值,用来为函数外部的对象赋值.

2.调用bar()函数,返回时存在临时对象(bar-x)构造,以及拷贝构造函数(_result)的调用.

那么怎么来改进呢?

着眼点是抑制拷贝构造函数的调用.

方法一:在使用者层面做优化,主要是某人提出了"计算用"的构造函数,

也就是构造函数内存在对成员变量除了赋值以外的操作,直接计算_result,这里不讨论.

方法二:在编译器层面作优化.编译器在bar()函数中把bar_x直接以_result取代,

所以转化前:

X bar()

{

X bar_x ; // 构造函数bar_x

.... //处理bar_x

return bar_x; // 析够函数 bar_x

}

转化后:

void bar(X &_result)

{

_result.X::X();//default constructor 被调用

....//直接对_result处理

return;

}

看到了吗?没有了bar_x对象,也没有了拷贝构造函数的调用.

这样的话,文章开头部分的程序段:

X bar()

{

X bar_x ; // 构造函数bar_x

.... //处理bar_x

return bar_x; // 析够函数 bar_x

}

void foo( )

{

//这里希望有一个copy constructor xx=bar()

X xx=bar();

// ...

// 这里调用destructor xx

}

这样,伪码如下:

void bar( X & _result) // 在编译器层遍做优化

{

//并无X xx的定义,直接将_result代入后面的表达式

... //直接处理_result

return; //65页的载使用者层面做优化

}

void foo ( )

{

X xx_result ; //这里没有调用构造函数

bar( xx_result); //在函数bar()中对xx_result构造,处理.

// 析够 xx_result

}

进入正题:你承认速度变快了吗?

但是而书上却说“在此情况下,对称性被打破了,程序运行较快,却是错误的“!

为什么?

对称性是指什么?

错在哪里呢?

{

to:jinfeng_wang(一天只需来一次),别人可以跳过,

你认为xx_result对象是在语句( X xx_result;)中就构造的,但根据<<深度探索>>p64倒数

第6行特意指出xx_result不是这里构造的而是在bar()函数体里构造的,你可以看上面的伪码,摘自p64.

}

我开始理解对称性是指构造函数与析构函数个数要对称,现在根据上面的分析可以看出,

这里的"对称性"显然指构造函数与析构函数的位置不对称.

因为

xx_result的构造函数在bar()中调用.

而析构函数在foo()的末尾调用.

(请看上面的伪码).

(五)从C++的Return Value Optimization (RVO)到C#的value type

先看一段简单的C++代码:

Type get(int I){

return Type(i);

}

Type t = get(1);

这里, 我们从C++的基本语义看上去, 应该是Type(i) 调用一次拷贝构造函数, 在堆栈中生成一个临时对象;然后,用该对象构造返回对象;然后对这个临时对象调用析构函数;在调用者方, 用返回的临时对象调用拷贝构造函数以初始化对象t, 返回对象的析构函数在这之后, 函数返回之前调用。

所以, Type t = get(i); 应该有三个拷贝构造函数和两个析构函数的调用.

可是, 还有一种说法是, 编译器可能会对这两个临时对象进行优化,最终的优化结果会是只有一次的构造函数。因为很明显地可以看到, 这里我们其实只是要用一个整数构造一个Type对象。

嗯. 似乎很有道理!

那么, 哪一种说法对呢? 没有调查就没有发言权,于是本人用VC++6.0做了实验。 放了些cout<<…..在拷贝构造函数里,观察打印的结果, 结果却是跟我的simple, naïve的预测一致。三个拷贝构造函数, 两个析构函数。

“你个弱智编译器!脑袋进水了吧?”(忘了编译器没脑袋了)“很明显在这个例子里我的两个临时对象都没有用的啊!”

于是,上网, 查资料, Google一下吧!

下面是我查到的一些结果:

其实, 这种对值传递的优化的研究, 并不只局限于返回值。对下面这个例子:

void f(T t){}

void main(){

T t1;

f(t1);

}

也有这种考虑。

f(T)是按值传递的。语义上应该做一个复制, 使得函数内部对T的改变不会影响到原来的t1.

但是,因为在调用f(t1)之后, 我们没有再使用t1(除了一个隐含的destructor调用),是否可能把复制优化掉, 直接使用t1呢?这样可以节省掉一个拷贝构造函数和一个析构函数。

可是, 不论是对返回值的优化, 还是对上面这种局部对象的优化,在1995年的C++新标准草案出台前都是为标准所严格限制的 (虽然有些编译器并没有遵行这个标准, 还是支持了这种“优化”)

那么, 这又是为什么呢?

这里面涉及到一个普遍的对side-effect的担忧。

什么又是side-effect呢?

所谓side-effect就是一个函数的调用与否能够对系统的状态造成区别。

int add(int I, int j){return I+j;}就是没有side-effect的,而

void set(int* p, int I, int v){p[I]=v;}就是有side-effect的。因为它改变了一个数组元素的值, 而这个数组元素在函数外是可见的。

通常意义上来说, 所有的优化应该在不影响程序的可观察行为的基础上进行的。否则,快则快了, 结果却和所想要的完全不同!

而C++的拷贝构造函数和析构函数又很多都是有side-effect的。如果我们的“优化”去掉了一个有side-effect的拷贝构造函数和一个析构函数, 这个“优化”就有可能改变程序的可观察行为。(注意, 我这里说的是“可能”,因为“负负得正”, 两个有side-effect的函数的调用, 在不考虑并行运行的情况下, 也许反而不会影响程序的可观察行为。不过, 这种塞翁失马的巧合, 编译器就很难判断了)

基于这种忧虑, 1995年以前的标准, 明确禁止对含有side-effect的拷贝构造函数和析构函数的优化。同时, 还有一些对C++扩充的提议, 考虑让程序员自己对类进行允许优化的声明。 程序员可以明确地告诉编译器:不错, 我这个拷贝构造函数, 析构函数是有side-effect, 但你别管, 尽管优化, 出了事有我呢!

哎, side-effect真是一个让人又恨又爱的东西!它使编译器的优化变得困难;加大了程序维护和调试的难度。因此functional language 把side-effect当作洪水猛兽一样,干脆禁止。但同时,我们又很难离开side-effect. 不说程序员们更习惯于imperative 的编程方法, 象数据库操作,IO操作都天然就是side-effect.

不过,个人还是认为C++标准对“优化”的保守态度是有道理的。无论如何,让“优化”可以潜在地偷偷地改变程序的行为总是让人想起来就不舒服的。

但是, 矛盾是对立统一的。(想当年俺马列可得了八十多分呢)。 对这种aggressive的“优化”的呼声是一浪高过一浪。 以Stan Lippeman为首的一小撮顽固分子对标准的颠覆和和平演变的阴谋从来就没有停止过。 这不?在1996年的一个风雨交加的夜晚, 一个阴险的C++新标准草案出炉了。在这个草案里, 加入了一个名为RVO (Return Value Optimization) 的放宽对优化的限制, 妄图走资本主义道路, 给资本家张目的提案。其具体内容就是说:允许编译器对命名过的局部对象的返回进行优化, 即使拷贝构造函数/析构函数有side-effect也在所不惜。这个提议背后所隐藏的思想就是:为了提高效率, 宁可冒改变程序行为的风险。宁要资本主义的苗, 不要社会主义的草了!

我想, 这样的一个罪大恶极的提案竟会被提交,应该是因为C++的值拷贝的语义的效率实在太“妈妈的”了。 当你写一个 Complex operator+(const Complex& c1, const Complex& c2);的时候, 竟需要调用好几次拷贝构造函数和析构函数!同志们!(沉痛地, 语重心长地)社会主义的生产关系的优越性怎么体现啊?

接下来, 当我想Google C++最新的标准, 看RVO是否被最终采纳时, 却什么也找不到了。 到ANSI的网站上去, 居然要付钱才能download文档。 “老子在城里下馆子都不付钱, down你几个烂文档还要给钱?!”

故事没有结局, 实在是不爽。 也不知是不是因为标准还没有敲定, 所以VC++6 就没有优化, 还是VC根本就没完全遵守标准。

不过,有一点是肯定的。 当写程序的时候, 最好不要依赖于RVO (有人, 象Stan Lippeman, 又叫它NRV优化)。 因为, 不论对标准的争论是否已经有了结果, 实际上各个编译器的实现仍还是各自为政, 没有统一。 一个叫Scott Meyers的家伙(忘了是卖什么的了)就说, 如果你的程序依赖于RVO, 最好去掉这种依赖。也就是说, 不管RVO到底标准不标准, 你还是不能用。 不仅不能用, 还得时刻警惕着RVO可能带来的程序行为上的变化。 (也不知这帮家伙瞎忙了半天到底为啥!)

说到这里, 倒想起了C#里一个困惑了我很久的问题。记得读C#的specification的时候, 非常不解为什么C#不允许给value type 定义析构函数。

这里, 先简略介绍一下C#里的value type (原始数据类型, struct 类型)。

在C#里的value_type就象是值, 永远只能copy, 取值。因此, 它永远是in-place的。如果你把一个value type的数据放在一个对象里,它的生命期就和那个对象相同;如果你声明一个value type 的变量在函数中, 它的生命期就在lexical scope里。

{

The_ValueType value;

}//value 到这里就死菜了

啊呀呀! 这不正是我们怀念的C++的stack object吗?

在C++里,Auto_ptr, shared_ptr, 容器们, 不都是利用析构函数来管理资源的吗?

C#,java 虽然利用garbage collection技术来收集无用对象, 使我们不用再担心内存的回收。 但garbage collection并不保证无用对象一定被收集, 并不保证Dispose()函数一定被调用, 更不保证一个对象什么时候被回收。 所以对一些非内存的资源, 象数据库连接, 网络连接, 我们还是希望能有一个类似于smart pointer的东西来帮我们管理啊。(try-finally 虽然可以用, 但因为它影响到lexical scope, 有时用起来不那么方便)

于是, 我对C#的取消value type的析构函数充满了阶级仇恨。

不过, 现在想来, C#的这种设计一定是惩于C++失败的教训:

1. value type 没有拷贝构造函数。C#只做缺省copy, 没有side-effect

2. value type 不准有析构函数。C#有garbage collection, 析构函数的唯一用途只会是做一些side-effect象关闭数据库连接。 所以取消了析构函数, 就取消了value type的side-effect.

3. 没有了side-effect, 系统可以任意地做优化了

对以下程序:

The_Valuetype get(int I){return The_Valuetype(i);}

The_Valuetype t = get(1);

在C#里我们可以快乐地说:只调用了一次构造函数。 再没有side-effect的沙漠, 再没有难以优化的荒原, smart pointer望而却步, 效率之花处处开遍。 I have a dream, ……
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: