一文帶你搞懂golang中內(nèi)存分配逃逸分析
一. golang 的內(nèi)存分配逃逸
1. 關(guān)于堆和棧
棧 可以簡單得理解成一次函數(shù)調(diào)用內(nèi)部申請到的內(nèi)存,它們會隨著函數(shù)的返回把內(nèi)存還給系統(tǒng)。
func F() { temp := make([]int, 0, 20) ... }
類似于上面代碼里面的temp變量,只是內(nèi)函數(shù)內(nèi)部申請的臨時變量,并不會作為返回值返回,它就是被編譯器申請到棧里面。
申請到 棧內(nèi)存 好處:函數(shù)返回直接釋放,不會引起垃圾回收,對性能沒有影響。
再來看看堆得情況之一如下代碼:
func F() []int{ a := make([]int, 0, 20) return a }
而上面這段代碼,申請的代碼一模一樣,但是申請后作為返回值返回了,編譯器會認(rèn)為變量之后還會被使用,當(dāng)函數(shù)返回之后并不會將其內(nèi)存歸還,那么它就會被申請到 堆 上面了。
申請到堆上面的內(nèi)存才會引起垃圾回收,如果這個過程(特指垃圾回收不斷被觸發(fā))過于高頻就會導(dǎo)致 gc 壓力過大,程序性能出問題。
我們再看看如下幾個例子:
func F() { a := make([]int, 0, 20) // 棧 空間小 b := make([]int, 0, 20000) // 堆 空間過大 l := 20 c := make([]int, 0, l) // 堆 動態(tài)分配不定空間 }
像是 b 這種 即使是臨時變量,申請過大也會在堆上面申請。
對于 c 編譯器對于這種不定長度的申請方式,也會在堆上面申請,即使申請的長度很短。
2. 逃逸分析(Escape analysis)
所謂逃逸分析(Escape analysis)是指由編譯器決定內(nèi)存分配的位置,不需要程序員指定。
在函數(shù)中申請一個新的對象:
- 如果分配 在棧中,則函數(shù)執(zhí)行結(jié)束可自動將內(nèi)存回收;
- 如果分配在堆中,則函數(shù)執(zhí)行結(jié)束可交給GC(垃圾回收)處理;
注意,對于函數(shù)外部沒有引用的對象,也有可能放到堆中,比如內(nèi)存過大超過棧的存儲能力。
3. 逃逸場景(什么情況才分配到堆中)
3.1 指針逃逸
Go可以返回局部變量指針,這其實是一個典型的變量逃逸案例,示例代碼如下:
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("Jim", 18) }
雖然 在函數(shù) StudentRegister() 內(nèi)部 s 為局部變量,其值通過函數(shù)返回值返回,s 本身為一指針,其指向的內(nèi)存地址不會是棧而是堆,這就是典型的逃逸案例。
終端運行命令查看逃逸分析日志:
go build -gcflags=-m
可見在StudentRegister()函數(shù)中,也即代碼第9行顯示”escapes to heap”,代表該行內(nèi)存分配發(fā)生了逃逸現(xiàn)象。
3.2 棧空間不足逃逸(空間開辟過大)
package main func Slice() { s := make([]int, 1000, 1000) for index, _ := range s { s[index] = index } } func main() { Slice() }
上面代碼Slice()函數(shù)中分配了一個1000個長度的切片,是否逃逸取決于??臻g是否足夠大。 直接查看編譯提示,如下:
所以只是1000的長度還不足以發(fā)生逃逸現(xiàn)象。然后就x10倍吧
package main func Slice() { s := make([]int, 10000, 10000) for index, _ := range s { s[index] = index } } func main() { Slice() }
分析如下:
當(dāng)切片長度擴(kuò)大到10000時就會逃逸。
實際上當(dāng)棧空間不足以存放當(dāng)前對象時或無法判斷當(dāng)前切片長度時會將對象分配到堆中。
3.3 動態(tài)類型逃逸(不確定長度大小)
很多函數(shù)參數(shù)為interface類型,比如fmt.Println(a …interface{}),編譯期間很難確定其參數(shù)的具體類型,也能產(chǎn)生逃逸。
如下代碼所示:
package main import "fmt" func main() { s := "Escape" fmt.Println(s) }
逃逸分下如下:
D:\SourceCode\GoExpert\src>go build -gcflags=-m
# _/D_/SourceCode/GoExpert/src
.\main.go:7: s escapes to heap
.\main.go:7: main ... argument does not escape
又或者像前面提到的例子:
func F() { a := make([]int, 0, 20) // 棧 空間小 b := make([]int, 0, 20000) // 堆 空間過大 逃逸 l := 20 c := make([]int, 0, l) // 堆 動態(tài)分配不定空間 逃逸 }
3.4 閉包引用對象逃逸
Fibonacci數(shù)列的函數(shù):
package main import "fmt" func Fibonacci() func() int { a, b := 0, 1 return func() int { a, b = b, a+b return a } } func main() { f := Fibonacci() for i := 0; i < 10; i++ { fmt.Printf("Fibonacci: %d\n", f()) } }
輸出如下:
~/go/src/gitHub/test/pool go run main.go
Fibonacci: 1
Fibonacci: 1
Fibonacci: 2
Fibonacci: 3
Fibonacci: 5
Fibonacci: 8
Fibonacci: 13
Fibonacci: 21
Fibonacci: 34
Fibonacci: 55
逃逸如下:
~/go/src/gitHub/test/pool go build -gcflags=-m
# gitHub/test/pool
./main.go:7:9: can inline Fibonacci.func1
./main.go:7:9: func literal escapes to heap
./main.go:7:9: func literal escapes to heap
./main.go:8:10: &b escapes to heap
./main.go:6:5: moved to heap: b
./main.go:8:13: &a escapes to heap
./main.go:6:2: moved to heap: a
./main.go:17:34: f() escapes to heap
./main.go:17:13: main ... argument does not escape
Fibonacci()函數(shù)中原本屬于局部變量的a和b由于閉包的引用,不得不將二者放到堆上,以致產(chǎn)生逃逸。
逃逸分析的作用是什么呢?
- 逃逸分析的好處是為了減少gc的壓力,不逃逸的對象分配在棧上,當(dāng)函數(shù)返回時就回收了資源,不需要gc標(biāo)記清除。
- 逃逸分析完后可以確定哪些變量可以分配在棧上,棧的分配比堆快,性能好(逃逸的局部變量會在堆上分配 ,而沒有發(fā)生逃逸的則有編譯器在棧上分配)。
- 同步消除,如果你定義的對象的方法上有同步鎖,但在運行時,卻只有一個線程在訪問,此時逃逸分析后的機(jī)器碼,會去掉同步鎖運行。
逃逸總結(jié):
- 棧上分配內(nèi)存比在堆中分配內(nèi)存有更高的效率
- 棧上分配的內(nèi)存不需要GC處理
- 堆上分配的內(nèi)存使用完畢會交給GC處理
- 逃逸分析目的是決定內(nèi)分配地址是棧還是堆
- 逃逸分析在編譯階段完成
提問:函數(shù)傳遞指針真的比傳值效率高嗎?
我們知道傳遞指針可以減少底層值的拷貝,可以提高效率,但是如果拷貝的數(shù)據(jù)量小,由于指針傳遞會產(chǎn)生逃逸,可能會使用堆,也可能會增加GC的負(fù)擔(dān),所以傳遞指針不一定是高效的。
在官網(wǎng) (golang.org) FAQ 上有一個關(guān)于變量分配的問題如下:
From a correctness standpoint, you don’t need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.
The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function’s stack frame.
However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.
In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.
翻譯如下:
如何得知變量是分配在棧(stack)上還是堆(heap)上?
準(zhǔn)確地說,你并不需要知道。Golang 中的變量只要被引用就一直會存活,存儲在堆上還是棧上由內(nèi)部實現(xiàn)決定而和具體的語法沒有關(guān)系。
知道變量的存儲位置確實和效率編程有關(guān)系。如果可能,Golang 編譯器會將函數(shù)的局部變量分配到函數(shù)棧幀(stack frame)上。 然而,如果編譯器不能確保變量在函數(shù) return之后不再被引用,編譯器就會將變量分配到堆上。而且,如果一個局部變量非常大,那么它也應(yīng)該被分配到堆上而不是棧上。
當(dāng)前情況下,如果一個變量被取地址,那么它就有可能被分配到堆上。然而,還要對這些變量做逃逸分析,如果函數(shù)return之后,變量不再被引用,則將其分配到棧上。
二. golang 臨時對象池sync.Pool
1. 內(nèi)存碎片化問題
實際項目基本都是通過
c := make([]int, 0, l)
來申請內(nèi)存,長度都是不確定的,自然而然這些變量都會申請到堆上面了。
Golang使用的垃圾回收算法是『標(biāo)記——清除』。
簡單得說,就是程序要從操作系統(tǒng)申請一塊比較大的內(nèi)存,內(nèi)存分成小塊,通過鏈表鏈接。
每次程序申請內(nèi)存,就從鏈表上面遍歷每一小塊,找到符合的就返回其地址,沒有合適的就從操作系統(tǒng)再申請。如果申請內(nèi)存次數(shù)較多,而且申請的大小不固定,就會引起內(nèi)存碎片化的問題。
申請的堆內(nèi)存并沒有用完,但是用戶申請的內(nèi)存的時候卻沒有合適的空間提供。這樣會遍歷整個鏈表,還會繼續(xù)向操作系統(tǒng)申請內(nèi)存。這就能解釋我一開始描述的問題,申請一塊內(nèi)存變成了慢語句。
到此這篇關(guān)于一文帶你搞懂golang中內(nèi)存分配逃逸分析的文章就介紹到這了,更多相關(guān)go內(nèi)存分配逃逸內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go-micro開發(fā)RPC服務(wù)以及運行原理介紹
這篇文章介紹了go-micro開發(fā)RPC服務(wù)的方法及其運行原理,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-07-07關(guān)于golang利用channel和goroutine完成統(tǒng)計素數(shù)的思路
這篇文章主要介紹了golang利用channel和goroutine完成統(tǒng)計素數(shù)的思路詳解,通過思路圖分析及實例代碼相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-08-08