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



相关文章
|
5月前
|
Go 容器
Go语言变量与常量 -《Go语言实战指南》
本章详细介绍了Go语言中变量与常量的基础知识。变量支持多种声明方式,包括标准声明、类型推导和短变量声明等,未初始化的变量会自动赋零值。常量在声明时必须赋值,且运行时不可更改,支持使用`iota`实现枚举。两者的主要区别在于变量可变而常量不可变,变量有零值而常量必须初始化。此外,还强调了`:=`的使用限制及代码整洁性要求,并通过实践示例巩固理解。掌握这些内容是学好Go语言的关键基础。
初识go变量,使用var和:=来声明变量,声明变量的三种方式
这篇文章介绍了Go语言中使用`var`和`:=`声明变量的三种不同方式,包括声明单个或多个变量、通过值确定数据类型以及在函数体内使用`:=`声明局部变量。
初识go变量,使用var和:=来声明变量,声明变量的三种方式
|
7月前
|
Java 编译器 Go
go的内存逃逸分析
内存逃逸分析是Go编译器在编译期间根据变量的类型和作用域,确定变量分配在堆上还是栈上的过程。如果变量需要分配在堆上,则称作内存逃逸。Go语言有自动内存管理(GC),开发者无需手动释放内存,但编译器需准确分配内存以优化性能。常见的内存逃逸场景包括返回局部变量的指针、使用`interface{}`动态类型、栈空间不足和闭包等。内存逃逸会影响性能,因为操作堆比栈慢,且增加GC压力。合理使用内存逃逸分析工具(如`-gcflags=-m`)有助于编写高效代码。
139 2
|
11月前
|
存储 编译器 Go
go语言中的变量、常量、数据类型
【11月更文挑战第3天】
125 9
|
11月前
|
网络协议 安全 Go
Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
【10月更文挑战第28天】Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
197 13
|
12月前
|
算法 Java 编译器
你为什么不应该过度关注go语言的逃逸分析
【10月更文挑战第21天】逃逸分析是 Go 语言编译器的一项功能,用于确定变量的内存分配位置。变量在栈上分配时,函数返回后内存自动回收;在堆上分配时,则需垃圾回收管理。编译器会根据变量的使用情况自动进行逃逸分析。然而,过度关注逃逸分析可能导致开发效率降低、代码复杂度增加,并且对性能的影响相对较小。编译器优化通常比人工干预更准确,因此开发者应更多关注业务逻辑和整体性能优化。
|
存储 编译器 Go
Go语言中的逃逸分析
Go语言中的逃逸分析
|
存储 算法 Go
在Go中理解栈和先进先出原则
在Go中理解栈和先进先出原则
Go1.22 新特性:for 循环不再共享循环变量,且支持整数范围
Go1.22 新特性:for 循环不再共享循环变量,且支持整数范围
|
设计模式 Java 编译器
Go - 基于逃逸分析来提升程序性能
Go - 基于逃逸分析来提升程序性能
101 2

热门文章

最新文章

下一篇
日志分析软件