Go 逃逸分析

47 阅读3分钟

概述

编译器通过逃逸分析技术去选择堆或者栈,逃逸分析的基本思想如下:检查变量的生命周期是否是完全可知的,如果通过检查,则可以在栈上分配。否则,就是所谓的逃逸,必须在堆上进行分配

逃逸分析原则

如果变量在函数外部没有引用,则优先放到栈中

如果变量在函数外部存在引用,则必定放在堆中

场景分析

变量类型不确定


package main

import "fmt"

func main() {

a := 666

fmt.Println(a)

}

逃逸分析结果


[root@master 20230915]# go build -gcflags '-m -m -l' demo_escapes_2.go

# command-line-arguments

./demo_escapes_2.go:7:14: a escapes to heap:

./demo_escapes_2.go:7:14: flow: {storage for ... argument} = &{storage for a}:

./demo_escapes_2.go:7:14: from a (spill) at ./demo_escapes_2.go:7:14

./demo_escapes_2.go:7:14: from ... argument (slice-literal-element) at ./demo_escapes_2.go:7:13

./demo_escapes_2.go:7:14: flow: {heap} = {storage for ... argument}:

./demo_escapes_2.go:7:14: from ... argument (spill) at ./demo_escapes_2.go:7:13

./demo_escapes_2.go:7:14: from fmt.Println(... argument...) (call parameter) at ./demo_escapes_2.go:7:13

./demo_escapes_2.go:7:13: ... argument does not escape

./demo_escapes_2.go:7:14: a escapes to heap

分析结果告诉我们变量a逃逸到了堆上。但是,我们并没有外部引用啊,为啥也会有逃逸呢?

a逃逸是因为它被传入了fmt.Println的参数中,这个方法参数自己发生了逃逸。


func Println(a ...interface{}) (n int, err error)

因为fmt.Println的函数参数为interface类型,编译期不能确定其参数的具体类型,所以将其分配于堆上

对于上述的情况,更改代码。就不会出现逃逸


package main

func main() {

a := 666

println(a)

// fmt.Println(a)

}

逃逸分析


[root@master 20230915]# go build -gcflags '-m -m -l' demo_escapes_2.go

[root@master 20230915]#

暴露给外部指针


package main

func foo() *int {

a := 666

return &a

}

func main() {

_ = foo()

}

逃逸分析


[root@master 20230915]# go build -gcflags '-m -m -l' demo_escapes_3.go

# command-line-arguments

./demo_escapes_3.go:4:2: a escapes to heap:

./demo_escapes_3.go:4:2: flow: ~r0 = &a:

./demo_escapes_3.go:4:2: from &a (address-of) at ./demo_escapes_3.go:5:9

./demo_escapes_3.go:4:2: from return &a (return) at ./demo_escapes_3.go:5:2

./demo_escapes_3.go:4:2: moved to heap: a

这种情况直接满足我们上述中的原则:变量在函数外部存在引用

因为当函数执行完毕,对应的栈帧就被销毁,但是引用已经被返回到函数之外

如果这时外部从引用地址取值,虽然地址还在,但是这块内存已经被释放回收了,这就是非法内存,问题可就大了

变量所占内存较大


package main

func foo() {

s := make([]int, 10000, 10000)

for i := 0; i < len(s); i++ {

s[i] = i

}

}

func main() {

foo()

}

逃逸分析


# command-line-arguments

./demo_escapes_4.go:4:11: make([]int, 10000, 10000) escapes to heap:

./demo_escapes_4.go:4:11: flow: {heap} = &{storage for make([]int, 10000, 10000)}:

./demo_escapes_4.go:4:11: from make([]int, 10000, 10000) (too large for stack) at ./demo_escapes_4.go:4:11

./demo_escapes_4.go:4:11: make([]int, 10000, 10000) escapes to heap

当我们创建了一个容量为10000的int类型的底层数组对象时,由于对象过大,它也会被分配到堆上

变量大小不确定


package main

func foo() {

n := 1

s := make([]int, n)

for i := 0; i < len(s); i++ {

s[i] = i

}

}

func main() {

foo()

}

逃逸分析结果


[root@master 20230915]# go build -gcflags '-m -m -l' demo_escapes_5.go

# command-line-arguments

./demo_escapes_5.go:5:14: make([]int, n) escapes to heap:

./demo_escapes_5.go:5:14: flow: {heap} = &{storage for make([]int, n)}:

./demo_escapes_5.go:5:14: from make([]int, n) (non-constant size) at ./demo_escapes_5.go:5:14

./demo_escapes_5.go:5:14: make([]int, n) escapes to heap

在make方法中,没有直接指定大小,而是填入了变量n,这时Go逃逸分析也会将其分配到堆区去

可见,为了保证内存的绝对安全,Go的编译器可能会将一些变量不合时宜地分配到堆上,但是因为这些对象最终也会被垃圾收集器处理

参考资料