Go语言中的逃逸分析机制

发表时间: 2024-03-22 11:46

逃逸分析

逃逸分析(Escape Analysis)指的是将变量的内存分配在合适的地方(堆或者栈)。 在函数中申请内存有2种情况: - 如果内存分配在栈(stack)上,当函数退出的时候,这部分内存会自然的回收,不需要垃圾回收(GC,Garbage Collection) - 如果内存分配在堆(heap)上,函数的执行会交给GC(Garbage Collection)来处理。

此外,Golang与闭包密切相关。

Golang的逃逸分析

Golang的逃逸分析的基本原则是:如果函数返回了变量的引用,那么这个变量就会逃逸。 编译器通过分析代码,决定变量分配的地方: - 如果变量在函数外没有被引用,那么优先分配在栈(stack)上。 - 如果变量在函数外被引用,那么优先分配在堆(heap)上。

需要注意的是,没有在函数外被引用的变量,也有可能被分配在堆(heap)上。例如,这个变量需要的内存太大,超出了栈的容量,(目前,一个Goroutine的栈的最大容量,在64位系统是1GB,在32位系统是250MB)。栈内存的分配和回收是非常快速的,只需要2条CPU指令,PUSHRELEASE。而堆内存,分配需要找到合适大小的内存块,回收则是通过GC。

因此,通过内存的逃逸分析,可以尝试将不必要分配在堆上的变量分配在栈上,减少分配堆内存的开销和GC的压力。下面看一下一些逃逸的例子。

指针逃逸

看下面的代码cat.go

package maintype Cat struct {    Name string    Age  int}//go:noinlinefunc NewCat(name string, age int) *Cat {    c := new(Cat) // c will excape to heap    c.Name = name    c.Age = age    return c}func main() {    NewCat("Tom", 5)}

进行逃逸分析

$ go build -gcflags="-m" cat.go# command-line-arguments./cat.go:16:6: can inline main./cat.go:9:13: leaking param: name./cat.go:10:10: new(Cat) escapes to heap

可以看到,./cat.go:10:10: new(Cat) escapes to heap, 有变量的内存逃逸。

动态类型逃逸

package mainimport "fmt"func test() *int {    s := 3    return &s}func main() {    x := test()    fmt.Println(*x)}

编译代码

$ go build -gcflags="-m -l" dynamic.go# command-line-arguments./dynamic.go:6:2: moved to heap: s./dynamic.go:11:13: ... argument does not escape./dynamic.go:11:14: *x escapes to heap

变量s产生了内存逃逸,正如前一个例子。 这里要注意的是,/dynamic.go:11:14: *x escapes to heap也发生了内存逃逸,这是因为fmt.Println(a ...interface{}),fmt接受的参数是interface{},这是类型不确定的。 编译期间不能确定参数的具体的类型,逃逸就会产生。

slice,map和channel的指针引用

package mainfunc main() {    a := make([]*int, 1)    b := 12    a[0] = &b    c := make(map[string]*int)    d := 14    c["aaa"] = &d    e := make(chan *int, 1)    f := 15    e <- &f}

编译代码

go run -gcflags "-m -l" main.go./main.go:7:2: moved to heap: b./main.go:11:2: moved to heap: d./main.go:15:2: moved to heap: f./main.go:6:11: main make([]*int, 1) does not escape./main.go:10:11: main make(map[string]*int) does not escape

在这里,变量b,d,f的内存都被移动到堆上,因为,Golang中,slice,map,channel引用指针的变量,一定会逃逸。 Golang中,slice,map,channel对指针的引用会比之保留变量的slice,map,channel性能低,这里是根本原因。

闭包

func Fibonacci() func() int {    a, b := 0, 1    return func() int {        a, b = b, a+b        return a    }}

编译

$ go build -gcflags="-m" fib.go# command-line-arguments./fib.go:5:9: can inline Fibonacci.func1./fib.go:4:2: moved to heap: a./fib.go:4:5: moved to heap: b./fib.go:5:9: func literal escapes to heap# command-line-argumentsruntime.main_main·f: function main is undeclared in the main package

Fibonacci()函数中,a,b是一个本地的变量,因为被闭包引用,所以被分配在了堆上。

栈容量不足

看下面的代码:

func BigSlice() {    s := make([]int, 1000, 1000)    for index, _ := range s {        s[index] = index    }}func main() {    BigSlice()}

BigSlice()分配一个长度为1000的int数组,变量s是否会内存逃逸,取决与栈容量的大小是否足够。

$ go build -gcflags="-m" big.go# command-line-arguments./big.go:9:6: can inline main./big.go:4:11: make([]int, 1000, 1000) does not escape

这里,没有产生内存逃逸,如果将slice的长度增长10倍,就会产生逃逸。

$ go build -gcflags="-m" big.go# command-line-arguments./big.go:9:6: can inline main./big.go:4:11: make([]int, 10000, 10000) escapes to heap