Go語言中的逃逸分析究竟是什么?
1、逃逸分析介紹
學(xué)計(jì)算機(jī)的同學(xué)都知道,在編譯原理中,分析指針動(dòng)態(tài)范圍的方法稱之為逃逸分析。通俗來講,當(dāng)一個(gè)對(duì)象的指針被多個(gè)方法或線程引用時(shí),我們稱這個(gè)指針發(fā)生了“逃逸”。
Go語言的逃逸分析是編譯器執(zhí)行靜態(tài)代碼分析后,對(duì)內(nèi)存管理進(jìn)行的優(yōu)化和簡(jiǎn)化,它可以決定一個(gè)變量是分配到堆還棧上。
寫過C/C++的小伙伴應(yīng)該知道,使用比較經(jīng)典的malloc
和new
函數(shù)可以在堆上分配一塊內(nèi)存,這塊內(nèi)存的使用和回收(銷毀)的任務(wù)在程序員中,處理不當(dāng),很可能會(huì)發(fā)生內(nèi)存泄露。
2、Go中內(nèi)存分配在哪里?
但是在Go語言中,基本不用擔(dān)心內(nèi)存泄露的問題,因?yàn)閮?nèi)存回收Go語言中已經(jīng)幫我們處理了(GC回收機(jī)制)。雖然也有new函數(shù),但是使用new
函數(shù)得到的內(nèi)存不一定就在堆上。堆和棧的區(qū)別對(duì)程序員“模糊化”了,當(dāng)然這一切都是Go編譯器在背后幫我們完成的。
Go語言逃逸分析最基本的原則是:如果一個(gè)函數(shù)返回對(duì)一個(gè)變量的引用,那么它就會(huì)發(fā)生逃逸。
簡(jiǎn)單來說,編譯器會(huì)分析代碼的特征和代碼生命周期,Go中的變量只有在編譯器可以證明在函數(shù)返回后不會(huì)再被引用的,才分配到棧上,其他情況下都是分配到堆上。
Go語言里沒有一個(gè)關(guān)鍵字或者函數(shù)可以直接讓變量被編譯器分配到堆上,相反,編譯器通過分析代碼來決定將變量分配到何處。
對(duì)一個(gè)變量取地址,可能會(huì)被分配到堆上。但是編譯器進(jìn)行逃逸分析后,如果考察到在函數(shù)返回后,此變量不會(huì)被引用,那么還是會(huì)被分配到棧上。
編譯器會(huì)根據(jù)變量是否被外部引用來決定是否逃逸:
- 如果在函數(shù)外面沒有引用到,則優(yōu)先放到棧區(qū)中;
- 如果在函數(shù)外面存在引用的可能,則就會(huì)放到堆區(qū)中;
當(dāng)我們寫C/C++代碼時(shí),為了提高效率,會(huì)經(jīng)常將pass-by-value
(傳值)提升成pass-by-reference
,企圖避免構(gòu)造函數(shù)的運(yùn)行,并且直接返回一個(gè)指針。
你一定還記得,這里隱藏了一個(gè)很大的坑:在函數(shù)內(nèi)部定義了一個(gè)局部變量,然后返回這個(gè)局部變量的地址(指針)。這些局部變量是在棧上分配的(靜態(tài)內(nèi)存分配),一旦函數(shù)執(zhí)行完畢,變量占據(jù)的內(nèi)存會(huì)被銷毀,任何對(duì)這個(gè)返回值作的動(dòng)作(如解引用),都將擾亂程序的運(yùn)行,甚至導(dǎo)致程序直接崩潰。比如下面的這段代碼:
int *foo ( void ) { int t = 3; return &t; }
有些同學(xué)可能知道上面這個(gè)坑,用了個(gè)更聰明的做法:在函數(shù)內(nèi)部使用new函數(shù)構(gòu)造一個(gè)變量(動(dòng)態(tài)內(nèi)存分配),然后返回此變量的地址。因?yàn)樽兞渴窃诙焉蟿?chuàng)建的,所以函數(shù)退出時(shí)不會(huì)被銷毀。
但是,這樣就行了嗎?new
出來的對(duì)象該在何時(shí)何地delete
呢?調(diào)用者可能會(huì)忘記delete或者直接拿返回值傳給其他函數(shù),之后就再也不能delete
它了,也就是發(fā)生了內(nèi)存泄露。關(guān)于這個(gè)坑,大家可以去看看《Effective C++》條款21,講得非常好!
3、Go與C++內(nèi)存分配的區(qū)別
上面講的C/C++
中會(huì)遇到的問題,在Go中作為一個(gè)語言特性被大力推崇,可以解決以上的難點(diǎn)!
C/C++中的動(dòng)態(tài)分配的內(nèi)存需要我們手動(dòng)來釋放,這樣會(huì)帶來一個(gè)問題:有些內(nèi)存處理不當(dāng)或回收不及時(shí),導(dǎo)致內(nèi)存泄露。
但是這樣的好處是:開發(fā)人員可以自己管理內(nèi)存。
Go的垃圾回收,讓堆和棧對(duì)程序員保持透明。真正解放了程序員的雙手,讓他們可以專注于業(yè)務(wù),“高效”地完成代碼編寫。把那些內(nèi)存管理的復(fù)雜機(jī)制交給編譯器,而程序員可以去享受生活。
4、逃逸分析騷操作
逃逸分析這種“騷操作”把變量合理地分配到它該去的地方。即使你是用new申請(qǐng)到的內(nèi)存,如果我發(fā)現(xiàn)你竟然在退出函數(shù)后沒有用了,那么就把你丟到棧上,畢竟棧上的內(nèi)存分配比堆上快很多;反之,即使你表面上只是一個(gè)普通的變量,但是經(jīng)過逃逸分析后發(fā)現(xiàn)在退出函數(shù)之后還有其他地方在引用,那我就把你分配到堆上。
如果變量都分配到堆上,堆不像棧可以自動(dòng)清理。它會(huì)引起Go頻繁地進(jìn)行垃圾回收,而垃圾回收會(huì)占用比較大的系統(tǒng)開銷(占用CPU容量的25%)。
堆和棧相比,堆適合不可預(yù)知大小的內(nèi)存分配。但是為此付出的代價(jià)是分配速度較慢,而且會(huì)形成內(nèi)存碎片。棧內(nèi)存分配則會(huì)非???。棧分配內(nèi)存只需要兩個(gè)CPU指令:“PUSH
”和“RELEASE
”,分配和釋放;而堆分配內(nèi)存首先需要去找到一塊大小合適的內(nèi)存塊,之后要通過垃圾回收才能釋放。
通過逃逸分析,可以盡量把那些不需要分配到堆上的變量直接分配到棧上,堆上的變量少了,會(huì)減輕分配堆內(nèi)存的開銷,同時(shí)也會(huì)減少gc
的壓力,提高程序的運(yùn)行速度。
5、逃逸分析引申示例說明
引申1:如何查看某個(gè)變量是否發(fā)生了逃逸?兩種方法:使用go命令,查看逃逸分析結(jié)果;反匯編源碼;
比如用這個(gè)例子:
package main import "fmt" func foo() *int { t := 3 return &t; } func main() { x := foo() fmt.Println(*x) }
使用go命令:
go build -gcflags '-m -l' main.go
加-l是為了不讓foo
函數(shù)被內(nèi)聯(lián)。得到如下輸出:
# 命令行變量 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
函數(shù)里的變量t逃逸了,和我們預(yù)想的一致。讓我們不解的是為什么main
函數(shù)里的x也逃逸了?這是因?yàn)橛行┖瘮?shù)參數(shù)為interface
類型,比如fmt.Println(a …interface{})
,編譯期間很難確定其參數(shù)的具體類型,也會(huì)發(fā)生逃逸。
反匯編代碼比較難理解,這里就不講了。
引申2:下面代碼中的變量發(fā)生逃逸了嗎?
先來看示例1:
package main type S struct {} func main() { var x S _ = identity(x) } func identity(x S) S { return x }
分析:Go語言函數(shù)傳遞都是通過值的,調(diào)用函數(shù)的時(shí)候,直接在棧上copy出一份參數(shù),不存在逃逸。
再來看示例二:
package main type S struct {} func main() { var x S y := &x _ = *identity(y) } func identity(z *S) *S { return z }
分析:identity
函數(shù)的輸入直接當(dāng)成返回值了,因?yàn)闆]有對(duì)z作引用,所以z沒有逃逸。對(duì)x的引用也沒有逃出main
函數(shù)的作用域,因此x也沒有發(fā)生逃逸。
繼續(xù)看示例三:
package main type S struct {} func main() { var x S _ = *ref(x) } func ref(z S) *S { return &z }
分析:z是對(duì)x的拷貝,ref函數(shù)中對(duì)z取了引用,所以z不能放在棧上,否則在ref函數(shù)之外,通過引用如何找到z,所以z必須要逃逸到堆上。僅管在main函數(shù)中,直接丟棄了ref的結(jié)果,但是Go的編譯器還沒有那么智能,分析不出來這種情況。而對(duì)x從來就沒有取引用,所以x不會(huì)發(fā)生逃逸。
還有示例四:如果對(duì)一個(gè)結(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 }
分析:refStruct
函數(shù)對(duì)y取了引用,所以y發(fā)生了逃逸。
最后看示例五:
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 }
分析:在main
函數(shù)里對(duì)i取了引用,并且把它傳給了refStruct
函數(shù),i的引用一直在main
函數(shù)的作用域用,因此i沒有發(fā)生逃逸。和上一個(gè)例子相比,有一點(diǎn)小差別,但是導(dǎo)致的程序效果是不同的:例子4中,i先在main
的棧幀中分配,之后又在refStruct
棧幀中分配,然后又逃逸到堆上,到堆上分配了一次,共3次分配。本例中,i只分配了一次,然后通過引用傳遞。
到此這篇關(guān)于Go語言中的逃逸分析究竟是什么?的文章就介紹到這了,更多相關(guān)Go語言中的逃逸內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言編程中判斷文件是否存在是創(chuàng)建目錄的方法
這篇文章主要介紹了Go語言編程中判斷文件是否存在是創(chuàng)建目錄的方法,示例都是使用os包下的函數(shù),需要的朋友可以參考下2015-10-10go語言中的數(shù)組指針和指針數(shù)組的區(qū)別小結(jié)
本文主要介紹了go語言中的數(shù)組指針和指針數(shù)組的區(qū)別小結(jié),文中通過示例代碼介紹的很詳細(xì),具有一定的參考價(jià)值,感興趣的可以了解一下2024-10-10Golang項(xiàng)目在github創(chuàng)建release后自動(dòng)生成二進(jìn)制文件的方法
這篇文章主要介紹了Golang項(xiàng)目在github創(chuàng)建release后如何自動(dòng)生成二進(jìn)制文件,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03基于go手動(dòng)寫個(gè)轉(zhuǎn)發(fā)代理服務(wù)的代碼實(shí)現(xiàn)
這篇文章主要介紹了基于go手動(dòng)寫個(gè)轉(zhuǎn)發(fā)代理服務(wù)的代碼實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02gtoken替換jwt實(shí)現(xiàn)sso登錄的排雷避坑
這篇文章主要為大家介紹了gtoken替換jwt實(shí)現(xiàn)sso登錄的排雷避坑,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06