同样是 return 一个中间变量,为什么另一种代码的反汇编分配了两次空间?

2019-07-14 17:07:20 +08:00
 amiwrong123

总所周知,函数体返回值那里没有&,就会返回一个中间变量。

class sale {
public:
	int i = 1;
};

sale add(const sale& lift, const sale& right) {
	sale sum = lift;
	sum.i += right.i;
	return sum;
}

int main()
{
	sale one;
	sale two;
	const sale& global = add(one, two);
}

假设有如上代码,进行反汇编后,汇编如下:

	const sale& global = add(one, two);
00D51A32 8D 45 E8             lea         eax,[two]  
00D51A35 50                   push        eax  
00D51A36 8D 4D F4             lea         ecx,[one]  
00D51A39 51                   push        ecx  
00D51A3A E8 40 F9 FF FF       call        add (0D5137Fh)  
00D51A3F 83 C4 08             add         esp,8  #调用完毕后,清除栈空间
00D51A42 89 85 04 FF FF FF    mov         dword ptr [ebp-0FCh],eax  #为中间变量分配空间
00D51A48 8B 95 04 FF FF FF    mov         edx,dword ptr [ebp-0FCh]  
00D51A4E 89 55 D0             mov         dword ptr [ebp-30h],edx  #把中间变量复制给新的变量
00D51A51 8D 45 D0             lea         eax,[ebp-30h]  #导入新变量的地址
00D51A54 89 45 DC             mov         dword ptr [global],eax  #把这个地址给 global,因为引用本质是指针

上面写的注释不一定对,但为什么这里要分配两次空间呢? 如果我换一个简单的程序:

int re() {
	return 5;
}

int main()
{
	//int a = 1;
	const int& b = re();
}

他的汇编就和我想象中一样了,只分配一次空间:

	const int& b = re();
00B019B2 E8 0E FA FF FF       call        re (0B013C5h)  
00B019B7 89 45 E8             mov         dword ptr [ebp-18h],eax  #分配空间
00B019BA 8D 45 E8             lea         eax,[ebp-18h]  #把地址导入 eax
00B019BD 89 45 F4             mov         dword ptr [b],eax #把 eax 赋值给 b,因为 b 是引用,相当于指针
3134 次点击
所在节点    程序员
36 条回复
nethard
2019-07-14 21:20:09 +08:00
哈哈,楼主要是这么喜欢这样写,返回一个栈上的指针,可以去写 go 玩玩。
lcdtyph
2019-07-14 21:25:17 +08:00
@amiwrong123 #18
clang 有个参数 -fno-elide-constructors 可以让编译器不做 rvo,这样在你第一种代码里一定会分配两次内存。默认是会做 rvo 的。
visual studio 的编译器不知道有没有类似的选项……
ispinfx
2019-07-14 21:27:50 +08:00
好奇那么多说不合法的…这不是常引用最常见的用法吗
lcdtyph
2019-07-14 21:32:57 +08:00
还有实际上 lz 的情况在 c++17 中一定不会产生临时对象,这是 c++17 新的 guaranteed copy elision 特性来保证的。
amiwrong123
2019-07-14 21:50:42 +08:00
@lcdtyph
看到啦,在 Temporary object lifetime 的 There are two exceptions from that:,我这个属于 const lvalue reference。

你说的-fno-elide-constructors,这又是我的知识盲区了。。。看了看博客,意思就是会省略一些中间变量的创建,因为两次拷贝和一次拷贝效果是一样的。但为啥你说,我第一种代码里一定会分配两次内存,不是默认是会优化的吗? vs2017 里面好像没有这个选项。。

guaranteed copy elision,又是一个知识点,我拿小本本记上。
amiwrong123
2019-07-14 22:00:39 +08:00
@nethard
也不是喜欢这么写,只是在测试想把各种情况测试一遍,带不带 const,带不带&,这样


@ispinfx
开始这个用法对不对还真拿不准,然后就很疑惑。不过看了 17 楼的文档里写了这种用法。
lcdtyph
2019-07-14 22:02:31 +08:00
@amiwrong123
这个选项是强制在可以省略临时对象的情况下调用拷贝构造,所以打开这个选项就相当于没了返回值优化,就会造成一次额外的拷贝
ispinfx
2019-07-14 22:36:11 +08:00
@amiwrong123 这个我记得以前上谭 cpp primer 里就有说的吧,虽然我已不写 cpp10 年了。。
karia
2019-07-14 22:39:29 +08:00
《程序员的自我修养》内存那一章(11 章?忘了)讲到过这个问题

return 栈上大型结构的时候,caller 的 stack frame 里会分配 2 个临时空间,一个隐式的用来保存返回值,返回之后再 memcpy 给你的显式声明的 global

另外提醒楼主...乐于尝试是不错,但是不要钻牛角尖了...孔夫子曰过思而不学则殆...多看书,或许很多问题别人已经研究过了
lrxiao
2019-07-15 07:39:35 +08:00
在 debug 模式看汇编 yy 分配空间... 也可能只是中间变量没优化而已..
你需要的是写个 destructor 然后看调用次数..
lrxiao
2019-07-15 07:45:49 +08:00
这两个都是 copy elision/rvo 后的结果了 并没有额外的 constructor/destructor 介入
14m3
2019-07-15 09:25:58 +08:00
1. 楼主,以后可以在 https://godbolt.org/(一个网页版的交互式编译器)上面来测试,可以选择不同的编译器,开启不同的编译选项,也能很方便看到汇编代码
2. 前面 @lcdtyph 已经说过了,const lvalue reference 会延长临时对象的生命周期,所以问题中的代码是合法的
3. 其实讨论这个代码,是需要确定讨论的环境的,是 C++11/14,还是 C++17。因为 C++17 标准中添加了 Guaranteed Copy Elision,重新定义了 value category 中 prvalues 的语义
4. 我自己测试了一下 https://godbolt.org/z/NRKI-R,在 C++17 标准下,不管 main 函数中是 const sale& global = add(one, two); 还是 const sale global = add(one, two); 问题中的代码都是调用了三次构造函数,前两次构造函数是 sale one; 和 sale two;,第三次构造函数是拷贝构造。在其他标准下,楼主可以自己测试.
amiwrong123
2019-07-15 13:50:13 +08:00
@lcdtyph
好吧,懂啦,看来这个编译器选项也挺重要啊。

@ispinfx
我就正在 primer 呢,但看得断断续续,我是因为看到书中某页的一句话“返回的是 sum 的副本”,但没细说,然后就开始想这个问题。
amiwrong123
2019-07-15 14:08:34 +08:00
@karia
这本书买了,还没看。的确很多问题别人已经研究过了,所以一般有问题我都是先看网上博客啥的

@lrxiao
你说,写个 destructor 然后看调用次数,这个挺好使啊。我在 32 楼给的链接里看到了调用析构的次数了。
你意思这两段代码都是已经被优化了的呗
amiwrong123
2019-07-15 14:12:02 +08:00
@14m3
哇,你这个链接是个神器,回头好好研究下。确实,这个代码怎么优化的跟环境有关系啊。你这个代码而且写得很清楚,加了构造和析构的打印后,思路瞬间清晰了。
14m3
2019-07-15 20:25:05 +08:00
@amiwrong123 嗯嗯,互相学习 :)

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/582872

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX