這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
0x01 什麼是逃逸
第一次聽說逃逸是在雨痕學堂,一臉懵逼的百度了半天也沒找到一個明確的說法,直到昨天在論壇上看到一篇關於變數逃逸的文章才明白。
因為函數都是運行在棧上的,在棧聲明臨時變數分配記憶體,函數運行完畢再回收該段棧空間,並且每個函數的棧空間都是獨立的,其他代碼都是不可訪問的。
但是在某些情況下,棧上的空間需要在該函數被釋放後依舊能訪問到,這時候就涉及到記憶體的逃逸了。
代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
type data struct { name string } //go:noinline func f1()data{ d := data{"maqian"} return d } //go:noinline func f2() *data { d := data{"maqian"} return &d } func main(){ d1 := f1() d2 := f2() }
|
f1和f2兩個函數都是建立一個變數返回,不同的是f1返回變數副本,f2返回變數指標。在大多數語言例如C/C++,類似f2的函數是不對的,因為d是一個臨時變數,return過後就會被釋放掉,返回毫無意義。但是在golang中,這種文法是允許的,它能正確的把d的地址返回到上層調用函數而不被釋放。
正如上面所說,該函數在運行完畢後肯定是要釋放的,內部分配的臨時記憶體也要釋放,所以d也應該被釋放。而為了讓d能被正確返回到上層調用,golang採取了一種記憶體策略,把d從棧拿到堆的中去,此時d就不會跟隨f2一同消亡了,這個過程就是一次逃逸。
0x02 編譯報告
代碼中兩個函數上方的go:noinline
注釋是一個編譯標記,讓編譯器不內聯當前代碼方便觀察編譯狀態。因為這裡代碼十分簡單,不給標記編譯器會自動內聯該段代碼看不到效果。
在編譯時間可以通過gcflags
選項帶上-m
參數查看到編譯狀態,-m
一共可以攜帶四個:
1 2 3 4 5 6 7 8 9 10
|
[ma@ma escape_analysis]$ go build -o app -gcflags "-m -m" # _/data/code/go/src/test-src/level6-other/escape_analysis ./escape.go:8:6: cannot inline f1: marked go:noinline ./escape.go:14:6: cannot inline f2: marked go:noinline ./escape.go:19:6: cannot inline main: non-leaf function ./escape.go:16:9: &d escapes to heap ./escape.go:16:9: from ~r0 (return) at ./escape.go:16:2 ./escape.go:15:20: moved to heap: d ./escape.go:20:8: d1 declared and not used ./escape.go:21:8: d2 declared and not used
|
通過第四行和第五行輸出很直接就能看出,在代碼的第16行,即函數f2
的return
處,參數d產生了逃逸行為。