詳解Go語(yǔ)言中的逃逸分析
什么是逃逸
一句話,逃逸分析是編譯器用于決定將變量分配到棧上還是堆上的一種行為。
眾所周知,函數(shù)的運(yùn)行都在操作系統(tǒng)內(nèi)存空間中的??臻g內(nèi)。我們?cè)跅I下暶髋R時(shí)變量,分配內(nèi)存,函數(shù)運(yùn)行完畢后,回收內(nèi)存。每個(gè)函數(shù)的??臻g都是獨(dú)立的,其他函數(shù)沒(méi)有權(quán)限訪問(wèn)。但在某些情況下,我們需要在函數(shù)結(jié)束以后訪問(wèn)棧上面的某些數(shù)據(jù),這就涉及到內(nèi)存逃逸了。
如果變量從棧上逃逸,那么他會(huì)逃到哪兒去呢?他會(huì)跑到堆上。由于棧上的變量是在函數(shù)結(jié)束的時(shí)候自動(dòng)進(jìn)行回收,回收代價(jià)比較??;而堆空間分配內(nèi)存,則首先需要找到一塊大小合適的內(nèi)存,之后通過(guò)GC回收才能釋放。對(duì)于這種情況,頻繁使用垃圾回收會(huì)占用比較大的開(kāi)銷(xiāo),所以要盡量分配內(nèi)存到棧上,減少GC的壓力。
逃逸分析基本過(guò)程
Go語(yǔ)言的逃逸分析最基本的原則:如果一個(gè)函數(shù)返回一個(gè)對(duì)變量的引用,那么他就會(huì)發(fā)生逃逸。
在任何情況下,如果一個(gè)值被分配到了??臻g以外的地方,那么它一定是被分配到了堆上。簡(jiǎn)言之:編譯器會(huì)分析代碼的特征和生命周期,Go中的變量只有在編譯器可以證明在函數(shù)返回后不會(huì)再被引用的情況下,才會(huì)被分配到棧上,否則會(huì)被分配到堆上。
不同于C++中的new,Go語(yǔ)言中的new關(guān)鍵字不一定會(huì)將內(nèi)存分配到堆空間上,在Go語(yǔ)言中,沒(méi)有關(guān)鍵字或者函數(shù)可以直接將變量分配到堆上,而是通過(guò)編譯器來(lái)分析代碼決定將變量分配到何處。
一句話:
編譯器會(huì)根據(jù)變量是否被外部引用來(lái)決定是否逃逸。
如果函數(shù)外部沒(méi)有引用,則優(yōu)先放到棧中;
如果函數(shù)外部存在引用,則必定放到堆中;
常見(jiàn)逃逸情況
指針逃逸
我們知道傳遞指針可以減少底層值的拷貝,提高效率。但是如果拷貝的數(shù)據(jù)量小,指針傳遞會(huì)產(chǎn)生逃逸,可能會(huì)使用堆空間,增加GC負(fù)擔(dān),所以傳遞指針不一定是高效的。
比如:
package main type Student struct { Name string Age int } func StudentRegister(name string, age int) *Student { s := new(Student) // 局部變量s逃逸到堆 s.Name = name s.Age = age return s } func main() { StudentRegister("similar", 18) }
雖然函數(shù)StudentRegister
內(nèi)部s為局部變量,但是由于返回了指針,其指向的內(nèi)存地址不會(huì)是棧而是堆,這是典型的逃逸案例。
使用命令 go build -gcflags '-m -l' main.go
,得到:
# command-line-arguments
./escape.go:8:22: leaking param: name
./escape.go:9:10: new(Student) escapes to heap # 表示該行內(nèi)存發(fā)生了逃逸現(xiàn)象
棧空間不足
如果分配太大容量的slice在棧上,當(dāng)??臻g不足存放當(dāng)前對(duì)象或者無(wú)法判斷當(dāng)前切片長(zhǎng)度時(shí)就會(huì)將對(duì)象分配到堆中。
package main func MakeSlice() { s := make([]int, 10000, 10000) for index := range(s){ s[index] = index } } func main() { MakeSlice() }
同樣使用命令go build -gcflags '-m -l' main.go
:
# command-line-arguments
./escape_1.go:4:11: make([]int, 10000) escapes to heap
動(dòng)態(tài)類(lèi)型逃逸
很多函數(shù)的參數(shù)為interface
類(lèi)型,比如:
func Printf(format string, a ...interface{}) (n int, err error) func Scanf(format string, a ...interface{}) (n int, err error) func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
在編譯時(shí)很難確定其參數(shù)的具體類(lèi)型,也能產(chǎn)生逃逸。
變量大小不確定
在創(chuàng)建切片,初始化切片容量的時(shí)候,有時(shí)會(huì)傳入一個(gè)變量指定其大小,由于變量的值不能被編譯器確定,所以不能確定其占用空間大小,從而編譯器可能會(huì)直接將變量分配到堆上。
package main func MakeSlice() { length := 1 a := make([]int, length, length) for i := 0; i < length; i++ { a[i] = i } } func main() { MakeSlice() }
編譯結(jié)果:
# command-line-arguments
./escape_1.go:5:11: make([]int, length, length) escapes to heap
常見(jiàn)的逃逸情況總結(jié)
指針逃逸:函數(shù)內(nèi)部返回一個(gè)局部變量指針
分配大對(duì)象:導(dǎo)致??臻g不足,不得不分配到堆上
調(diào)用接口類(lèi)型的方法,接口類(lèi)型的方法調(diào)用是動(dòng)態(tài)調(diào)度 - 實(shí)際使用的具體實(shí)現(xiàn)只能在運(yùn)行時(shí)確定。
盡管能夠符合分配到棧的場(chǎng)景,但是其大小不能在編譯的時(shí)候確定,也會(huì)分配到堆上。
如何避免
Go中的接口類(lèi)型的方法調(diào)用是動(dòng)態(tài)調(diào)度,因此不能夠在編譯階段確定,所有類(lèi)型結(jié)構(gòu)轉(zhuǎn)換成接口的過(guò)程會(huì)涉及到內(nèi)存逃逸的情況發(fā)生。如果對(duì)于性能要求比較高且訪問(wèn)頻次比較高的函數(shù)調(diào)用,應(yīng)該盡量避免使用接口類(lèi)型。
由于切片一般都是使用在函數(shù)傳遞的場(chǎng)景下,而且切片在append的時(shí)候可能會(huì)涉及到重新分配內(nèi)存,如果切片在編譯期間的大小不能夠確認(rèn)或者大小超出棧的限制,多數(shù)情況下都會(huì)被分配到堆上。
總結(jié)
堆上分配內(nèi)存比棧上分配內(nèi)存,開(kāi)銷(xiāo)大很多。
變量分配在棧上需要能夠在編譯期確定他的作用域,否則會(huì)分配到堆上。
Go語(yǔ)言編譯器會(huì)通過(guò)變量是否被外部引用類(lèi)決定是否逃逸
通過(guò)go build -gcflags '-m'
命令可以觀察變量是否逃逸
不能盲目使用變量的指針作為函數(shù)參數(shù),雖然會(huì)減少?gòu)?fù)制操作,但是當(dāng)參數(shù)為變量自身的時(shí)候,復(fù)制是在棧上完成的操作,開(kāi)銷(xiāo)遠(yuǎn)比變量逃逸后動(dòng)態(tài)地在堆上分配內(nèi)存少得多。
到此這篇關(guān)于詳解Go語(yǔ)言中的逃逸分析的文章就介紹到這了,更多相關(guān)Go逃逸分析內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go語(yǔ)言實(shí)現(xiàn)猜數(shù)字小游戲的方法
這篇文章主要介紹了go語(yǔ)言實(shí)現(xiàn)猜數(shù)字小游戲的方法,實(shí)例分析了Go語(yǔ)言流程判斷與處理的技巧,需要的朋友可以參考下2015-03-03Golang pipe在不同場(chǎng)景下遠(yuǎn)程交互
這篇文章主要介紹了Golang pipe在不同場(chǎng)景下遠(yuǎn)程交互,pipe實(shí)現(xiàn)從一個(gè)進(jìn)程重定向至另一個(gè)進(jìn)程,它是雙向數(shù)據(jù)通道,用于實(shí)現(xiàn)進(jìn)行間通信2023-03-03Golang的os標(biāo)準(zhǔn)庫(kù)中常用函數(shù)的整理介紹
這篇文章主要介紹了Go語(yǔ)言的os標(biāo)準(zhǔn)庫(kù)中常用函數(shù),主要用來(lái)實(shí)現(xiàn)與操作系統(tǒng)的交互功能,需要的朋友可以參考下2015-10-10Golang使用Gin實(shí)現(xiàn)文件上傳的示例代碼
本文我們主要介紹了Golang如何使用Gin實(shí)現(xiàn)文件上傳,Go標(biāo)準(zhǔn)庫(kù)net/http對(duì)文件上傳已經(jīng)提供了非常完善的支持,而Gin框架在其基礎(chǔ)上進(jìn)一步封裝,因此使用Gin開(kāi)發(fā)文件上傳功能時(shí),只需要簡(jiǎn)單幾行代碼便可以實(shí)現(xiàn),需要的朋友可以參考下2024-02-02