『每周譯Go』Go如何做逃逸分析
垃圾回收是 Go - 自動內(nèi)存管理的一個便利功能, 使代碼更整潔,內(nèi)存泄漏的可能性更小。但是,GC 還會增加間接性能消耗,因?yàn)槌绦蛐枰ㄆ谕V共⑹占词褂玫膶ο蟆o 編譯器足夠智能,可以自動決定是否應(yīng)在堆上分配變量,之后需要在堆上收集垃圾,或者是否可以將其分配為該變量的函數(shù)的棧的一部分。棧與堆分配變量不同,棧分配變量不會產(chǎn)生任何 GC 開銷,因?yàn)樗鼈冊跅5钠溆嗖糠郑ó?dāng)功能返回時)被銷毀。
例如,Go 的逃生分析比HotSpot JVM 更基本。基本規(guī)則是,如果從申報的函數(shù)返回對變量的引用,則會"逃逸" - 函數(shù)返回后可以引用該變量,因此必須將其堆分配。這是比較復(fù)雜的,因?yàn)椋?/p>
調(diào)用其他功能的函數(shù) 分配給結(jié)構(gòu)體成員的引用 切片和maps cgo將指針指向變量
為了執(zhí)行逃生分析,Go 在編譯時構(gòu)建一個函數(shù)調(diào)用圖,并跟蹤輸入?yún)?shù)和返回值的流。函數(shù)可能引用其中一個參數(shù),但如果該引用未返回,變量不會逃逸。函數(shù)也可以返回引用,但在申明變量返回的函數(shù)之前,該引用可能由棧中的另一個函數(shù)取消引用或未返回。為了說明一些簡單的案例,我們可以運(yùn)行編譯器,這將打印詳細(xì)的逃生分析信息:-gcflags '-m'
package main
type S struct {}
func main() {
var x S
_ = identity(x)
}
func identity(x S) S {
return x
}
你必須用 go run -gcflags '-m -l' '-l'標(biāo)簽阻止功能被內(nèi)聯(lián) (這是另一個時間的主題) 來構(gòu)建這個功能。輸出是:什么都沒有!Go 使用值傳遞,因此始終將變量復(fù)制到棧中。在沒有引用的一般代碼中,總是很少使用棧分配。沒有逃生分析可做。再看下面一個例子:
package main
type S struct {}
func main() {
var x S
y := &x
_ = *identity(y)
}
func identity(z *S) *S {
return z
}
輸出:
$ go run -gcflags '-m -l' main.go
# command-line-arguments
.\main.go:11:15: leaking param: z to result ~r1 level=0
第一行顯示變量"流過":輸入變量返回為輸出。但不采取參考,所以變量不會逃逸。不在main返回之后沒有對x的引用存在,因此x分配在main的堆上。第三個實(shí)驗(yàn):
package main
type S struct {}
func main() {
var x S
_ = *ref(x)
}
func ref(z S) *S {
return &z
}
輸出:
$ go run -gcflags '-m -l' main.go
# command-line-arguments
.\main.go:10:10: moved to heap: z
現(xiàn)在有一些逃避正在發(fā)生。請記住,go是值傳遞,所以z是main中x變量的副本。返回z的引用,所以z不能是棧的一部分-返回時的參考點(diǎn)在哪里?取而代之的是它逃到堆。盡管 Go 在不取消計算參考值的情況下會立即扔掉引用,但 Go 的逃逸分析不夠精密,無法找出這一點(diǎn) - 它只查看輸入和返回變量的流。值得注意的是,在這種情況下,如果我們不阻止它,編譯器就會強(qiáng)調(diào)這一點(diǎn)。
如果將引用分配給結(jié)構(gòu)成員,該怎么辦?
package main
type S struct {
M *int
}
func main() {
var i int
refStruct(i)
}
func refStruct(y int) (z S) {
z.M = &y
return z
}
輸出:
$ go run -gcflags '-m -l' main.go
# command-line-arguments
.\main.go:13:16: moved to heap: y
在這種情況下,Go 仍然可以跟蹤引用流,即使引用是結(jié)構(gòu)體的成員。既然refStruct 做了引用并返回它,y就必須逃逸。與本案例相比:
package main
type S struct {
M *int
}
func main() {
var i int
refStruct(&i)
}
func refStruct(y *int) (z S) {
z.M = y
return z
}
輸出:
$ go run -gcflags '-m -l' main.go
# command-line-arguments
.\main.go:13:16: leaking param: y to result z level=0
由于main做了引用并傳遞refStruct,引用永遠(yuǎn)不會超過申報引用變量的棧。這和前面的程序有稍微不同的語義,但如果第二個程序足夠的話,它會更有效率:在第一個例子i必須分配在main的棧上,然后在堆上重新分配并將其復(fù)制為refStruct的參數(shù)。在第二個示例中i只分配一次,并傳遞引用。
一個更深入的例子:
package main
type S struct {
M *int
}
func main() {
var x S
var i int
ref(&i, &x)
}
func ref(y *int, z *S) {
z.M = y
}
輸出:
$ go run -gcflags '-m -l' main.go
# command-line-arguments
.\main.go:14:10: leaking param: y
.\main.go:14:18: z does not escape
.\main.go:10:6: moved to heap: i
這里的問題是 y 是分配給輸入結(jié)構(gòu)體的成員。Go 無法跟蹤該關(guān)系 - 輸入僅允許流到輸出 - 因此逃逸分析失敗,必須對變量進(jìn)行堆分配。有許多有據(jù)可查的案例(as of Go 1.5),由于go逃逸分析的限制,必須堆分配變量 -請參閱此鏈接(https://docs.google.com/document/d/1CxgUBPlx9iJzkz9JWkb6tIpTe5q32QDmz8l0BouG0Cw/preview) 。
最后,maps和切片呢?請記住,maps和切片實(shí)際上只是使用指針構(gòu)建到堆分配的內(nèi)存:切片結(jié)構(gòu)暴露在包中(SliceHeader : https://golang.org/pkg/reflect/#SliceHeader)中。map結(jié)構(gòu)是更難找到的,但它存在:hmap 。如果這些結(jié)構(gòu)無法逃逸,它們將被棧分配,但備份數(shù)組或哈希存儲桶中的數(shù)據(jù)本身將每次都堆分配。避免這種情況的唯一方法是分配一個固定大小的數(shù)組(如[10000]int)。
如果您已經(jīng)看過分析程序的堆使用情況 ,并且需要減少 GC 時間,則可能會從堆中移動頻繁分配的變量而獲得一些收獲。這也只是一個引人入勝的話題:要進(jìn)一步閱讀 HotSpot JVM 如何處理逃逸分析,請查看這篇文章(https://www.cc.gatech.edu/~harrold/6340/cs6340_fall2009/Readings/choi99escape.pdf) ,其中涉及堆棧分配,以及檢測何時可以消除同步。
www.gopherchina.org 還有 Gopher China 2021 重磅來襲
,期待 Gopher 們的到來!!!

