Golang逃逸分析

Tybyq發表於2019-08-15

帶GC語言給我們程式的編寫帶來了極大的便利,但是與此同時遮蔽了很多底層的細節,比如一個物件是在棧上分配還是在堆上分配。對於普通的程式碼來說雖然不需要關心這麼多,但是作為強迫症程式猿,還是希望能讓自己寫出來的程式碼效能最優,所以還是需要了解什麼是逃逸,以及如何判斷是否發生了逃逸。

什麼是堆和棧?

首先需要知道,我們說的堆和棧是啥。這個可不是資料結構裡面的"堆"和"棧",而是作業系統裡面的概念。

在程式中,每個函式塊都會有自己的記憶體區域用來存自己的區域性變數(記憶體佔用少)、返回地址、返回值之類的資料,這一塊記憶體區域有特定的結構和定址方式,大小在編譯時已經確定,定址起來也十分迅速,開銷很少。這一塊記憶體地址稱為棧。棧是執行緒級別的,大小在建立的時候已經確定,所以當資料太大的時候,就會發生"stack overflow"。

在程式中,全域性變數、記憶體佔用大的區域性變數、發生了逃逸的區域性變數存在的地方就是堆,這一塊記憶體沒有特定的結構,也沒有固定的大小,可以根據需要進行調整。簡單來說,有大量資料要存的時候,就存在堆裡面。堆是程式級別的。當一個變數需要分配在堆上的時候,開銷會比較大,對於go這種帶GC的語言來說,也會增加gc壓力,同時也容易造成記憶體碎片。

為什麼有的變數要分配在堆,有的要分配在棧?

這個問題要從C++說起了。在C++中,假設我們有以下程式碼:

```c++
int* f1() {
int i = 5;
return &i;
}

int main() {
int  i = f1();
i = 6;
return 0;
}

這時候程式結果是無法預期的,因為在函式f1中,i是一個區域性變數,會分配在棧上,而棧在函式返回之後就失效了(Plan9 彙編中SP指標被修改),於是i的地址所存的值是不可預期的,後續在main中對返回的i的地址中的值的修改可能會修改掉程式執行的資料,造成結果無法預期。
所以對於需要返回一個地址回去的情況,在C++中需要用new來分配一塊堆上的記憶體才行,因為堆是程式級別的,也就是全域性的,除非程式猿手動釋放,否則不會被回收(釋放不好會段錯誤,忘了釋放會記憶體洩漏),於是就可以使得這個地址不會再被使用到,可以安全地返回。## 如何進行逃逸分析?在golang中,所有記憶體都是由runtime管理的,程式猿不需要關心具體變數分配在哪裡,什麼時候回收,但是編譯器需要知道這一點,這樣才能確定函式棧幀大小、哪些變數需要"new"在堆上,所以編譯器需要進行`逃逸分析`。簡單來說,`逃逸分析`決定了一個變數是分配在棧上還是分配在堆上。
golang逃逸分析最基本的原則是:`如果一個函式返回的是一個(區域性)變數的地址,那麼這個變數就發生逃逸`。
在golang裡面,變數分配在何處和是否使用new無關,意味著程式猿無法手動指定某個變數必須分配在棧上或者堆上(自己擼asm的當我沒說),所以我們需要透過一些方法來確定某個變數到底是分配在了棧上還是堆上。
我們用以下程式碼作為例子:
```go
package main
func main() {    a := f1()
    *a++
}//go:noinlinefunc f1() *int {    i := 1
    return &i
}

在以上程式碼中,給f1增加了noinline標記,讓go編譯器不要將函式內聯。

使用編譯引數

golang提供了編譯的引數讓我們可以直觀地看到變數是否發生了逃逸,只需要在go build時指定  -gcflags '-m' 即可:

$ go build -gcflags '-m' escape.go# command-line-arguments./escape.go:3:6: can inline main
./escape.go:11:9: &i escapes to heap
./escape.go:10:2: moved to heap: i

這樣可以很直觀地看到在第10、11行,i發生了逃逸,記憶體會分配在堆上。

除了使用編譯引數之外,我們還可以使用一種更底層的,更硬核,也更準確的方式來判斷一個物件是否逃逸,那就是:直接看彙編!

使用匯編

我們使用 go tool compile -S 生成彙編程式碼:

$ go tool compile -S escape.go | grep escape.go:10
    0x001d 00029 (escape.go:10) PCDATA  $2, $1
    0x001d 00029 (escape.go:10) PCDATA  $0, $0
    0x001d 00029 (escape.go:10) LEAQ    type.int(SB), AX
    0x0024 00036 (escape.go:10) PCDATA  $2, $0
    0x0024 00036 (escape.go:10) MOVQ    AX, (SP)
    0x0028 00040 (escape.go:10) CALL    runtime.newobject(SB)
    0x002d 00045 (escape.go:10) PCDATA  $2, $1
    0x002d 00045 (escape.go:10) MOVQ    8(SP), AX
    0x0032 00050 (escape.go:10) MOVQ    $1, (AX)

可以看到,這裡的00040有呼叫 runtime.newobject(SB) 這個方法,看到這個方法大家就應該懂了!

總結

以上提供了兩種方法可以用來判斷某個變數是否發生了逃逸,其中使用編譯引數比較簡單,使用匯編比較硬核。透過這兩種方法分析完逃逸,就能進一步最佳化堆上記憶體數量,減輕GC壓力了。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31557424/viewspace-2653811/,如需轉載,請註明出處,否則將追究法律責任。

相關文章