新聞中心
本文轉(zhuǎn)載自微信公眾號(hào)「腦子進(jìn)煎魚(yú)了」,作者陳煎魚(yú)。轉(zhuǎn)載本文請(qǐng)聯(lián)系腦子進(jìn)煎魚(yú)了公眾號(hào)。

東寶網(wǎng)站建設(shè)公司成都創(chuàng)新互聯(lián)公司,東寶網(wǎng)站設(shè)計(jì)制作,有大型網(wǎng)站制作公司豐富經(jīng)驗(yàn)。已為東寶成百上千提供企業(yè)網(wǎng)站建設(shè)服務(wù)。企業(yè)網(wǎng)站搭建\外貿(mào)網(wǎng)站建設(shè)要多少錢(qián),請(qǐng)找那個(gè)售后服務(wù)好的東寶做網(wǎng)站的公司定做!
大家好,我是正在學(xué)習(xí)蒸魚(yú)的煎魚(yú)。
我們?cè)趯?xiě)代碼的時(shí)候,有時(shí)候會(huì)想這個(gè)變量到底分配到哪里了?這時(shí)候可能會(huì)有人說(shuō),在棧上,在堆上。信我準(zhǔn)沒(méi)錯(cuò)...
但從結(jié)果上來(lái)講你還是一知半解,這可不行,萬(wàn)一被人懵了呢。今天我們一起來(lái)深挖下 Go 在這塊的奧妙,自己動(dòng)手豐衣足食!
問(wèn)題
- type User struct {
- ID int64
- Name string
- Avatar string
- }
- func GetUserInfo() *User {
- return &User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"}
- }
- func main() {
- _ = GetUserInfo()
- }
開(kāi)局就是一把問(wèn)號(hào),帶著問(wèn)題進(jìn)行學(xué)習(xí)。請(qǐng)問(wèn) main 調(diào)用 GetUserInfo 后返回的&User{...}。這個(gè)變量是分配到棧上了呢,還是分配到堆上了?
什么是堆/棧
在這里并不打算詳細(xì)介紹堆棧,僅簡(jiǎn)單介紹本文所需的基礎(chǔ)知識(shí)。如下:
- 堆(Heap):一般來(lái)講是人為手動(dòng)進(jìn)行管理,手動(dòng)申請(qǐng)、分配、釋放。一般所涉及的內(nèi)存大小并不定,一般會(huì)存放較大的對(duì)象。另外其分配相對(duì)慢,涉及到的指令動(dòng)作也相對(duì)多。
- 棧(Stack):由編譯器進(jìn)行管理,自動(dòng)申請(qǐng)、分配、釋放。一般不會(huì)太大,我們常見(jiàn)的函數(shù)參數(shù)(不同平臺(tái)允許存放的數(shù)量不同),局部變量等等都會(huì)存放在棧上。
今天我們介紹的 Go 語(yǔ)言,它的堆棧分配是通過(guò) Compiler 進(jìn)行分析,GC 去管理的,而對(duì)其的分析選擇動(dòng)作就是今天探討的重點(diǎn)。
什么是逃逸分析
在編譯程序優(yōu)化理論中,逃逸分析是一種確定指針動(dòng)態(tài)范圍的方法,簡(jiǎn)單來(lái)說(shuō)就是分析在程序的哪些地方可以訪問(wèn)到該指針。
通俗地講,逃逸分析就是確定一個(gè)變量要放堆上還是棧上,規(guī)則如下:
是否有在其他地方(非局部)被引用。只要有可能被引用了,那么它一定分配到堆上。否則分配到棧上。
即使沒(méi)有被外部引用,但對(duì)象過(guò)大,無(wú)法存放在棧區(qū)上。依然有可能分配到堆上。
對(duì)此你可以理解為,逃逸分析是編譯器用于決定變量分配到堆上還是棧上的一種行為。
在什么階段確立逃逸
在編譯階段確立逃逸,注意并不是在運(yùn)行時(shí)。
為什么需要逃逸
這個(gè)問(wèn)題我們可以反過(guò)來(lái)想,如果變量都分配到堆上了會(huì)出現(xiàn)什么事情?例如:
- 垃圾回收(GC)的壓力不斷增大。
- 申請(qǐng)、分配、回收內(nèi)存的系統(tǒng)開(kāi)銷(xiāo)增大(相對(duì)于棧)。
- 動(dòng)態(tài)分配產(chǎn)生一定量的內(nèi)存碎片。
簡(jiǎn)單來(lái)說(shuō),就是頻繁申請(qǐng)并分配堆內(nèi)存是有一定 “代價(jià)” 的。會(huì)影響應(yīng)用程序運(yùn)行的效率,間接影響到整體系統(tǒng)。
因此 “按需分配” 最大限度的靈活利用資源,才是正確的治理之道。這就是為什么需要逃逸分析的原因,你覺(jué)得呢?
怎么確定是否逃逸
第一,通過(guò)編譯器命令,就可以看到詳細(xì)的逃逸分析過(guò)程。而指令集 -gcflags 用于將標(biāo)識(shí)參數(shù)傳遞給 Go 編譯器,涉及如下:
- -m 會(huì)打印出逃逸分析的優(yōu)化策略,實(shí)際上最多總共可以用 4 個(gè) -m,但是信息量較大,一般用 1 個(gè)就可以了。
- -l 會(huì)禁用函數(shù)內(nèi)聯(lián),在這里禁用掉 inline 能更好的觀察逃逸情況,減少干擾。
- $ go build -gcflags '-m -l' main.go
第二,通過(guò)反編譯命令查看
- $ go tool compile -S main.go
注:可以通過(guò) go tool compile -help 查看所有允許傳遞給編譯器的標(biāo)識(shí)參數(shù)。
逃逸案例
案例一:指針
第一個(gè)案例是一開(kāi)始拋出的問(wèn)題,現(xiàn)在你再看看,想想,如下:
- type User struct {
- ID int64
- Name string
- Avatar string
- }
- func GetUserInfo() *User {
- return &User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"}
- }
- func main() {
- _ = GetUserInfo()
- }
執(zhí)行命令觀察一下,如下:
- $ go build -gcflags '-m -l' main.go
- # command-line-arguments
- ./main.go:10:54: &User literal escapes to heap
通過(guò)查看分析結(jié)果,可得知 &User 逃到了堆里,也就是分配到堆上了。這是不是有問(wèn)題啊...再看看匯編代碼確定一下,如下:
- $ go tool compile -S main.go
- "".GetUserInfo STEXT size=190 args=0x8 locals=0x18
- 0x0000 00000 (main.go:9) TEXT "".GetUserInfo(SB), $24-8
- ...
- 0x0028 00040 (main.go:10) MOVQ AX, (SP)
- 0x002c 00044 (main.go:10) CALL runtime.newobject(SB)
- 0x0031 00049 (main.go:10) PCDATA $2, $1
- 0x0031 00049 (main.go:10) MOVQ 8(SP), AX
- 0x0036 00054 (main.go:10) MOVQ $13746731, (AX)
- 0x003d 00061 (main.go:10) MOVQ $7, 16(AX)
- 0x0045 00069 (main.go:10) PCDATA $2, $-2
- 0x0045 00069 (main.go:10) PCDATA $0, $-2
- 0x0045 00069 (main.go:10) CMPL runtime.writeBarrier(SB), $0
- 0x004c 00076 (main.go:10) JNE 156
- 0x004e 00078 (main.go:10) LEAQ go.string."EDDYCJY"(SB), CX
- ...
我們將目光集中到 CALL 指令,發(fā)現(xiàn)其執(zhí)行了 runtime.newobject 方法,也就是確實(shí)是分配到了堆上。這是為什么呢?
分析結(jié)果
這是因?yàn)?GetUserInfo() 返回的是指針對(duì)象,引用被返回到了方法之外了。因此編譯器會(huì)把該對(duì)象分配到堆上,而不是棧上。
否則方法結(jié)束之后,局部變量就被回收了,豈不是翻車(chē)。所以最終分配到堆上是理所當(dāng)然的
再想想
那你可能會(huì)想,那就是所有指針對(duì)象,都應(yīng)該在堆上?并不。如下:
- func main() {
- str := new(string)
- *str = "EDDYCJY"
- }
你想想這個(gè)對(duì)象會(huì)分配到哪里?如下:
- $ go build -gcflags '-m -l' main.go
- # command-line-arguments
- ./main.go:4:12: main new(string) does not escape
顯然,該對(duì)象分配到棧上了。很核心的一點(diǎn)就是它有沒(méi)有被作用域之外所引用,而這里作用域仍然保留在 main 中,因此它沒(méi)有發(fā)生逃逸。
案例二:未確定類(lèi)型
- func main() {
- str := new(string)
- *str = "EDDYCJY"
- fmt.Println(str)
- }
執(zhí)行命令觀察一下,如下:
- $ go build -gcflags '-m -l' main.go
- # command-line-arguments
- ./main.go:9:13: str escapes to heap
- ./main.go:6:12: new(string) escapes to heap
- ./main.go:9:13: main ... argument does not escape
通過(guò)查看分析結(jié)果,可得知 str 變量逃到了堆上,也就是該對(duì)象在堆上分配。但上個(gè)案例時(shí)它還在棧上,我們也就 fmt 輸出了它而已。這...到底發(fā)生了什么事?
分析結(jié)果
相對(duì)案例一,案例二只加了一行代碼 fmt.Println(str),問(wèn)題肯定出在它身上。其原型:
- func Println(a ...interface{}) (n int, err error)
通過(guò)對(duì)其分析,可得知當(dāng)形參為 interface 類(lèi)型時(shí),在編譯階段編譯器無(wú)法確定其具體的類(lèi)型。因此會(huì)產(chǎn)生逃逸,最終分配到堆上。
如果你有興趣追源碼的話,可以看下內(nèi)部的 reflect.TypeOf(arg).Kind() 語(yǔ)句,其會(huì)造成堆逃逸,而表象就是 interface 類(lèi)型會(huì)導(dǎo)致該對(duì)象分配到堆上。
案例三、泄露參數(shù)
- type User struct {
- ID int64
- Name string
- Avatar string
- }
- func GetUserInfo(u *User) *User {
- return u
- }
- func main() {
- _ = GetUserInfo(&User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"})
- }
執(zhí)行命令觀察一下,如下:
- $ go build -gcflags '-m -l' main.go
- # command-line-arguments
- ./main.go:9:18: leaking param: u to result ~r1 level=0
- ./main.go:14:63: main &User literal does not escape
我們注意到 leaking param 的表述,它說(shuō)明了變量 u 是一個(gè)泄露參數(shù)。結(jié)合代碼可得知其傳給 GetUserInfo 方法后,沒(méi)有做任何引用之類(lèi)的涉及變量的動(dòng)作,直接就把這個(gè)變量返回出去了。
因此這個(gè)變量實(shí)際上并沒(méi)有逃逸,它的作用域還在 main() 之中,所以分配在棧上。
再想想
那你再想想怎么樣才能讓它分配到堆上?結(jié)合案例一,舉一反三。修改如下:
- type User struct {
- ID int64
- Name string
- Avatar string
- }
- func GetUserInfo(u User) *User {
- return &u
- }
- func main() {
- _ = GetUserInfo(User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"})
- }
執(zhí)行命令觀察一下,如下:
- $ go build -gcflags '-m -l' main.go
- # command-line-arguments
- ./main.go:10:9: &u escapes to heap
- ./main.go:9:18: moved to heap: u
只要一小改,它就考慮會(huì)被外部所引用,因此妥妥的分配到堆上了
總結(jié)
在本文我給你介紹了逃逸分析的概念和規(guī)則,并列舉了一些例子加深理解。但實(shí)際肯定遠(yuǎn)遠(yuǎn)不止這些案例,你需要做到的是掌握方法,遇到再看就好了。除此之外你還需要注意:
靜態(tài)分配到棧上,性能一定比動(dòng)態(tài)分配到堆上好。
底層分配到堆,還是棧。實(shí)際上對(duì)你來(lái)說(shuō)是透明的,不需要過(guò)度關(guān)心。
每個(gè) Go 版本的逃逸分析都會(huì)有所不同(會(huì)改變,會(huì)優(yōu)化)。
直接通過(guò) go build -gcflags '-m -l' 就可以看到逃逸分析的過(guò)程和結(jié)果。
到處都用指針傳遞并不一定是最好的,要用對(duì)。
這塊的知識(shí)點(diǎn)。我的建議是適當(dāng)了解,但沒(méi)必要硬記,因?yàn)?Go 語(yǔ)言每次升級(jí)都有可能會(huì)改??炕A(chǔ)知識(shí)點(diǎn)加命令調(diào)試觀察就好了。
像是曹大之前講的 “你琢磨半天逃逸分析,一壓測(cè),瓶頸在鎖上”,完全沒(méi)必要過(guò)度在意...
參考
- Golang escape analysis
- FAQ
- 逃逸分析
網(wǎng)站題目:靈魂拷問(wèn) Go 語(yǔ)言:這個(gè)變量到底分配到哪里了?
轉(zhuǎn)載源于:http://m.fisionsoft.com.cn/article/dhidopi.html


咨詢(xún)
建站咨詢(xún)
