詳解Go語言中的逃逸分析
什么是逃逸
一句話,逃逸分析是編譯器用于決定將變量分配到棧上還是堆上的一種行為。
眾所周知,函數(shù)的運行都在操作系統(tǒng)內(nèi)存空間中的棧空間內(nèi)。我們在棧上聲明臨時變量,分配內(nèi)存,函數(shù)運行完畢后,回收內(nèi)存。每個函數(shù)的??臻g都是獨立的,其他函數(shù)沒有權限訪問。但在某些情況下,我們需要在函數(shù)結束以后訪問棧上面的某些數(shù)據(jù),這就涉及到內(nèi)存逃逸了。
如果變量從棧上逃逸,那么他會逃到哪兒去呢?他會跑到堆上。由于棧上的變量是在函數(shù)結束的時候自動進行回收,回收代價比較??;而堆空間分配內(nèi)存,則首先需要找到一塊大小合適的內(nèi)存,之后通過GC回收才能釋放。對于這種情況,頻繁使用垃圾回收會占用比較大的開銷,所以要盡量分配內(nèi)存到棧上,減少GC的壓力。
逃逸分析基本過程
Go語言的逃逸分析最基本的原則:如果一個函數(shù)返回一個對變量的引用,那么他就會發(fā)生逃逸。
在任何情況下,如果一個值被分配到了??臻g以外的地方,那么它一定是被分配到了堆上。簡言之:編譯器會分析代碼的特征和生命周期,Go中的變量只有在編譯器可以證明在函數(shù)返回后不會再被引用的情況下,才會被分配到棧上,否則會被分配到堆上。
不同于C++中的new,Go語言中的new關鍵字不一定會將內(nèi)存分配到堆空間上,在Go語言中,沒有關鍵字或者函數(shù)可以直接將變量分配到堆上,而是通過編譯器來分析代碼決定將變量分配到何處。
一句話:
編譯器會根據(jù)變量是否被外部引用來決定是否逃逸。
如果函數(shù)外部沒有引用,則優(yōu)先放到棧中;
如果函數(shù)外部存在引用,則必定放到堆中;
常見逃逸情況
指針逃逸
我們知道傳遞指針可以減少底層值的拷貝,提高效率。但是如果拷貝的數(shù)據(jù)量小,指針傳遞會產(chǎn)生逃逸,可能會使用堆空間,增加GC負擔,所以傳遞指針不一定是高效的。
比如:
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)存地址不會是棧而是堆,這是典型的逃逸案例。
使用命令 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)象
??臻g不足
如果分配太大容量的slice在棧上,當棧空間不足存放當前對象或者無法判斷當前切片長度時就會將對象分配到堆中。
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
動態(tài)類型逃逸
很多函數(shù)的參數(shù)為interface
類型,比如:
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ù)的具體類型,也能產(chǎn)生逃逸。
變量大小不確定
在創(chuàng)建切片,初始化切片容量的時候,有時會傳入一個變量指定其大小,由于變量的值不能被編譯器確定,所以不能確定其占用空間大小,從而編譯器可能會直接將變量分配到堆上。
package main func MakeSlice() { length := 1 a := make([]int, length, length) for i := 0; i < length; i++ { a[i] = i } } func main() { MakeSlice() }
編譯結果:
# command-line-arguments
./escape_1.go:5:11: make([]int, length, length) escapes to heap
常見的逃逸情況總結
指針逃逸:函數(shù)內(nèi)部返回一個局部變量指針
分配大對象:導致??臻g不足,不得不分配到堆上
調(diào)用接口類型的方法,接口類型的方法調(diào)用是動態(tài)調(diào)度 - 實際使用的具體實現(xiàn)只能在運行時確定。
盡管能夠符合分配到棧的場景,但是其大小不能在編譯的時候確定,也會分配到堆上。
如何避免
Go中的接口類型的方法調(diào)用是動態(tài)調(diào)度,因此不能夠在編譯階段確定,所有類型結構轉(zhuǎn)換成接口的過程會涉及到內(nèi)存逃逸的情況發(fā)生。如果對于性能要求比較高且訪問頻次比較高的函數(shù)調(diào)用,應該盡量避免使用接口類型。
由于切片一般都是使用在函數(shù)傳遞的場景下,而且切片在append的時候可能會涉及到重新分配內(nèi)存,如果切片在編譯期間的大小不能夠確認或者大小超出棧的限制,多數(shù)情況下都會被分配到堆上。
總結
堆上分配內(nèi)存比棧上分配內(nèi)存,開銷大很多。
變量分配在棧上需要能夠在編譯期確定他的作用域,否則會分配到堆上。
Go語言編譯器會通過變量是否被外部引用類決定是否逃逸
通過go build -gcflags '-m'
命令可以觀察變量是否逃逸
不能盲目使用變量的指針作為函數(shù)參數(shù),雖然會減少復制操作,但是當參數(shù)為變量自身的時候,復制是在棧上完成的操作,開銷遠比變量逃逸后動態(tài)地在堆上分配內(nèi)存少得多。
到此這篇關于詳解Go語言中的逃逸分析的文章就介紹到這了,更多相關Go逃逸分析內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!