<<深度探索C++对象模型>>的第70页关于编译器对拷贝构造函数优化的问题再讨论~~

alidiedie 2002-06-20 10:19:03
首先请看jinfeng_wang的帖子:
http://www.csdn.net/expert/topic/806/806937.xml?temp=.2044489
其实我看<<深度探索>>的时候也没搞清楚这个问题,在那个帖子里我胡言乱语一番,
现在想把这个问题彻底弄清楚,现在把我对书上部分内容的理解写出来,抛砖引玉,
欢迎指正.有书的朋友见书上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()的末尾调用.
(请看上面的伪码).














...全文
702 23 打赏 收藏 转发到动态 举报
写回复
用AI写文章
23 条回复
切换为时间正序
请发表友善的回复…
发表回复
alidiedie 2002-07-12
  • 打赏
  • 举报
回复
侯捷网站上的大讨论:|C++ Object Model 答客问 (3) - NRV 最佳化
2000.03.25

leetron wrote (2000/02/11) :

> 您好,关於深度探索C++物件模型一书,2.3节程式
> 转化语意学的部分,有个问题
> 想请教您。
>
> 问题:
> 在67页,最下面两行:
> 这个程式的第一个版本不能实施NRV最佳化,因为test class
> 缺少一个copy constructor。
> 但是在66页「在编译器层面做最佳化」那一段中所列的码显示,
> 当编译器把xx以__result取代,变成__result.X::X();
> 即default constructor被唤起。唤起default constructor
> 是可以理解的,可是编译器转换後的码并没有使用到
> copy constructor呀,为什麽67页最後两行却说缺少一个
> copy constructor,就不能实施这个最佳化了呢?
>
> 我对上面这个问题做了些解释,但不知我的猜想是否正确。
>
> 我的解释是:
> 如同63页与64页「回返值的初始化」这一段,编译器可能将
> 63页下面的 X bar()函式定义转换成64页的虚拟码,其中有
> 一行__result.X::X(xx); 这会使用到copy constructor。
>
> 转换成64页的码後,65页与66页分述了两种後续可能出现的
> 最佳化动作,其中一种即是66页的编译器层面做最佳化。
> 如此,虽然66页最佳化後的码看起来并不使用到copy constructor,
> 但是这些码是根据像64页那种样子的码(注一)最佳化而来的,
> 而若没有copy constructor,根本无法转换成64页那种虚拟码,
> 因为其中有一个呼叫copy constructor的动作。所以,虽然
> 66页经过编译器最佳化的结果省去了__result.X::X(xx);
> 这个copy constructor的呼唤动作(因为根本没有xx了),
> 但若没有明白提供一个copy constructor,却无法让编译器
> 进行这样的最佳化。
>
> 另一方面,我叁考第5章,205页最下面一段话:
> 「一般而言如果你的设计之中,有许多函式都需要以传值(by value)
> 传回一个local class object....那麽提供一个copy constructor
> 就比较合理--甚至即使default memberwise语意已经足够。
> 它的出现会触发NRV最佳化。然而,就像我在前一个例子中
> 所展现的那样,NRV最佳化後将不再需要唤起copy constructor,
> 因为运算结果已经被直接计算於「将被传回的object」体内了。」
> 所以,我提出如上所述那个解释,但不确定是否正确,所
> 以e-mail给您以确认一下。
>
> 注一:当然,编译器到底怎麽实作这些转换动作,理论上
> 我们是未知的,不能一概而论。所以我写「像64页那种样子的码」。


侯捷回覆:

我最害怕的事情就是,读者写信来讨论《深度探索 C++ 物件模型》
一书内容。因为都是些高手提出些深奥的题目,而我必须把尘封的
记忆找出来┅ :)

OK,作者(或译者)没有抱怨的权利 :)。我的回覆如下。

首先,我要说 leetron 把他的意思描述得非常清楚。在我收到
的读者来函中,算是上品 ─ 尤其是描述这麽复杂的思路。

其次,我同意 leetron 说:

> 转换成64页的码後,65页与66页分述了两种後续可能出现的
> 最佳化动作,其中一种即是66页的编译器层面做最佳化。

但是我不同意 leetron 这样的看法:

> 如此,虽然66页最佳化後的码看起来并不使用到copy constructor,
> 但是这些码是根据像64页那种样子的码(注一)最佳化而来的,

我认为,NRV 最佳化并非是由 p63 的原始码而至 p64 的虚拟码,
再至 p66 的最佳化。我认为是从 p63 的原始码直接至 p66 的最佳化。
所以,似乎可以不需要 copy ctor。

但这麽一来我也无法解释为什麽 lippman 在 p67 最下强调
「必须要有 copy ctor 才能实施 NRV 最佳化」。

以下是其他读者的讨论,给您叁考。

-- quote -- (引自 www.jjhou.com :书籍勘误/深度探索 C++ 物件模型)

★黄俊达先生认为:Lippman 在 p67 最後一行所言『这个程式的第一个版本
不能实施 NRV 最佳化,因为 test class 缺少一个 copy constructor』,
此语错误。黄先生认为如果程式没有 explicit copy constructor,编译器会
自动为我们做出来(如为 trivial,则直接 bitwise copy;如为 nontrivial,
则由编译器为我们合成出一个 copy constructor)。因此,有没有 explicit
copy constructor 并不影响 NRV 最佳化的实施。他认为 NRV 最佳化主要是
由编译器 option 来决定要不要实施。他并且做了一些实验,判断 VC 和 gcc
都没有做到 NRV 最佳化,而其不做的理由不是因为技术上的困难,是为了
避免造成「user defined copy constructor 之副作用失效」-- 所谓副作用
是指,例如「在 user defined copy constructor 中做一个 cout 输出」之类
这种「与 memberwise copy 无关」的动作。

★侯俊杰答覆:颇有道理。但请注意,Lippman
在 p.205 下方, p.221 上方等处,仍再三强调 copy constructor
对於 NRV 最佳化的导引之功,不知是否其间有什麽是我们没想到的?

★唐志青先生亦来信对於 NRV 提出与黄俊达先生相同的看法。感谢。
-- unquote --

-- the end




alidiedie 2002-06-29
  • 打赏
  • 举报
回复
侯捷在深度探索上也对vc环境下测试了,结果nrv更慢,候推测vc编译器不支持nrv。
kingofark 2002-06-29
  • 打赏
  • 举报
回复
说些题外话:
我用PIII866/256Mb的机子按照简体版P.69页jjhou给出的代码测试了一下:

Inside the cpp object model:
P.69

[说明]:
m代表分钟,s代表秒;
No_NRV表示没有copy-ctor,预想中的“不做NRV优化”;
With_NRV代表提供copy-ctor,预想中似乎应该“做NRV优化”;
NRV+-O2代表使用-O2(for speed)编译的With_NRV;

cnt<10000000 (这是书中的情况)
_______No_NRV____With_NRV____NRV+-O2____
vc6 16s 17s 7s
vc7 9s 10s <1s (!?)
bcb5 10s 10s 10s


cnt<100000000 (再加一个0的情况)
_______No_NRV____With_NRV____NRV+-O2____
vc6 2m34s 2m43s 1m7s
vc7 1m37s 1m42s <1s (!?)
bcb5 1m36s 1m43s 1m39s


无论如何看不出来NRV优化的影子;且vc7用cl -O2 编译后,居然被优化优得小于1s。看上去bcb5对代码处理得更好。不知道通过查看汇编代码能不能看出什么有意义的东西出来。

我比较菜,要是说错了,请大家指正。
anrxhzh 2002-06-26
  • 打赏
  • 举报
回复
第一点,我相信编译器不会干坏事,RVO 不会对程序产生任何副作用。
第二点,C++标准对是否实现RVO或者如何实现RVO没有做任何规定,RVO只是一个实现细节,程序员可以了解它,不可以依赖它。
第三点,实际的例子是最有说服力的,任何书本都可能有错误。

下面是VC6.0上的一个例子,不依赖任何优化开关。

#include <iostream>

struct RVO{
RVO(){std::cout<<"RVO()\n";}
RVO(int){std::cout<<"RVO(int)\n";}
RVO(const RVO&){std::cout<<"RVO(RVO)\n";}
~RVO(){std::cout<<"~RVO()\n";}
};

RVO get(int i=0)
{
return RVO(i);
}

struct NO_RVO{
NO_RVO(){std::cout<<"NO_RVO()\n";}
NO_RVO(char){std::cout<<"NO_RVO(int)\n";}
NO_RVO(const NO_RVO&){std::cout<<"NO_RVO(NO_RVO)\n";}
//~NO_RVO(){std::cout<<"~NO_RVO()\n";}
};

NO_RVO get1(int i=0)
{
return NO_RVO(i);
}

int main()
{
{
RVO t = get();
}
std::cout << std::endl;
{
NO_RVO t1 = get1();
}
}

//output:
//RVO(int)
//~RVO()

//NO_RVO(int)
//NO_RVO(NO_RVO)
//NO_RVO(NO_RVO)
alidiedie 2002-06-26
  • 打赏
  • 举报
回复
faint.this computer dosen't have chinese input(shu ru fa ).
so i have to express it in english.
i can't understand what you have written here .so i will download to a softdisk,and bring it back home,carefully think over them.
alidiedie 2002-06-24
  • 打赏
  • 举报
回复
to:飞鸟
“另外我对书上说得要有copy构造函数存在才能激发nrv优化感到不可理解
我也看了侯捷网站上的讨论,大家都觉的这是错的,可是stan在书中好像
有好几处一再提到此点,不知对此点大家有什么看法。”
我觉得要有copy构造函数的存在才能激发nrv优化很好理解。
因为nrv优化的目的就是编译器调用拷贝构造函数时的优化策略之一。


ajoo 2002-06-23
  • 打赏
  • 举报
回复
编译器的优化必须在保证我们的程序的运行结果和我们预计的一致的前提下 才能进行
that's what I thought and agreed with. It's also what the old standard was defined.

But seems the new proposal of NRV is allowing the NRV even at the cost of changing the program behavior.

ajoo 2002-06-23
  • 打赏
  • 举报
回复
jinfeng wang:
你说的不错, IO, db, 对象状态的修改,new, delete都是side-effect.
95年以前, 都是标准所不允许优化的。所以,放cout<<...进去, 就会阻止优化的发生。

判断一个函数是否有side-effect, 其实并不是太难。
IO, db操作, 赋值操作, 都是side-effect. 对其他函数的调用, 只要编译器能够得到包含那个函数代码的目标文件, 理论上也是可能判断的。(当然, 如果类型系统能够帮助区分就更好了, 不幸的是, C++这点上作得不够好)
对运行库, DLL的调用应该被归做side-effect, 因为你永远不知道函数内部做了些什么。
对虚函数,也是一样, 因为编译时无法决定调用哪个函数, 所以也应该保守地认为它是有side-effect的。


当然,对含有side-effect的拷贝构造函数/析构函数的ROV, 在新标准中也许会被允许。如果是那样, 编译器就可能不会理会你的cout<<…., 还是优化掉你的拷贝构造函数/析构函数的。
VC++6.0显然不是这样的。

另外, 同意你的观点, 自己定义的拷贝构造函数只可能阻止优化的发生。

elvahuang 2002-06-23
  • 打赏
  • 举报
回复
to : jinfeng_wang(一天只需(许)来一次)
例如通过传值方式传回objects?如果答案是yes,那么提供一个copy_construcot的explicit inline函数实体就非常合理——在“你的编译器提供NRV优化”的前提下。
为什么?????不是书上说对于暗中的那个是不存在调用的!!!不是这样在时间上更有效率!!???为什么要explicit inline函数实体就非常合理???
是为了nrv???????是在“你的编译器提供NRV优化”的前提下的原因吗?????
thanks in advance!!!!!!!!


Juventus 2002-06-22
  • 打赏
  • 举报
回复
"在<<深度 探索>>上的伪码里,构造函数是在第一次使用时"
这是优化时构造函数的调用时机吧,如果普通的话我想应该是定义的时候

另外我对书上说得要有copy构造函数存在才能激发nrv优化感到不可理解
我也看了侯捷网站上的讨论,大家都觉的这是错的,可是stan在书中好像
有好几处一再提到此点,不知对此点大家有什么看法。

当然,个人以为优化是各家编译器自己的事
对于自己的编译器要知道效率究竟如何,还是要实际来回答
oujinliang 2002-06-22
  • 打赏
  • 举报
回复
如果 constructor 是在第一次使用的时候调用,那么此处的对称性确实是被打破了。在bar()中构造,返回时却不析构。而foo()中 只是声明,没有构造,退出时却要插入一个 destructor ! 至于对象在第一次使用的时候构造,编译器有它自己的理由的。

由此一来,速度确实是快了的:省却了临时对象,局部对象的构造,省却了copy constructor 的调用,省却了所有这些对象的析构,自然是要快的。
陈硕 2002-06-22
  • 打赏
  • 举报
回复
gz...
alidiedie 2002-06-21
  • 打赏
  • 举报
回复
楼上,如果加了cout<<等语句.
至于选择不选择编译器层的优化,我怎么去控制呢?
ajoo 2002-06-21
  • 打赏
  • 举报
回复
I wrote a doc in the doc center about this. But when I submitted it, it did not appear in mydoc folder, why is that?
alidiedie 2002-06-21
  • 打赏
  • 举报
回复
在<<深度 探索>>上的伪码里,构造函数是在第一次使用 时
oujinliang 2002-06-21
  • 打赏
  • 举报
回复
临时对象未必是const吧。

对于 constructor 的调用的时机,到底是在声明的时候呢,还在在第一次使用的时候?我觉得这是理解楼主的问题的关键。
alidiedie 2002-06-21
  • 打赏
  • 举报
回复
但为什么临时对象一定是const呢?没必要吧.
即使是这样也会产生临时对象
string& rs="";
实际上是
string t="";
string &rs=t;
不可以吗?
alidiedie 2002-06-21
  • 打赏
  • 举报
回复
同意楼上的意见.
我的本意是"没有多大用处的对象".没有表述清楚.
确实,临时对象是编译器介入的.
非常感谢.
elvahuang 2002-06-21
  • 打赏
  • 举报
回复
我来说说我的看法:
我觉得楼主下面这句话:
2.调用bar()函数,返回时存在临时对象构造,以及拷贝构造函数的调用.
中的“临时对象”不对!!
在EC(Effective C++)中:const string& rs="";只有这样才是会产生临时对象!!注意临时对象是const!!所以编译器会:string t="";
const string& rs=t;
t才是临时对象!为const!but 在程序转化中产生的对象不是(因为是由于程序语意转化!语意要求他不是const !)

我的理解是临时对象是编译器介入的!!!!!!
而2。是程序语意转化的!!!
***我 的 看 法***
alidiedie 2002-06-21
  • 打赏
  • 举报
回复
还不是.
我也知道,现在讨论的问题没有用,因为实际上是编译器设计策略问题,但既然碰到这个问题了,就弄个明白.
加载更多回复(3)

69,371

社区成员

发帖
与我相关
我的任务
社区描述
C语言相关问题讨论
社区管理员
  • C语言
  • 花神庙码农
  • 架构师李肯
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧