天天看點

golang 傳回引用_GoLang中的逃逸分析簡介

golang 傳回引用_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
: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了解在哪裡進行了指派和配置設定,以及變量的尋址和解引用。

下面是之前代碼生成的

抽象文法樹

的一個例子:

golang 傳回引用_GoLang中的逃逸分析簡介

源代碼的抽象文法樹

關于抽象文法樹請參考: package ast[1], ast example[2]

為了簡化分析, 下面我給出了一個簡化版的

抽象文法樹

的結果:

golang 傳回引用_GoLang中的逃逸分析簡介

簡化的抽象文法樹

由于該樹暴露了定義的變量(用

NAME

表示)和對指針的操作(用

ADDR

DEREF

表示),故它可以向GoLang提供進行

逃逸分析

所需要的所有資訊。一旦建立了樹,并解析了函數和參數,GoLang現在就可以應用

逃逸分析

邏輯來檢視哪些應該是堆或棧配置設定的。

超過堆棧架構的生命周期

在運作

逃逸分析

并從AST圖中周遊函數(即: 标記)的同時,Go會尋找那些超過目前棧架構并是以需要進行堆配置設定的變量。假設沒有堆配置設定,在這個基礎上,通過前面例子的棧架構來表示,我們先來定義一下

outlive

的含義。下面是調用這兩個函數時,堆棧向下生長的情況。

golang 傳回引用_GoLang中的逃逸分析簡介

棧記憶體

在這種情況下,變量

num

不能指向之前堆上配置設定的變量。在這種情況下,Go必須在

上配置設定變量,確定它的生命周期超過堆棧架構的生命周期。

golang 傳回引用_GoLang中的逃逸分析簡介

堆配置設定

變量

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代碼:

golang 傳回引用_GoLang中的逃逸分析簡介

簡化版 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