Go 語言–變量逃逸
堆和棧各有優缺點,該怎麼在編程中處理這個問題呢?在 C/C++語言中,需要開發者自己學習如何進行內存分配,選用怎樣的內存分配方式來適應不同的算法需求。比如,函數局部變量儘量使用棧;全局變量、結構體成員使用堆分配等。程序員不得不花費很多年的時間在不同的項目中學習、記憶這些概念並加以實踐和使用。
Go語言將這個過程整合到編譯器中,命名爲“變量逃逸分析”。這個技術由編譯器分析代碼的特徵和代碼生命期,決定應該如何堆還是棧進行內存分配,即使程序員使用Go語言完成了整個工程後也不會感受到這個過程。
這樣的概念可能會讓你一知半解,來先看一段代碼
逃逸分析
package main
import "fmt"
func dummy(a int) int {
var b int = a
return b
}
func void() {
}
func main() {
// 聲明變量 c 並打印
var c int
void()
fmt.Println(c, dummy(0))
}
使用 go run 運行程序時,-gcflags 參數是編譯參數。其中 -m 表示進行內存分配分析,-l 表示避免程序內聯,也就是避免進行程序優化。
程序運行結果分析如下:
- 輸出第 2 行告知“main 的變量 c 逃逸到堆”。
- 第 3 行告知“dummy(0)調用逃逸到堆”。由於 dummy() 函數會返回一個整型值,這個值被 fmt.Println 使用後還是會在其聲明後繼續在 main() 函數中存在。
- 第 4 行,這句提示是默認的,可以忽略。
取地址發生逃逸
package main
import "fmt"
// 聲明空結構體測試結構體逃逸情況
type Data struct {
}
func dummy() *Data {
// 實例化c爲Data類型
var c Data
//返回函數局部變量地址
return &c
}
func main() {
fmt.Println(dummy())
}
代碼說明如下:
- 第 6 行,聲明一個空的結構體做結構體逃逸分析。
- 第 9 行,將 dummy() 函數的返回值修改爲 *Data 指針類型。
- 第 12 行,將 c 變量聲明爲 Data 類型,此時 c 的結構體爲值類型。
- 第 15 行,取函數局部變量 c 的地址並返回。Go語言的特性允許這樣做。
- 第 20 行,打印 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() 的返回值將是 Data 結構的一個不可預知的內存地址。這種情況一般是 C/C++ 語言中容易犯錯的地方:引用了一個函數局部變量的地址。
Go語言最終選擇將 c 的 Data 結構分配在堆上。然後由垃圾回收器去回收 c 的內存。
總結
在使用Go語言進行編程時,Go語言的設計者不希望開發者將精力放在內存應該分配在棧還是堆上的問題。編譯器會自動幫助開發者完成這個糾結的選擇。但變量逃逸分析也是需要了解的一個編譯器技術,這個技術不僅用於Go語言,在 Java等語言的編譯器優化上也使用了類似的技術。
編譯器覺得變量應該分配在堆和棧上的原則是:
- 變量是否被取地址。
- 變量是否發生逃逸。