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

简介: 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



相关文章
初识go变量,使用var和:=来声明变量,声明变量的三种方式
这篇文章介绍了Go语言中使用`var`和`:=`声明变量的三种不同方式,包括声明单个或多个变量、通过值确定数据类型以及在函数体内使用`:=`声明局部变量。
初识go变量,使用var和:=来声明变量,声明变量的三种方式
|
2月前
|
存储 编译器 Go
go语言中的变量、常量、数据类型
【11月更文挑战第3天】
40 9
|
2月前
|
网络协议 安全 Go
Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
【10月更文挑战第28天】Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
79 13
|
3月前
|
算法 Java 编译器
你为什么不应该过度关注go语言的逃逸分析
【10月更文挑战第21天】逃逸分析是 Go 语言编译器的一项功能,用于确定变量的内存分配位置。变量在栈上分配时,函数返回后内存自动回收;在堆上分配时,则需垃圾回收管理。编译器会根据变量的使用情况自动进行逃逸分析。然而,过度关注逃逸分析可能导致开发效率降低、代码复杂度增加,并且对性能的影响相对较小。编译器优化通常比人工干预更准确,因此开发者应更多关注业务逻辑和整体性能优化。
|
5月前
|
存储 编译器 Go
Go语言中的逃逸分析
Go语言中的逃逸分析
|
5月前
|
存储 算法 Go
在Go中理解栈和先进先出原则
在Go中理解栈和先进先出原则
|
5月前
|
Go
Go1.22 新特性:for 循环不再共享循环变量,且支持整数范围
Go1.22 新特性:for 循环不再共享循环变量,且支持整数范围
|
5月前
|
设计模式 Java 编译器
Go - 基于逃逸分析来提升程序性能
Go - 基于逃逸分析来提升程序性能
52 2
|
5月前
|
安全 Go
|
5月前
|
自然语言处理 Go 开发者
深入理解Go语言中的变量作用域
【8月更文挑战第31天】
38 0