![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5iZ1QmMiZTZlJjZwMzMwEGOhZjZ1cDMhVWOyYWMzUmZm9CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
文章目錄
靜态分析
超過堆棧架構的生命周期
尋址和解引用
微信公衆号: [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
:1: .this does not escape: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[1], ast example[2]
為了簡化分析, 下面我給出了一個簡化版的
抽象文法樹
的結果:
簡化的抽象文法樹
由于該樹暴露了定義的變量(用
NAME
表示)和對指針的操作(用
ADDR
或
DEREF
表示),故它可以向GoLang提供進行
逃逸分析
所需要的所有資訊。一旦建立了樹,并解析了函數和參數,GoLang現在就可以應用
逃逸分析
邏輯來檢視哪些應該是堆或棧配置設定的。
超過堆棧架構的生命周期
在運作
逃逸分析
并從AST圖中周遊函數(即: 标記)的同時,Go會尋找那些超過目前棧架構并是以需要進行堆配置設定的變量。假設沒有堆配置設定,在這個基礎上,通過前面例子的棧架構來表示,我們先來定義一下
outlive
的含義。下面是調用這兩個函數時,堆棧向下生長的情況。
棧記憶體
在這種情況下,變量
num
不能指向之前堆上配置設定的變量。在這種情況下,Go必須在
堆
上配置設定變量,確定它的生命周期超過堆棧架構的生命周期。
堆配置設定
變量
tmp
現在包含了配置設定給堆棧的記憶體位址,可以安全地從一個堆棧架構複制到另一個堆棧架構。然而,并不是隻有傳回的值才會失效。下面是規則:
• 任何傳回的值都會超過函數的生命周期,因為被調用的函數不知道這個值。• 在循環外聲明的變量在循環内的指派後會失效。如下面的例子:
• 在閉包外聲明的變量在閉包内的指派後失效。
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代碼:
簡化版 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 可以安全地在堆上配置設定它。
外部連結
[1] package ast https://golang.org/pkg/go/ast/#example_Print
[2] ast example https://golang.org/src/go/ast/example_test.go