Go的逃逸分析

什么是逃逸分析

在编译原理中,分析指针动态范围的方法被称之为逃逸分析。当一个对象的指针被其他地方引用的时候,那么这个真正就发生了逃逸,指的是由编译器决定内存分配的位置,不需要程序员指定。

逃逸分析就决定了一个变量时分配在堆上还是分配在栈上

逃逸策略

那什么时候会发生逃逸分析呢?

那就是判断该对象是否会被函数外部引用。

前面也指出了逃逸分析会决定一个变量合理的分配到它该去的地方:堆或者栈。在函数中申请了一个新的对象时,编译器会判断该对象是否会被函数外部引用:

  • 在函数结束后该对象没有被引用(被函数外部使用),则会分配到栈上;
  • 在函数解释后该对象有被使用(被函数外部使用),那必定会分配到堆上。

除此之外,对于今年在函数内部使用的变量,也有可能放到堆中,比如内存过大超过了栈的存储能力。

逃逸场景

1)指针逃逸

在Go中可以返回一个对象的指针,这就是很典型的指针逃逸:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main  

type Student struct {
Name string
Age int
}

func NewStudent(name string, age int) *Student {
return &Student{
Name: name,
Age: age,
}
}

func main() {
NewStudent("jack", 25)
}

然后使用go build -gcflags=-m命令编译并查看输出

-gcfkags 参数用于启用编译器支持的额外标志
-m 用于输出编译器的优化细节
-N 关闭编译器优化
-l 用于关闭内联优化

1
2
3
4
5
6
7
8
9
10
PS D:\Code\project\study_go\base\escape> go build -gcflags=-m
# study_go/base/escape
.\main.go:8:6: can inline NewStudent
.\main.go:18:6: can inline main
.\main.go:19:12: inlining call to NewStudent
.\main.go:8:17: leaking param: name
.\main.go:12:9: &Student{...} escapes to heap
.\main.go:19:12: &Student{...} does not escape
PS D:\Code\project\study_go\base\escape>

函数NewStudent()直接返回了一个Student对象指针,在NewStudent()函数结束后,该指针被main函数所引用,所以就发生了指针逃逸。

2)栈空间不足逃逸

看看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 2. 栈空间不足逃逸  

func slice() {
s := make([]int, 1000, 1000)

for i, _ := range s {
s[i] = i
}
}

func main() {

// 2. 栈空间不足逃逸
slice()
}

查看编译情况

1
2
3
4
5
6
7
PS D:\Code\project\study_go\base\escape> go build -gcflags=-m
# study_go/base/escape
.\main.go:19:6: can inline slice
.\main.go:27:6: can inline main
.\main.go:33:7: inlining call to slice
.\main.go:20:11: make([]int, 1000, 1000) does not escape
.\main.go:33:7: make([]int, 1000, 1000) does not escape

没有发生情况。那如果我把代码slice()函数切片分配的长度修改为10000后再编译呢?

1
2
3
4
5
6
7
PS D:\Code\project\study_go\base\escape> go build -gcflags=-m
# study_go/base/escape
.\main.go:19:6: can inline slice
.\main.go:27:6: can inline main
.\main.go:33:7: inlining call to slice
.\main.go:20:11: make([]int, 1000, 10000) escapes to heap
.\main.go:33:7: make([]int, 1000, 10000) escapes to heap

我们发现当切片长度扩大到10000时就会发生逃逸。

实际上当栈空间不足以存放当前对象或者无法判断当前切片长度时会将对象分配到堆上。

3)动态类型逃逸

在Go中,有一种类型能涵盖所有的类型,那就是 interface{}。很多库包括标准库的函数入参都为 interface{},比如 fmt.Println(a ...interface{}) ,编译期间很难确定其参数的具体类型,也会发生逃逸。

Go的最新版本都将 interface{} 修改为 any 了,但实际上也就是把 interface{} 起了一个类型别名:type any interface{}

1
2
3
4
func main() {  
s := "escape"
fmt.Println(s)
}

编译查看结果

1
2
3
4
5
PS D:\Code\project\study_go\base\escape> go build -gcflags=-m
# study_go/base/escape
.\main.go:37:13: inlining call to fmt.Println
.\main.go:37:13: ... argument does not escape
.\main.go:37:13: s escapes to heap

总结

什么是逃逸分析?

逃逸分析是由编译器决定内存分配的位置,不需要程序员指定。逃逸分析就决定了一个变量时分配在堆上还是分配在栈上

在函数中申请了一个新的对象时:

  • 如果分配在栈上,则函数执行结束时可自动将内存回收
  • 如果分配在堆上,函数执行结束后交给GC处理。

逃逸的策略有哪些?

逃逸分析的判断依据是该对象是否被函数外部引用

  • 在函数结束后该对象没有被引用(被函数外部使用),则会分配到栈上;
  • 在函数解释后该对象有被使用(被函数外部使用),那必定会分配到堆上。

逃逸的场景有哪些?

  • 指针逃逸

最经典的逃逸场景,函数内的指针被函数外部引用时就会发生逃逸。

  • 栈空间不足逃逸

实际上当栈空间不足以存放当前对象或者无法判断当前切片长度时会将对象分配到堆上。

  • 动态类型逃逸

在编译期间无法确定函数参数的具体类型时就会发生逃逸现象,比如下面函数的入参就是个 interface{} :

1
2
3
func Println(a ...any) (n int, err error) {  
return Fprintln(os.Stdout, a...)
}

Go的逃逸分析
https://sunshinechen.cn/2023/02/13/Go的逃逸分析/
作者
sunshine
发布于
2023年2月13日
许可协议