Go語言中的逃逸分析究竟是什么?

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

