參考資料:
go局部變量的存儲空間是堆還是棧: https://studygolang.com/articles/11878
Go的變量到底在堆還是棧中分配: https://studygolang.com/articles/7559
go變量逃逸分析: https://www.cnblogs.com/itbsl/p/10476674.html
Go逃逸分析最基本的原則是:如果一個函數返回對一個變量的引用
,那么它就會發生逃逸。
簡單來說,編譯器會分析代碼的特征和代碼生命周期,Go中的變量只有在編譯器可以證明在函數返回后不會再被引用的,才分配到棧上,其他情況下都是分配到堆上。
Go語言里沒有一個關鍵字或者函數可以直接讓變量被編譯器分配到堆上,相反,編譯器通過分析代碼來決定將變量分配到何處。變量是在堆還是棧上分配空間並不是由用var
還是new
聲明變量的方式決定的。
例
var global *int
func f() {
var x int
x = 1
global = &x
}
func g() {
y := new(int)
*y = 1
}
f函數里的x變量在堆上分配,因為它在函數退出后依然可以通過global變量找到,雖然它是在函數內部定義的;用Go語言的術語說,這個x局部變量從函數f中逃逸了。相反,當g函數返回時,變量y將是不可達的,也就是說可以馬上被回收的。因此,y並沒有從函數g中逃逸,編譯器可以選擇在棧上分配*y的存儲空間(譯注:也可以選擇在堆上分配,然后由Go語言的GC回收這個變量的內存空間),雖然這里用的是new方式。其實在任何時候,你並不需為了編寫正確的代碼而要考慮變量的逃逸行為,要記住的是,逃逸的變量需要額外分配內存,同時對性能的優化可能會產生細微的影響。
逃逸分析實例
Go提供了相關的命令,可以查看變量是否發生逃逸。
還是用上面我們提到的例子:
package main
import "fmt"
func foo() *int {
t := 3
return &t;
}
func main() {
x := foo()
fmt.Println(*x)
}
foo函數返回一個局部變量的指針,main函數里變量x接收它。執行如下命令:
go build -gcflags '-m -l' main.go
加-l是為了不讓foo函數被內聯。得到如下輸出:
# command-line-arguments
src/main.go:7:9: &t escapes to heap
src/main.go:6:7: moved to heap: t
src/main.go:12:14: *x escapes to heap
src/main.go:12:13: main ... argument does not escape
foo函數里的變量t逃逸了,和我們預想的一致。讓我們不解的是為什么main函數里的x也逃逸了?這是因為有些函數參數為interface類型,比如fmt.Println(a ...interface{}),編譯期間很難確定其參數的具體類型,也會發生逃逸。
使用反匯編命令也可以看出變量是否發生逃逸。
go tool compile -S main.go
截取部分結果,圖中標記出來的說明t是在堆上分配內存,發生了逃逸。