Go語言之變數逃逸(Escape Analysis)分析
前面已經詳細分析過堆和棧的區別,變數是如何分配在堆和棧上的,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 等語言的編譯器優化上也使用了類似的技術。
編譯器覺得變數應該分配在堆和棧上的原則是:
- 變數是否被取地址
- 變數是否發生逃逸
相關文章
- Go語言變數生命期和變數逃逸分析Go變數
- Go語言之methodGo
- Go語言之ContextGoContext
- Go語言之介面Go
- [Dart]Dart語言之旅<二>:變數Dart變數
- Go記憶體逃逸分析Go記憶體
- go語言之反射-------ReflectionGo反射
- Go語言之 Struct TagGoStruct
- Go記憶體管理逃逸分析Go記憶體
- 深度解密Go語言之Slice解密Go
- 深度解密 Go 語言之 channel解密Go
- 深度解密Go語言之channel解密Go
- 深度解密Go語言之context解密GoContext
- 深度解密Go語言之 map解密Go
- 深度解密 Go 語言之 context解密GoContext
- 深度解密GO語言之反射解密Go反射
- Go語言之包(package)管理GoPackage
- Go語言之讀寫鎖Go
- go語言變數Go變數
- go語言之陣列與切片Go陣列
- Go語言之旅:基本型別Go型別
- Go語言之錯誤處理Go
- Go語言之併發示例(Runner)Go
- GO語言————4.4 變數Go變數
- 面試官:簡單聊聊 Go 逃逸分析?面試Go
- 從一個例子看Go的逃逸分析Go
- go語言之結構體和方法Go結構體
- 深度解密 Go 語言之 sync.map解密Go
- 深度解密 Go 語言之 sync.Pool解密Go
- R語言-Survival analysis(生存分析)R語言
- 初學Go語言 變數Go變數
- 你為什麼不應該過度關注go語言的逃逸分析Go
- 在 Fefora 上開啟 Go 語言之旅Go
- python和GO語言之間的區別!PythonGo
- Go語言之切片(slice)快速入門篇Go
- Go語言之陣列快速入門篇Go陣列
- Go語言中的變數作用域Go變數
- Go 語言入門教程:變數Go變數