golang逃逸分析

2021-09-26 05:20:39 字數 2545 閱讀 3244

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

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

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

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

這個問題要從c++說起了。在c++中,假設我們有以下**:

```c++

int* f1()

int main()

這時候程式結果是無法預期的,因為在函式f1中,i是乙個區域性變數,會分配在棧上,而棧在函式返回之後就失效了(plan9 彙編中sp指標被修改),於是i的位址所存的值是不可預期的,後續在main中對返回的i的位址中的值的修改可能會修改掉程式執行的資料,造成結果無法預期。

所以對於需要返回乙個位址回去的情況,在c++中需要用new來分配一塊堆上的記憶體才行,因為堆是程序級別的,也就是全域性的,除非程式猿手動釋放,否則不會被**(釋放不好會段錯誤,忘了釋放會記憶體洩漏),於是就可以使得這個位址不會再被使用到,可以安全地返回。

## 如何進行逃逸分析?

在golang中,所有記憶體都是由runtime管理的,程式猿不需要關心具體變數分配在**,什麼時候**,但是編譯器需要知道這一點,這樣才能確定函式棧幀大小、哪些變數需要"new"在堆上,所以編譯器需要進行`逃逸分析`。簡單來說,`逃逸分析`決定了乙個變數是分配在棧上還是分配在堆上。

在golang裡面,變數分配在何處和是否使用new無關,意味著程式猿無法手動指定某個變數必須分配在棧上或者堆上(自己擼asm的當我沒說),所以我們需要通過一些方法來確定某個變數到底是分配在了棧上還是堆上。

我們用以下**作為例子:

```go

package main

func main()

//go:noinline

func f1() *int

在以上**中,給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壓力了。

逃逸分析 Escape Analysis)

什麼是逃逸?逃逸是指在某個方法之內置立的物件,除了在方法體之內被引用之外,還在方法體之外被其它變數引用到 這樣帶來的後果是在該方法執行完畢之後,該方法中建立的物件將無法被gc 由於其被其它變數引用。正常的方法呼叫中,方法體中建立的物件將在執行完畢之後,將 其中建立的物件 故由於無法 即成為逃逸。無逃...

Go記憶體逃逸分析

分析記憶體逃逸之前要搞清楚一件事 我們編寫的程式中的函式和區域性變數是存放在棧上的 補充一點堆上儲存的資料的指標 是存放在棧上的 因為指標的大小是可以提前預知的 還有就是go的基本型別也是存放在棧內的 而其餘的變數是存在堆上的,棧是由作業系統層面控制 進行記憶體空間的釋放 堆預設是程式控制的 像c ...

分層編譯和逃逸分析

分層編譯和逃逸分析在1.8中是預設是開啟的 即時編譯 just in time compilation,jit 是一種通過在執行時將位元組碼翻譯為機器碼,從而改善位元組碼編譯語言效能的技術。在hotspot實現中有多種選擇 c1 c2和c1 c2,分別對應client server和分層編譯。1 c...