1. 程式人生 > 其它 >【譯】Go: 逃逸分析介紹

【譯】Go: 逃逸分析介紹

逃逸分析是 Go 編譯器的一部分。它分析原始碼並確定哪些變數應該分配到棧上、哪些逃逸到堆上。

本文基於 Go 1.13

逃逸分析是 Go 編譯器的一部分。它分析原始碼並確定哪些變數應該分配到棧上、哪些逃逸到堆上。

靜態分析

Go 在編譯階段就定義了什麼應該在堆,什麼應該在棧上。在 go rungo build 時加上 -gcflags="-m" 就可以得到分析結果。
這裡有個簡單的例子:

func main() {
	num := getRandom()
	println(*num)
}

//go:noinline
func getRandom() *int {
	tmp := rand.Intn(100)
	return &tmp
}

逃逸分析告訴我們 tmp 逃逸到了堆上

./main.go:12:2: moved to heap: tmp

靜態分析的第一步時構建原始碼的抽象語法樹,允許 Go 理解在哪裡進行賦值和分配,以及變數定址和解引用。
下面是上一份程式碼的示例:

然而,為了放便分析,我們去掉 AST 的不相關資訊,可以得到一個簡單的版本

由於數公開了定義的變數(NAME 表示)和對指標的操作(ADDR 或 DEREF 表示),因此它將所有資訊提供給 Go 執行逃逸分析,一旦樹被構建並解析了函式和引數,Go 就可以使用逃逸分析邏輯檢視應該給哪些分配堆和棧。

存活時間超過棧幀

在執行逃逸分析並從 AST 圖中遍歷函式時,Go 會查詢比當前棧幀存活時間更長的變數,因此這些變數會分配到堆上。首先我們定義什麼是 outlive

,假如上個例子的棧幀中沒有堆分配。下面是兩個函式被呼叫時棧向下增長的情況:

那麼,當函式 getRandom 返回, 此函式建立的棧失效時,任何在函式棧上建立的變數都無法訪問。

在這種情況下,變數 num 不能指向在前一個棧上分配的變數。在這個例子中, Go 必須將變數分配到堆上,確保它比棧幀活的長:

變數 tmp 包含了分配到棧上的記憶體地址,並且可以安全的從一個棧複製到另一個棧。然而,返回值並不是唯一可以 outlive的值,它們的規則如下:

  • 任何返回值都超過該函式的壽命,因為被呼叫的函式不知道該值

  • 在迴圈外宣告的變數比迴圈內活的更久

    func main() {
      var l *int
      for i := 0; i < 10; i++ {
        l = new(int)
        *l = i
      }
      println(*l)
    }
    
    ./main.go:6:10: new(int) escapes to heap
    
  • 在閉包外部宣告的變數比在閉包內部賦值活的更久:

    func main() {
      var l *int
      func() {
        l = new(int)
        *l = 1
      }()
      println(*l)
    }
    
    ./main.go:8:3: new(int) escapes to heap
    

逃逸分析的第二部分包括確定它如何操作指標,幫助理解哪些內容可能留在棧上。

定址和解引用

構建表示定址/解引用計數的加權圖能讓 Go 優化棧的分配。讓我們分析一個例子來理解它是如何工作的:

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:10: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 可以安全的將它分配到堆上。