天秤座的唐风

总会有一个人需要你的分享~!- 唐风 -

  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  13 随笔 :: 0 文章 :: 69 评论 :: 0 Trackbacks

与临时对象的斗争(上)

作者:唐风

原载:www.cnblogs.com/liyiwen

C++ 是一门以效率见长的语言(虽然近来越来越多的人“不齿”谈及效率,我深以为不然,在某一次的程序编写中不对效率锱铢必较并不意味意味着我们就不应该追求更多的更好的做法)。总之吧,相比起其它语言,程序员们在使 C++ 的时候会更加有意识地去避免没有效率的做法。在C++ 的程序中,临时对象的产生就是损及效率的“恶因”之一,因此也产生出一些意思的技术和优化手段,这篇文章里我总结一下最近在这些方面学习的一些收获:

返回值优化(RVO)与具命返回值优化(NRVO)

这是一项编译器做的优化,已经是一种很常见的优化手段了,放狗搜一下可以找到很多的资料,在 MSDN 里也有相关的说明。

返回值优化,顾名思义,就是与返回值有关的优化(废话……),是当函数是按值返回(而不是引用啊、指针啊)时,为了避免产生不必要的临时对象以及值拷贝而进行的优化。

先看看下面的代码:

typedef unsigned int UINT32;

class MyCla
{
public:
    MyCla(UINT32 a_size = 10):size(a_size) {
        p = new UINT32[size];        
    }
    MyCla(MyCla const & a_right):size(a_right.size) {
        p = new UINT32[size];
        memcpy(p, a_right.p, size*sizeof(UINT32));
    }
    MyCla const& operator = (MyCla const & a_right) {
        size = a_right.size;
        p = new UINT32[size];
        memcpy(p, a_right.p, size*sizeof(UINT32));
        return *this;
    }
    ~MyCla() {
        delete [] p;
    }
private:
    UINT32 *p;
    UINT32 size;
};

MyCla TestFun() {
    return MyCla();
}

int _tmain(int argc, _TCHAR* argv[])
{
    MyCla a = TestFun();
    
    return 0;
}

TestFun() 函数返回了一个 MyCla 对象,而且是按值传递的。

在没有任何“优化”之前,这段代码的行为也许是这样的:return MyCla() 这行代码中,构造了一个 MyCla 类的临时的无名对象(姑且叫它t1),接着把 t1 拷贝到另一块临时对象 t2(不在栈上),然后函数保存好 t2 的地址(放在 eax 寄存器中)后返回,TestFun 的栈区间被“撤消”(这时 t1 也就“没有”了,t1 的生存域在 TestFun 中,所以被析构了),在 MyCla a = TestFun(); 这一句中,a 利用 t2 的地址,可以找到 t2 进行,接着进行构造。这样 a 的构造过程就完成了。然后再把 t2 也“干掉”。

可以看到,在这个过程中,t1 和 t2 这两个临时的对象的存在实在是很浪费的,占用空间不说,关键是他们都只是为a的构造而存在,a构造完了之后生命也就终结了。既然这两个临时的对象对于程序员来说根本就“看不到、摸不着”(匿名对象嘛,你怎么引用?),于是编译器干脆在里面做点手脚,不生成它们!怎么做呢?很简单,编译器“偷偷地”在我们写的fun函数中增加一个参数 A&,然后把 a 的地址传进去(注意,这个时候 a 的内存空间已经存在了,但对象还没有被“构造”,也就是构造函数还没有被调用),然后在函数体内部,直接用 a 来代替原来的“匿名对象”,在函数体内部就完成 a 的构造。这样,就省下了两个临时变量的开销。这就是所谓的“返回值优化”~!在 VC7 里,按值返回匿名对象时,默认都是这么做。

上面说的是“返回值优化(RVO)”,还有一种“具名返回值优化(NRVO)”,是对于按值返回“具名对象”(就是有名字的变量!)时的优化手段,其实道理是一样的,但由于返回的值是具名变量,情况会复杂很多,所以,能执行优化的条件更苛刻,在下面三种情况下(来自MSDN),NRVO 将一定不起作用:

  1. 不同的返回路径上返回不同名的对象(比如if XXX 的时候返回x,else的时候返回y)
  2. 引入 EH 状态的多个返回路径(就算所有的路径上返回的都是同一个具名对象)
  3. 在内联asm语句中引用了返回的对象名。

不过就算 NRVO 不能进行,在上面的描述中的 t2 这个临时变量也不会产生,对于 VC 的 C++ 编译器来说,只要你写的程序是把对象按值返回的,它会有两种做法,来避免 t2 的产生。拿下面这个程序来说明:

MyCla TestFun2() {
    MyCla x(3);
    return x;
}

一种做法是像 RVO一样,把作为表达式中获取返回值来进行构造的变量 a 当成一个引用参数传入函数中,然后在返回语句之前,用要返回的那个变量来拷贝构造 a,然后再把这个变量析构,函数返回原调用点,a 就构造好了。

还有一种方式,是在函数返回的时候,不析构 x ,而直接把 x 的地址放到 exa 寄存器中,返回调到 TestFun2 的调用点上,这时,a 可以用 exa 中存着的地址来进行构造,a 构造完成之后,再析构原来的变量 x !是的,注意到其实这时,x 的生存域已经超出了 TestFun2,但由于这里 x 所在 TestFun2 的栈虽然已经无效,但是并没有谁去擦写这块存,所以 x 其实还是有效的,当然,一切都在汇编的层面,对于 C++ 语言层面来讲是透明的。

嗯,(具名)返回值引用大约就是这么多,在网上和 MSDN 上还能查到更多的例子和解释,对于在多线程下  (N)RVO 需要注意什么,嗯,我完全没有多线程的经验,不敢乱写误人子弟……

右值引用与 move 语意

“C++ 中临时对象对效率产生的影响一直为人所诟病”(网上流传的说法),NRVO 等手段也只有在一定程度上弥补这个不足(你知道,在很多情况下无法做优化)。在 C++98 确定后的十多年时间后,“Cpper神圣”们终于给出了另一个对付它的法宝——右值引用。

对于右值引用,目前我所见过的最好的讲解是VC开发团队blog中发布的一篇长文(看这里),在CPP blog上飘飘白云的博主进行了全文翻译(译得很棒),建议细读三遍!理解里面每一个例子~这样至少你在右值引用的认识上就有了良好的基础了。(嗯,我只读了两遍,下面说的东西有错误的话请原谅并指出 :) )

简单的说,在C++中的左值,就是能取地址的表达式,比如var、++var之类的,右值就不是能取地址的表达式啦,比如常数 123、x++、x+y等等。

嗯,我们可以看到,右值常常就代表着临时对象,也就常常意味着“被诟病的浪费……”

比如,z = x + y,这里,翻译得更“低层”一点,那么这里将是:

temp = x + y
z = temp

这个temp是很尴尬的,不用它将无法实现正确、良好的 operator + 语意,用它就很难避免临时对象产生的不良开销。

我们回到上面 RVO 中的程序例子:

MyCla TestFun() {
    return MyCla();
}

看,这里返回的 MyCla(),正是一个右值(我们就给它取个名吧,不然不好称呼它,嗯,还叫 t1 吧)。在函数返回后,这个 t1 就被析构,它做的析构动作就是把原来申请的内存还给系统。想想在这之前,a 在干什么?a 在构造的时候向系统申请了一块内存!一个申请,一个还回,一来一回多费事啊,如果能直接把 t1 拥有的内存给 a ,就不省事了吗?反正 t1 马上就要挂了。好,右值引用给了我们这种机会,我们为 MyCla 实现一个 move 语意的拷贝构造函数(不知道什么是 move 拷贝构造?回头看上面链接的文章三遍!):

MyCla(MyCla && a_right):size(a_right.size) {
    p = a_right.p;
    a_right.p = NULL;        
}

当编译器探知用于构造 a 的是一个右值时,就调用这个 move 构造函数,然后我们在这个函数里偷梁换柱,把 t1 的资源窃取过来了。这样,就算不使用 RVO,这个构造的开销也是非常小的。

那么,对于像:

MyCla TestFun2() {
    MyCla x(3);
    return x;
}

这样的情况呢?是的,这里的 x 是一个左值,不会调用 move 构造函数。可是我们知道这个 x 其实马上也要挂了,它的资源不给白不给啊对不对?所以,我们就想告诉编译器,您就把它当成个右值吧,怎么告诉它呢?用 std::move 来实现这种 move 语意,像下面这样:

MyCla TestFun2() {
    MyCla x(3);
    return std::move(x);
}
好啦,这样又能用上 MyCla 的 move 构造函数啦。

总结一下,作为右值的临时对象,其实它的存在就是充当一个传递的桥梁,一旦表达式过了这个桥,那么这个临时对象的存在就没有意义了,也没有人能再用到它(因为它是个右值,没有名字,又不能取地址)。既然如此,一个无人问津的就要“死”的变量,把它拥的的资源抢过来也不算过份吧……。在 C++0x 之前,我们想这么做,但是没有手段,虽然编译器能分清楚左值右值,但我们无法通过程序告诉编译器,如果这是左值,请用这个方法,这个是右值,嘿嘿,那用另一个方法帮我抢它的资源吧……,。到了 C++0x ,我们有手段了,那就是右值引用,这个右值引用可以参与函数的重载,这样就给了我们机会,针对左右值分别提供不同的操作方法(函数)让编译器帮我们选择一个合适的。

一般来说,可能需要注意右值引用的地方有:

当我们写的类里拥有动态申请的资源时,那么总是应该提供一个move构造函数,这将会带给很多好处,可以让这个类的使用者(一般是我们自己函数,或是SDL等库)利用它来提升效率。

如果我们写的函数需要利用传入的(含有动态申请资源的)对象参数来构造新的变量时,我们可以提供右值引用的重载版本,并在构造新对象时使用std::move来窃取临时对象的资源。

右值引用在泛型编程中也有极为重要的作用(它能实现完美转发),但和本文没多大关系,就不多说了。

总之,右值引用是 C++0x 中非常耀眼的一个新的语言特性,VC2010已经将其列入支持范围(GCC 本人几乎没用过,没了解,不敢妄言[注{ThanksTo OwnWaterloo}:gcc新版本也支持了。 gcc4.4.0 的stl已经加上对move的支持了])。

从实践的角度讲,它能够完美地解决 C++ 中长久以来为人们所诟病的临时对象的效率问题。从语言本身来讲,它健全了 C++ 中引用类型在左值右值方面原先的缺陷,从库的设计者角度讲,它给设计者又带来了一把利器。而对于广大的库使用者而言,不动一兵一卒便能获得“免费”的效率提升。

牛吧!这个特性如此重要如此有用,几乎可以想见在支持右值的编译器一旦实用化,就将产生大量的使用右值引用特性代码和相关的idioms,也可能会遇到和这个相关的bug,一句话,趁早学吧,出来混,总是会碰上的……。

 

(上篇完,下篇将分析 Expression Template 在消除临时变量中的作用,以及对三种方法进行一个总结)

posted on 2009-12-02 22:11 唐风 阅读(2749) 评论(15)  编辑 收藏 引用 所属分类: 语言技术

评论

# re: 与临时对象的斗争(上) 2009-12-02 22:50 OwnWaterloo
gcc新版本也支持了。 gcc4.4.0 的stl已经加上对move的支持了。

没有右值引用,也可以消除很多临时变量,只是编程很复杂……
需要使用一些proxy,用来"记录""操作与操作数",仅仅是"记录"。
只有当出现操作的"接收者"时,操作才被真正执行,直接在接收者上进行操作了。

当然,有move更好,本来就应该是这样,对立马就要消亡的对象,盗取一些资源是很合理的……
只是不知道c++0x要什么时候才能流行起来……

看看现在还有N多用vc6的人…… 无语……


  回复  更多评论
  

# re: 与临时对象的斗争(上) 2009-12-02 22:58 OwnWaterloo
lambda,aotu,decltype这些都是很有用的特性,没它们有时候真的是相当的不方便……

而且,lambda完全可以很简单的纯手工模拟一个。
这种毫无新意的机械复制的事情,本来就应该交给编译器去做。
编译器实现lambda表达式是不需要花什么力气的。
只是解析可能会出现麻烦……

auto、decltype更是容易。
其实编译器已经实现了它们,只是没有暴露出来而已。

期待c++0x流行啊……

  回复  更多评论
  

# re: 与临时对象的斗争(上) 2009-12-02 23:17 non
c++0x过于复杂,在大的工程上保守派不敢上。。甚至于stl的都要求少用  回复  更多评论
  

# re: 与临时对象的斗争(上) 2009-12-02 23:18 唐风
话说 VC6 还真有点像 IE6 ,拥有极高的使用率,但又不能完全支持“标准”,呵呵,不过 VC6 可以原谅,毕竟开发的时候 C++ 标准还没出台嘛……

唉,托 D 版的福,我可是每发布一个新版本就立即升级的,呵呵,不过在公司做项目又没办法了,得听公司技术决策者的,呵,而我们公司那个大牛,MS 习惯 VC6 ……。

我也很期待 C++0x 呢,对 C++ 很有感情,哈哈

对于
【没有右值引用,也可以消除很多临时变量,只是编程很复杂……】
是啊,但通过这些途径,完成了一些“目标”之后仍然会觉得心中有缺憾,不能用最优雅的方式解决问题的时候总会不舒服,
就像 Expression Template 被开发出来,我想也是人们想有效率,又想直观的结果吧……

PS:
您还真是快啊~~我这才发布,你就来了。神仙~  回复  更多评论
  

# re: 与临时对象的斗争(上) 2009-12-02 23:29 唐风
@non
是有听这么说的。
不过我总觉得:不鼓励或是要求禁用 STL 的组织,肯定得要有牛人实现一套更合适于他们工程的基本类库,也许他们只是不想要通用的 STL 实现,但 STL 做的那些事,始终还是需要有“人”来做的。
  回复  更多评论
  

# re: 与临时对象的斗争(上) 2009-12-02 23:51 OwnWaterloo
@唐风
DB是什么???

对那些保守党,就任由他们去吧……
让他们在自己的世界里自娱自乐,一次又一次的发明那属于自己心中完美轮子。
都复用别人的,他们还怎么好开口向老板要钱啊?
一定要说:stl对我们的项目都是不适合的! —— 以显得自己的项目很牛逼。
然后追加:所以我们自行开发了xxx! —— 以显得自己很牛逼。


  回复  更多评论
  

# re: 与临时对象的斗争(上) 2009-12-03 00:06 唐风
@OwnWaterloo
呃,不好意思,我是想说“盗版”,呵呵
回想,DB 确实容易联想成 DataBase,那这句话就不好理解了……
It's my fault :)


  回复  更多评论
  

# re: 与临时对象的斗争(上) 2009-12-03 00:27 OwnWaterloo
@唐风
机器上的vs2005和2008是学校给的…… 据说有个什么政策,学校每年只用付3000元就可以使用大量的正版软件。
不过…… 我已经毕业了…… 机器上的还没删…… 继续用着……
还去下了一个vc10精简版…… 这肯定是D版了……

据说vc9有免费的,不含ide。

  回复  更多评论
  

# re: 与临时对象的斗争(上) 2009-12-03 13:36 空明流转
@OwnWaterloo
从2003开始,VS就有Express Edition了,不过仅用于开发非商业授权的软件。  回复  更多评论
  

# re: 与临时对象的斗争(上) 2009-12-03 16:33 OwnWaterloo
@空明流转
嗯,谢谢~


如果我只是想用VC的编译器测试一下可移植性。
但并不发布VC生成的binary。
这样可以么?


或者,我开发的东西使用的是new BSD或者LGPL之类的许可证,可以使用VC express么?

  回复  更多评论
  

# re: 与临时对象的斗争(上) 2009-12-03 16:40 OwnWaterloo
@空明流转
上面好像没说清楚…… 我整理一下……

1. 开发的软件使用商业许可证。
发布的binary使用的是,比如mingw,生成的。
也发布源代码。
但发布前使用VC express作移植性测试。
当然,还包括VC使用的工程文件也会发布。
有VC授权的人,可以自己使用VC编译。

这样算侵权么?

2. 开发的软件使用非商业许可证,比如new BSD或LGPL
发布源代码,VC工程文件。侵权么?
发布VC编译的binary呢?

  回复  更多评论
  

# re: 与临时对象的斗争(上) 2009-12-04 12:30 YESHG!
关于这篇的俺的第一篇回复呢?这里是不是会评论失败?
  回复  更多评论
  

# re: 与临时对象的斗争(上) 2009-12-06 22:25 唐风
@YESHG!
你也注册一个博客园(或是cppblog)的帐户呗
对评论和回复有邮件通知,这个功能挺好用的。  回复  更多评论
  

# re: 与临时对象的斗争(上) 2010-03-08 20:50 Jakcie
写的不错。
我现在用2005。其实VC6,里面Bug还是不少。尤其是MFC里面。2010都出了,至少该用2005吧。
免费版也有IDE,但没有很多高级功能和资源编辑器。  回复  更多评论
  

# re: 与临时对象的斗争(上) 2013-02-28 16:51 refugee
MyCla const& operator = (MyCla const & a_right) {
size = a_right.size;
p = new UINT32[size];
memcpy(p, a_right.p, size*sizeof(UINT32));
return *this;
}
这个赋值重载有点问题,一、没做自赋值检查;二、没释放原有空间,内存泄露(size一致的可以不释放,也不用new新空间,直接copy);三、返回值是否有必要用const限制,表达式(a=b)=c不能成立,虽然这表达式本身就有点脑残。  回复  更多评论
  


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理