Go語言之變數逃逸(Escape Analysis)分析

天ヾ道℡酬勤發表於2020-09-30

前面已經詳細分析過堆和棧的區別,變數是如何分配在堆和棧上的,go語言編譯器會自動決定把一個變數放在棧還是放在堆,編譯器會做逃逸分析(escape analysis),當發現變數的作用域沒有跑出函式範圍,就可以在棧上,反之則必須分配在堆。
但有時我們希望函式區域性變數儘量使用棧,全域性變數、結構體成員使用堆分配等。

那麼到底該如何分配呢?
Go語言將這個過程整合到了編譯器中,命名為“變數逃逸分析”。通過編譯器分析程式碼的特徵和程式碼的生命週期,決定應該使用堆還是棧來進行記憶體分配。

變數逃逸分析可以自動決定變數分配方式,提高執行效率。

逃逸分析

Go語言是如何使用命令列來分析變數逃逸?
示例:

package main

import "fmt"

// 本函式測試入口引數和返回值情況
func dummy(b int) int {    
    // 宣告一個變數c並賦值    
    var c int    
    c = b    
    
    return c
}

// 空函式, 什麼也不做
func void() {

}

func main() {   
    // 宣告a變數並列印   
    var a int    

    // 呼叫void()函式    
    void()    

    // 列印a變數的值和dummy()函式返回    
    fmt.Println(a, dummy(0))
}

解析:

  • dummy() 函式擁有一個引數,返回一個整型值,用來測試函式引數和返回值分析情況。
  • 宣告變數 c,用於演示函式臨時變數通過函式返回值返回後的情況。
  • func void():這是一個空函式,測試沒有任何引數函式的分析情況。
  • 在 main() 中宣告變數 a,測試 main() 中變數的分析情況。
  • 呼叫 void() 函式,沒有返回值,測試 void() 呼叫後的分析情況。
  • fmt.Println(a, dummy(0)):列印 a 和 dummy(0) 的返回值,測試函式返回值沒有變數接收時的分析情況。

接著使用如下命令列執行上面的程式碼:

go run -gcflags "-m -l" main.go

解析:

使用 go run 執行程式時,-gcflags 引數是編譯引數。其中 -m 表示進行記憶體分配分析,-l 表示避免程式內聯,也就是避免進行程式優化。

執行結果如下:

# command-line-arguments

./main.go:29:13: a escapes to heap

./main.go:29:22: dummy(0) escapes to heap

./main.go:29:13: main ... argument does not escape

0 0

程式執行結果分析如下:

  • 第 2 行告知“程式碼的第 29 行的變數 a 逃逸到堆”。
  • 第 3 行告知“dummy(0) 呼叫逃逸到堆”。由於 dummy() 函式會返回一個整型值,這個值被 fmt.Println使用後還是會在 main() 函式中繼續存在。
  • 第 4 行,這句提示是預設的,可以忽略。

上面例子中變數 c 是整型,其值通過 dummy() 的返回值“逃出”了 dummy() 函式。變數 c 的值被複制並作為 dummy() 函式的返回值返回,即使變數 c 在 dummy() 函式中分配的記憶體被釋放,也不會影響 main() 中使用 dummy() 返回的值。變數 c 使用棧分配不會影響結果。

取地址發生逃逸

使用結構體做資料,來了解結構體在堆上的分配情況。
示例:

package main

import "fmt"

// 宣告空結構體測試結構體逃逸情況
type Data struct {

}

func dummy() *Data {   
    
    // 例項化c為Data型別    
    var c Data    
    
    //返回函式區域性變數地址    
    return &c
}

func main() {    
   fmt.Println(dummy())
 }

解析:

  • type Data struct:宣告一個空的結構體做結構體逃逸分析。
  • func dummy() *Data:將 dummy() 函式的返回值修改為 *Data 指標型別。
  • var c Data:將變數 c 宣告為 Data 型別,此時 c 的結構體為值型別。
  • return &c:取函式區域性變數 c 的地址並返回。
  • fmt.Println(dummy()):列印 dummy() 函式的返回值。

執行逃逸分析:

go run -gcflags "-m -l" main.go

# command-line-arguments

./main.go:15:9: &c escapes to heap

./main.go:12:6: moved to heap: c

./main.go:20:19: dummy() escapes to heap

./main.go:20:13: main ... argument does not escape

&{}

注意第 4 行出現了新的提示:將 c 移到堆中。
這句話表示,Go 編譯器已經確認如果將變數 c 分配在棧上是無法保證程式最終結果的,如果這樣做,dummy() 函式的返回值將是一個不可預知的記憶體地址,這種情況一般是 C/C++ 語言中容易犯錯的地方,引用了一個函式區域性變數的地址。

Go語言最終選擇將 c 的 Data 結構分配在堆上。然後由垃圾回收器去回收 c 的記憶體。

原則

在使用Go語言進行程式設計時,為了不將精力放在記憶體應該分配在棧還是堆的問題上,編譯器會自動幫助開發者完成這個糾結的選擇,但變數逃逸分析也是需要了解的一個編譯器技術,這個技術不僅用於Go語言,在 Java 等語言的編譯器優化上也使用了類似的技術。

編譯器覺得變數應該分配在堆和棧上的原則是:

  • 變數是否被取地址
  • 變數是否發生逃逸

相關文章