
微信公眾號:[double12gzh]
關注容器技術、關注Kubernetes。問題或建議,請公眾號留言。
本篇文章基於GoLang 1.13.
逃逸分析是GoLang編譯器中的一個階段,它通過分析用戶源碼,決定哪些變量應該在堆棧上分配,哪些變量應該逃逸到堆中。
靜態分析
Go靜態地定義了在編譯階段應該被堆或棧分配的內容。當編譯(go build)和/或運行(go run)你的代碼時,可以通過標志-gcflags="-m "進行分析。下面是一個簡單的例子。
package main
import "fmt"
func main() {
num := GenerateRandomNum()
fmt.Println(*num)
}
//go:noinline
func GenerateRandomNum() *int {
tmp := rand.Intn(500)
return &tmp
}
運行逃逸分析,具體命令如下:
F:\hello>go build -gcflags="-m" main.go
# command-line-arguments
.\main.go:15:18: inlining call to rand.Intn
.\main.go:10:13: inlining call to fmt.Println
.\main.go:15:2: moved to heap: tmp
.\main.go:10:14: *num escapes to heap
.\main.go:10:13: []interface {} literal does not escape
<autogenerated>:1: .this does not escape
<autogenerated>:1: .this does not escape
從上面的結果.\main.go:15:2: moved to heap: tmp中我們發現tmp逃逸到了堆中。
靜態分析的第一步是生成源碼的抽象語法樹(具體命令:go build -gcflags="-m -m -m -m -m -W -W" main.go),讓GoLang了解在哪里進行了賦值和分配,以及變量的尋址和解引用。
下面是之前代碼生成的抽象語法樹的一個例子:

關於抽象語法樹請參考: package ast, ast example
為了簡化分析, 下面我給出了一個簡化版的抽象語法樹的結果:

由於該樹暴露了定義的變量(用NAME表示)和對指針的操作(用ADDR或DEREF表示),故它可以向GoLang提供進行逃逸分析所需要的所有信息。一旦建立了樹,並解析了函數和參數,GoLang現在就可以應用逃逸分析邏輯來查看哪些應該是堆或棧分配的。
超過堆棧框架的生命周期
在運行逃逸分析並從AST圖中遍歷函數(即: 標記)的同時,Go會尋找那些超過當前棧框架並因此需要進行堆分配的變量。假設沒有堆分配,在這個基礎上,通過前面例子的棧框架來表示,我們先來定義一下outlive的含義。下面是調用這兩個函數時,堆棧向下生長的情況。

在這種情況下,變量num不能指向之前堆上分配的變量。在這種情況下,Go必須在堆上分配變量,確保它的生命周期超過堆棧框架的生命周期。

變量tmp現在包含了分配給堆棧的內存地址,可以安全地從一個堆棧框架復制到另一個堆棧框架。然而,並不是只有返回的值才會失效。下面是規則:
- 任何返回的值都會超過函數的生命周期,因為被調用的函數不知道這個值。
- 在循環外聲明的變量在循環內的賦值后會失效。如下面的例子:
package main
func main() {
var l *int
for i := 0; i < 10; i++ {
l = new(int)
*l = i
}
println(*l)
}
./main.go:8:10: new(int) escapes to heap
- 在閉包外聲明的變量在閉包內的賦值后失效。
package main
func main() {
var l *int
func() {
l = new(int)
*l = 1
}()
println(*l)
}
./main.go:10:3: new(int) escapes to heap
逃逸分析的第二部分包括確定它是如何操作指針的,幫助了解哪些東西可能會留在堆棧上。
尋址和解引用
構建一個表示尋址/引用次數的加權圖,可以讓Go優化堆棧分配。讓我們分析一個例子來了解它是如何工作的:
package main
func main() {
n := getAnyNumber()
println(*n)
}
//go:noinline
func getAnyNumber() *int {
l := new(int)
*l = 42
m := &l
n := &m
o := **n
return o
}
運行逃逸分析表明,分配逃逸到了堆。
./main.go:12:10: new(int) escapes to heap
下面是一個簡化版的AST代碼:

Go通過建立加權圖來定義分配。每一次解引用,在代碼中用*表示,或者在節點中用DEREF表示,權重增加1;每一次尋址操作,在代碼中用&表示,或者在節點中用ADDR表示,權重減少1。
下面是由逃逸分析定義的序列:
variable o has a weight of 0, o has an edge to n
variable n has a weight of 2, n has an edge to m
variable m has a weight of 1, m has an edge to l
variable l has a weight of 0, l has an edge to new(int)
variable new(int) has a weight of -1
每個變量最后的計數為負數,如果超過了當前的棧幀,就會逃逸到堆中。由於返回的值超過了其函數的堆棧框架,並通過其邊緣得到了負數,所以分配逃到了堆上。
構建這個圖可以讓Go了解哪個變量應該留在棧上(盡管它超過了棧的時間)。下面是另一個基本的例子:
func main() {
num := func1()
println(*num)
}
//go:noinline
func func1() *int {
n1 := func2()
*n1++
return n1
}
//go:noinline
func func2() *int {
n2 := rand.Intn(99)
return &n2
}
./main.go:20:2: moved to heap: n2
變量n1超過了堆棧框架,但它的權重不是負數,因為func1沒有在任何地方引用它的地址。
然而,n2會超過棧幀並被取消引用,Go 可以安全地在堆上分配它。
