Go中逃逸现象, 变量“何时栈?何时堆?”

火山方舟向量数据库大模型

最灵繁的人也看不见自己的背脊。——非洲

1 C/C++报错?Golang通过?

我们先看一段代码

  
package main  
func foo(arg_val int)(*int){  
var foo_val int=11;  
return&foo_val;  
}  
func main(){  
 main_val := foo(666)  
 println(*main_val)  
}

编译运行

  
$ go run pro_1.go   
11

竟然没有报错!

了解C/C++的小伙伴应该知道,这种情况是一定不允许的,因为 外部函数使用了子函数的局部变量, 理论来说,子函数的foo_val 的声明周期早就销毁了才对,如下面的C/C++代码

  
#include<stdio.h>  
int*foo(int arg_val){  
int foo_val =11;  
return&foo_val;  
}  
int main()  
{  
int*main_val = foo(666);  
 printf("%d\n",*main_val);  
}

编译

  
$ gcc pro_1.c   
pro_1.c:Infunction‘foo’:  
pro_1.c:7:12: warning:function returns address of local variable [-Wreturn-local-addr]  
return&foo_val;  
^~~~~~~~

出了一个警告,不管他,再运行

  
$ ./a.out  
段错误(核心已转储)

程序崩溃.

如上C/C++编译器明确给出了警告,foo把一个局部变量的地址返回了;反而高大上的go没有给出任何警告,难道是go编译器识别不出这个问题吗?

2 Golang编译器得逃逸分析

go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做 逃逸分析(escape analysis),当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆 。go语言声称这样可以释放程序员关于内存的使用限制,更多的让程序员关注于程序功能逻辑本身。

我们再看如下代码:

  
package main  
func foo(arg_val int)(*int){  
var foo_val1 int=11;  
var foo_val2 int=12;  
var foo_val3 int=13;  
var foo_val4 int=14;  
var foo_val5 int=15;  
//此处循环是防止go编译器将foo优化成inline(内联函数)  
//如果是内联函数,main调用foo将是原地展开,所以foo_val1-5相当于main作用域的变量  
//即使foo_val3发生逃逸,地址与其他也是连续的  
for i :=0; i <5; i++{  
 println(&arg_val,&foo_val1,&foo_val2,&foo_val3,&foo_val4,&foo_val5)  
}  
//返回foo_val3给main函数  
return&foo_val3;  
}  
func main(){  
 main_val := foo(666)  
 println(*main_val, main_val)  
}

我们运行一下

  
$ go run pro_2.go   
0xc0000307580xc0000307380xc0000307300xc0000820000xc0000307280xc000030720  
0xc0000307580xc0000307380xc0000307300xc0000820000xc0000307280xc000030720  
0xc0000307580xc0000307380xc0000307300xc0000820000xc0000307280xc000030720  
0xc0000307580xc0000307380xc0000307300xc0000820000xc0000307280xc000030720  
0xc0000307580xc0000307380xc0000307300xc0000820000xc0000307280xc000030720  
130xc000082000

我们能看到foo_val3是返回给main的局部变量, 其中他的地址应该是0xc000082000,很明显与其他的foo_val1、2、3、4不是连续的.

我们用go tool compile测试一下

  
$ go tool compile -m pro_2.go  
pro_2.go:24:6: can inline main  
pro_2.go:7:9: moved to heap: foo_val3

果然,在编译的时候, foo_val3具有被编译器判定为逃逸变量, 将foo_val3放在堆中开辟.

我们在用汇编证实一下:

  
$ go tool compile -S pro\_2.go > pro\_2.S

打开pro_2.S文件, 搜索runtime.newobject关键字

  
...  
160x002100033(pro_2.go:5) PCDATA $0, $0  
170x002100033(pro_2.go:5) PCDATA $1, $0  
180x002100033(pro_2.go:5) MOVQ $11,"".foo_val1+48(SP)  
190x002a00042(pro_2.go:6) MOVQ $12,"".foo_val2+40(SP)  
200x003300051(pro_2.go:7) PCDATA $0, $1  
210x003300051(pro_2.go:7) LEAQ type.int(SB), AX  
220x003a00058(pro_2.go:7) PCDATA $0, $0  
230x003a00058(pro_2.go:7) MOVQ AX,(SP)  
240x003e00062(pro_2.go:7) CALL runtime.newobject(SB)//foo_val3是被new出来的  
250x004300067(pro_2.go:7) PCDATA $0, $1  
260x004300067(pro_2.go:7) MOVQ 8(SP), AX  
270x004800072(pro_2.go:7) PCDATA $1, $1  
280x004800072(pro_2.go:7) MOVQ AX,"".&foo_val3+56(SP)  
290x004d00077(pro_2.go:7) MOVQ $13,(AX)  
300x005400084(pro_2.go:8) MOVQ $14,"".foo_val4+32(SP)  
310x005d00093(pro_2.go:9) MOVQ $15,"".foo_val5+24(SP)  
320x006600102(pro_2.go:9) XORL CX, CX  
330x006800104(pro_2.go:15) JMP 252  
...

看出来, foo_val3是被runtime.newobject()在堆空间开辟的, 而不是像其他几个是基于地址偏移的开辟的栈空间.

3 new的变量在栈还是堆?

那么对于new出来的变量,是一定在heap中开辟的吗,我们来看看

  
package main  
func foo(arg_val int)(*int){  
var foo_val1 *int=new(int);  
var foo_val2 *int=new(int);  
var foo_val3 *int=new(int);  
var foo_val4 *int=new(int);  
var foo_val5 *int=new(int);  
//此处循环是防止go编译器将foo优化成inline(内联函数)  
//如果是内联函数,main调用foo将是原地展开,所以foo_val1-5相当于main作用域的变量  
//即使foo_val3发生逃逸,地址与其他也是连续的  
for i :=0; i <5; i++{  
 println(arg_val, foo_val1, foo_val2, foo_val3, foo_val4, foo_val5)  
}  
//返回foo_val3给main函数  
return foo_val3;  
}  
func main(){  
 main_val := foo(666)  
 println(*main_val, main_val)  
}

我们将foo_val1-5全部用new的方式来开辟, 编译运行看结果

  
$ go run pro_3.go   
6660xc0000307280xc0000307200xc00001a0e00xc0000307380xc000030730  
6660xc0000307280xc0000307200xc00001a0e00xc0000307380xc000030730  
6660xc0000307280xc0000307200xc00001a0e00xc0000307380xc000030730  
6660xc0000307280xc0000307200xc00001a0e00xc0000307380xc000030730  
6660xc0000307280xc0000307200xc00001a0e00xc0000307380xc000030730  
00xc00001a0e0

很明显, foo_val3的地址0xc00001a0e0依然与其他的不是连续的. 依然具备逃逸行为.

4 结论

Golang中一个函数内的局部变量,不管是不是动态new出来的,它会被分配在堆还是栈,是由编译器做逃逸分析之后做出的决定。

按理来说, 人家go的设计者明明就不希望开发者管这些,但是面试官就偏偏找这种问题问? 醉了也是。

5 关注公众号

微信公众号: 堆栈future

picture.image

扫我关注

0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论