Golang內存分配逃逸分析


一. golang 的內存分配逃逸 於堆和棧

注意我們此處談到的堆和棧是對操作系統中的,這個和數據結構中的堆和棧還是又一定區別的。

 

1. 關於 堆和棧

棧 可以簡單得理解成一次函數調用內部申請到的內存,它們會隨着函數的返回把內存還給系統。

func F() { temp := make([]int, 0, 20) ... }

類似於上面代碼里面的temp變量,只是內函數內部申請的臨時變量,並不會作為返回值返回,它就是被編譯器申請到棧里面。

申請到 棧內存 好處:函數返回直接釋放,不會引起垃圾回收,對性能沒有影響。

再來看看堆得情況之一如下代碼:

func F() []int{ a := make([]int, 0, 20) return a }

而上面這段代碼,申請的代碼一模一樣,但是申請后作為返回值返回了,編譯器會認為變量之后還會被使用,當函數返回之后並不會將其內存歸還,那么它就會被申請到 堆 上面了。

申請到堆上面的內存才會引起垃圾回收,如果這個過程(特指垃圾回收不斷被觸發)過於高頻就會導致 gc 壓力過大,程序性能出問題。

我們再看看如下幾個例子:

func F() { a := make([]int, 0, 20) // 棧 空間小 b := make([]int, 0, 20000) // 堆 空間過大 l := 20 c := make([]int, 0, l) // 堆 動態分配不定空間 }

像是 b 這種 即使是臨時變量,申請過大也會在堆上面申請。

對於 c 編譯器對於這種不定長度的申請方式,也會在堆上面申請,即使申請的長度很短。

2. 逃逸分析(Escape analysis)

所謂逃逸分析(Escape analysis)是指由編譯器決定內存分配的位置,不需要程序員指定。

在函數中申請一個新的對象:

  • 如果分配 在棧中,則函數執行結束可自動將內存回收;

  • 如果分配在堆中,則函數執行結束可交給GC(垃圾回收)處理;

注意,對於函數外部沒有引用的對象,也有可能放到堆中,比如內存過大超過棧的存儲能力。

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) }

雖然 在函數 StudentRegister() 內部 s 為局部變量,其值通過函數返回值返回,s 本身為一指針,其指向的內存地址不會是棧而是堆,這就是典型的逃逸案例。

終端運行命令查看逃逸分析日志:

go build -gcflags=-m

可見在StudentRegister()函數中,也即代碼第9行顯示”escapes to heap”,代表該行內存分配發生了逃逸現象。

3.2 棧空間不足逃逸(空間開辟過大)

package main

func Slice() {
    s := make([]int, 1000, 1000) for index, _ := range s { s[index] = index } } func main() { Slice() }

上面代碼Slice()函數中分配了一個1000個長度的切片,是否逃逸取決於棧空間是否足夠大。 直接查看編譯提示,如下:

所以只是1000的長度還不足以發生逃逸現象。然后就x10倍吧

package main

func Slice() {
    s := make([]int, 10000, 10000) for index, _ := range s { s[index] = index } } func main() { Slice() }

分析如下:

當切片長度擴大到10000時就會逃逸。

實際上當棧空間不足以存放當前對象時或無法判斷當前切片長度時會將對象分配到堆中。

3.3 動態類型逃逸(不確定長度大小)

很多函數參數為interface類型,比如fmt.Println(a …interface{}),編譯期間很難確定其參數的具體類型,也能產生逃逸。

如下代碼所示:

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) // 堆 動態分配不定空間 逃逸 }

3.4 閉包引用對象逃逸

Fibonacci數列的函數:

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()函數中原本屬於局部變量的a和b由於閉包的引用,不得不將二者放到堆上,以致產生逃逸。

逃逸分析的作用是什么呢?

  1. 逃逸分析的好處是為了減少gc的壓力,不逃逸的對象分配在棧上,當函數返回時就回收了資源,不需要gc標記清除。

  2. 逃逸分析完后可以確定哪些變量可以分配在棧上,棧的分配比堆快,性能好(逃逸的局部變量會在堆上分配 ,而沒有發生逃逸的則有編譯器在棧上分配)。

  3. 同步消除,如果你定義的對象的方法上有同步鎖,但在運行時,卻只有一個線程在訪問,此時逃逸分析后的機器碼,會去掉同步鎖運行。

逃逸總結:

  • 棧上分配內存比在堆中分配內存有更高的效率

  • 棧上分配的內存不需要GC處理

  • 堆上分配的內存使用完畢會交給GC處理

  • 逃逸分析目的是決定內分配地址是棧還是堆

  • 逃逸分析在編譯階段完成

提問:函數傳遞指針真的比傳值效率高嗎?

我們知道傳遞指針可以減少底層值的拷貝,可以提高效率,但是如果拷貝的數據量小,由於指針傳遞會產生逃逸,可能會使用堆,也可能會增加GC的負擔,所以傳遞指針不一定是高效的。

在官網 (golang.org) FAQ 上有一個關於變量分配的問題如下:

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)上?

准確地說,你並不需要知道。Golang 中的變量只要被引用就一直會存活,存儲在堆上還是棧上由內部實現決定而和具體的語法沒有關系。

知道變量的存儲位置確實和效率編程有關系。如果可能,Golang 編譯器會將函數的局部變量分配到函數棧幀(stack frame)上。 然而,如果編譯器不能確保變量在函數 return之后不再被引用,編譯器就會將變量分配到堆上。而且,如果一個局部變量非常大,那么它也應該被分配到堆上而不是棧上。

當前情況下,如果一個變量被取地址,那么它就有可能被分配到堆上。然而,還要對這些變量做逃逸分析,如果函數return之后,變量不再被引用,則將其分配到棧上。


二. golang 臨時對象池sync.Pool

1. 內存碎片化問題:

實際項目基本都是通過

c := make([]int, 0, l)

來申請內存,長度都是不確定的,自然而然這些變量都會申請到堆上面了。

Golang使用的垃圾回收算法是『標記——清除』。

簡單得說,就是程序要從操作系統申請一塊比較大的內存,內存分成小塊,通過鏈表鏈接。

每次程序申請內存,就從鏈表上面遍歷每一小塊,找到符合的就返回其地址,沒有合適的就從操作系統再申請。如果申請內存次數較多,而且申請的大小不固定,就會引起內存碎片化的問題。

申請的堆內存並沒有用完,但是用戶申請的內存的時候卻沒有合適的空間提供。這樣會遍歷整個鏈表,還會繼續向操作系統申請內存。這就能解釋我一開始描述的問題,申請一塊內存變成了慢語句。


2. 為了解決以上問題引入 臨時對象池sync.Pool

由於此部分篇幅可能較長所以我打算另外開一篇文章來寫,敬請期待。

https://driverzhang.github.io/post/golang%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E9%80%83%E9%80%B8%E5%88%86%E6%9E%90/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM