GoLang中的逃逸分析簡介
微信公眾號:[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 可以安全地在堆上分配它。