同样是 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 条回复
lhx2008
2019-07-14 17:15:02 +08:00
我猜,关掉编译器优化,反汇编之后结果就一样了
ipwx
2019-07-14 17:23:04 +08:00
首先,楼主你两段代码都是不合法的。

对于不合法的代码,C++ 编译器没有义务给你吐出合理的结果。
stephen9357
2019-07-14 17:23:24 +08:00
debug 编译的代码,编译器怎么顺手怎么来,别当真。甚至用 IDA 看系统自带的 release 动态库时,也遇到过类似情况,编译器毕竟不能保证每一行代码编译后都是最优的,能看明白就可以了。
amiwrong123
2019-07-14 17:36:22 +08:00
@lhx2008
用得是 vs2017,找了找,在当前项目的什么设置里面,找到了“优化”,里面有什么内联函数拓展、启动内部函数什么的,但基本都是关着的。

@ipwx
const int& b = re();原来这种用法是不合法的吗?有点没懂啊,我知道如果返回局部变量的引用,这种情况是不合法的,虽然编译器只是报个 warning。


@stephen9357
原来是这样的啊。确实大概能看明白,比如函数体返回值那里有没有&(返回的是不是引用),会体现到汇编上去。虽然分配了两次空间,但可能就是编译器没优化好呗。
thedrwu
2019-07-14 17:36:33 +08:00
首先,不能这样写。你让变量活在哪里?

至于楼主的问题, 对向返回的是一个地址. 最后到 edx 里的是地址的地址,即 %ebp-30h 这个数字。中间变量没毛病。
至于第二段, 不是地址的地址, 而只有一层地址。

没仔细看,仅供参考
akira
2019-07-14 17:47:02 +08:00
第一个是类吧 类应该是需要 2 个指针来表达的
hoyixi
2019-07-14 17:49:56 +08:00
'lift' and right
amiwrong123
2019-07-14 17:57:34 +08:00
@thedrwu
好吧,首先是不是,引用绑定到返回的中间变量,这种写法就是错的吗

然后,我又改了一下,改成 sale global = add(one, two);汇编就变成了:
00B019D2 8D 45 E8 lea eax,[two]
00B019D5 50 push eax
00B019D6 8D 4D F4 lea ecx,[one]
00B019D9 51 push ecx
00B019DA E8 F0 F9 FF FF call add (0B013CFh)
00B019DF 83 C4 08 add esp,8
00B019E2 89 85 10 FF FF FF mov dword ptr [ebp-0F0h],eax
00B019E8 8B 95 10 FF FF FF mov edx,dword ptr [ebp-0F0h]
00B019EE 89 55 DC mov dword ptr [global],edx
好像跟是不是对象没关系,只用一个地址就好了。
哎,我是不是有点钻牛角尖了,但是又有点好奇。

@akira
你看上面的汇编,好像跟是不是对象没关系啊。

@hoyixi
哈哈哈,一时手滑啦
aliwalker
2019-07-14 18:15:24 +08:00
我用 clang 编译了一下第一段,发现没有写把 add 返回值写两次内存的操作...

100000f5a: 48 8d 7d f8 leaq -8(%rbp), %rdi # &one
100000f5e: 48 8d 75 f0 leaq -16(%rbp), %rsi # &two
100000f62: e8 a9 ff ff ff callq -87 <__Z3addRK4saleS1_> # call add
100000f67: 31 c9 xorl %ecx, %ecx # 清零
100000f69: 89 45 e0 movl %eax, -32(%rbp) # 返回值存到临时变量
100000f6c: 48 8d 75 e0 leaq -32(%rbp), %rsi # 指针
100000f70: 48 89 75 e8 movq %rsi, -24(%rbp) # 指针值存到 global
100000f74: 89 c8 movl %ecx, %eax # 返回值为 0
100000f76: 48 83 c4 20 addq $32, %rsp
100000f7a: 5d popq %rbp
100000f7b: c3 retq

用 const 引用返回值是可以的,这个临时变量在 call site 的 frame 上是有分配空间的。如果改成 sale &global = add(one, two);就不行了:initial value of reference to non-const must be an lvalue。

第二段结果是一样的,只是生成的是 x64 机器码。
aliwalker
2019-07-14 18:21:56 +08:00
补充一下,从第二段反汇编出来的内容可以看到为什么不是 const 引用不行:

_main:
100000f90: 55 pushq %rbp
100000f91: 48 89 e5 movq %rsp, %rbp
100000f94: 48 83 ec 10 subq $16, %rsp
100000f98: e8 e3 ff ff ff callq -29 <__Z2rev>
100000f9d: 31 c9 xorl %ecx, %ecx
100000f9f: 89 45 f4 movl %eax, -12(%rbp)
100000fa2: 48 8d 55 f4 leaq -12(%rbp), %rdx
100000fa6: 48 89 55 f8 movq %rdx, -8(%rbp)
100000faa: 89 c8 movl %ecx, %eax
100000fac: 48 83 c4 10 addq $16, %rsp
100000fb0: 5d popq %rbp
100000fb1: c3 retq

返回的 int 是 4bytes,写在-12(%rbp)上,但是指针 b 的位置-8(%rbp)其实和这个返回的 temp 值重合。
zjsxwc
2019-07-14 18:33:46 +08:00
难道没人和我一样奇怪
sum 在栈里的内容在函数结束时会不会被释放吗
ipwx
2019-07-14 18:43:05 +08:00
@amiwrong123 不合法的理由,见 5L 和 11L 的疑惑。
ipwx
2019-07-14 18:45:31 +08:00
另外我大概理解楼主为什么要写不合法代码的理由了,是想研究 C++ 返回值地址的问题嘛?

但是 C++ 编译器会根据返回值的赋值进行代码优化的。

比如:

class A { ... };

A someFunction() { A a; return a; }

A target = someFunction();

在深度优化的时候不会发生拷贝,直接在返回值 target 上调用构造函数。
zjsxwc
2019-07-14 18:46:42 +08:00
In C++, unlike in C#, struct makes few differences with class. A struct is a class whose default visibility is public. Whether the allocation is performed on the stack or in the heap depends on the way you allocate your instance

class A;

void f()
{
A a;//stack allocated
A *a1 = new A();// heap
}
zjsxwc
2019-07-14 18:48:55 +08:00
@zjsxwc 楼主的 sum 内存会被释放
aliwalker
2019-07-14 18:50:15 +08:00
@ipwx yep. Return value optimization. 是 copy elision 的一种
lcdtyph
2019-07-14 20:27:46 +08:00
@ipwx #12
@thedrwu #5

两个都是合法的,临时变量在绑定到常引用之后生命周期会被延长,参见 https://en.cppreference.com/w/cpp/language/lifetime

在 c++11 右值出现之前都是这样做的。
amiwrong123
2019-07-14 20:50:15 +08:00
@aliwalker
既然你用 clang 编译没有出现两次写内存,那可能我的编译器的问题吧。
然后你的第二段,我看了,它把返回值 int 存在了-12,-11,-10,-9 这四个字节里,然后把地址存在了-8,-7,...,-1,没有什么重合啊感觉。所以没理解,“为什么不是 const 引用不行”。
amiwrong123
2019-07-14 20:57:03 +08:00
@ipwx
是啊,你说的差不多。主要把,我是想看看 return 到底是怎么 return 的,有几次内存拷贝,这样。
还有最后你说的这个深度优化,意思懂啦,相当于直接 A target;
amiwrong123
2019-07-14 20:59:17 +08:00
@zjsxwc
你们都说,内存会被释放掉。那么我打印 global 的值的时候,肯定就是非法值呗。
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);
cout << global.i;
}
执行这个代码,我发现还是能打印出来 2 啊,也不是什么非法值。

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

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

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

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

© 2021 V2EX