Go語言變數生命期和變數逃逸分析

我的世界ZHL發表於2019-05-17

什麼是棧

棧(Stack)是一種擁有特殊規則的線性表資料結構。

1) 概念

棧只允許往線性表的一端放入資料,之後在這一端取出資料,按照後進先出(LIFO,Last InFirst Out)的順序,如下圖
 


圖:棧的操作及擴充套件


往棧中放入元素的過程叫做入棧。入棧會增加棧的元素數量,最後放入的元素總是位於棧的頂部,最先放入的元素總是位於棧的底部。

從棧中取出元素時,只能從棧頂部取出。取出元素後,棧的數量會變少。最先放入的元素總是最後被取出,最後放入的元素總是最先被取出。不允許從棧底獲取資料,也不允許對棧成員(除棧頂外的成員)進行任何檢視和修改操作。

2) 變數和棧有什麼關係

棧可用於記憶體分配,棧的分配和回收速度非常快。

例:

  1. func calc(a, b int) int {
  2. var c int
  3. c = a * b
  4.  
  5. var x int
  6. x = c * 10
  7.  
  8. return x
  9. }

程式碼說明如下:

  • 第 1 行,傳入 a、b 兩個整型引數。
  • 第 2 行,宣告 c 整型變數,執行時,c 會分配一段記憶體用以儲存 c 的數值。
  • 第 3 行,將 a 和 b 相乘後賦予 c。
  • 第 5 行,宣告 x 整型變數,x 也會被分配一段記憶體。
  • 第 6 行,讓 c 乘以 10 後儲存到 x 變數中。
  • 第 8 行,返回 x 的值。

 

什麼是堆

堆在記憶體分配中類似於往一個房間裡擺放各種傢俱,傢俱的尺寸有大有小。分配記憶體時,需要找一塊足夠裝下傢俱的空間再擺放傢俱。經過反覆擺放和騰空傢俱後,房間裡的空間會變得亂七八糟,此時再往空間裡擺放傢俱會存在雖然有足夠的空間,但各空間分佈在不同的區域,無法有一段連續的空間來擺放傢俱的問題。

記憶體分配器就需要對這些空間進行調整優化,如下圖


圖:堆的分配及空間


堆分配記憶體和棧分配記憶體相比,堆適合不可預知大小的記憶體分配。缺點是分配速度較慢,而且會形成記憶體碎片。

 

變數逃逸(Escape Analysis)——自動決定變數分配方式,提高執行效率

Go 語言將這個過程整合到編譯器中,命名為“變數逃逸分析”。這個技術由編譯器分析程式碼的特徵和程式碼生命期,決定應該如何堆還是棧進行記憶體分配,即使程式設計師使用 Go 語言完成了整個工程後也不會感受到這個過程。

1) 逃逸分析

使用下面的程式碼來展現 Go 語言如何通過命令列分析變數逃逸,程式碼如下:


 
  1. package main
  2.  
  3. import "fmt"
  4.  
  5. // 本函式測試入口引數和返回值情況
  6. func dummy(b int) int {
  7.  
  8. // 宣告一個c賦值進入引數並返回
  9. var c int
  10. c = b
  11.  
  12. return c
  13. }
  14.  
  15. // 空函式, 什麼也不做
  16. func void() {
  17.  
  18. }
  19.  
  20. func main() {
  21.  
  22. // 宣告a變數並列印
  23. var a int
  24.  
  25. // 呼叫void()函式
  26. void()
  27.  
  28. // 列印a變數的值和dummy()函式返回
  29. fmt.Println(a, dummy(0))
  30. }

程式碼說明如下:

  • 第 6 行,dummy() 函式擁有一個引數,返回一個整型值,測試函式引數和返回值分析情況。
  • 第 9 行,宣告 c 變數,這裡演示函式臨時變數通過函式返回值返回後的情況。
  • 第 16 行,這是一個空函式,測試沒有任何引數函式的分析情況。
  • 第 23 行,在 main() 中宣告 a 變數,測試 main() 中變數的分析情況。
  • 第 26 行,呼叫 void() 函式,沒有返回值,測試 void() 呼叫後的分析情況。
  • 第 29 行,列印 a 和 dummy(0) 的返回值,測試函式返回值沒有變數接收時的分析情況。

 

2) 取地址發生逃逸

下面的例子使用結構體做資料,程式碼如下:


 
  1. package main
  2.  
  3. import "fmt"
  4.  
  5. // 宣告空結構體測試結構體逃逸情況
  6. type Data struct {
  7. }
  8.  
  9. func dummy() *Data {
  10.  
  11. // 例項化c為Data型別
  12. var c Data
  13.  
  14. //返回函式區域性變數地址
  15. return &c
  16. }
  17.  
  18. func main() {
  19.  
  20. fmt.Println(dummy())
  21. }

程式碼說明如下:

  • 第 6 行,宣告一個空的結構體做結構體逃逸分析。
  • 第 9 行,將 dummy() 函式的返回值修改為 *Data 指標型別。
  • 第 12 行,將 c 變數宣告為 Data 型別,此時 c 的結構體為值型別。
  • 第 15 行,取函式區域性變數 c 的地址並返回。Go 語言的特性允許這樣做。
  • 第 20 行,列印 dummy() 函式的返回值。



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

3) 原則

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

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

相關文章