深入理解Go-逃逸分析

How do I know whether a variable is allocated on the heap or the stack?

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.

在Go裏面定義了一個變量,到底是分配在堆上還是棧上,Go官方文檔告訴我們,不需要管,他們會分析,其實這個分析就是逃逸分析

在編程語言的編譯優化原理中,分析指針動態範圍的方法稱之爲逃逸分析。通俗來講,當一個對象的指針被多個方法或線程引用時,我們稱這個指針發生了逃逸。

發生逃逸行爲的情況主要有兩種:

  • 方法逃逸:當一個對象在方法中定義之後,作爲參數傳遞或返回值到其它方法中
  • 線程逃逸:如類變量或實例變量,可能被其它線程訪問到

這裏主要對 方法逃逸 進行分析,通過逃逸分析來判斷一個變量到底是分配在堆上還是棧上

逃逸策略

  • 如果編譯器不能證明某個變量在函數返回後不再被引用,則分配在堆上
  • 如果一個變量過大,則有可能分配在堆上

分析目的

  • 不逃逸的對象分配在棧上,則變量在用完後就會被編譯器回收,從而減少GC的壓力
  • 棧上的分配要比堆上的分配更加高效
  • 同步消除,如果定義的對象上有同步鎖,但是棧在運行時只有一個線程訪問,逃逸分析後如果在棧上則會將同步鎖去除

逃逸場景

指針逃逸

在 build 的時候,通過添加 -gcflags "-m" 編譯參數就可以查看編譯過程中的逃逸分析

在有些時候,因爲變量太大等原因,我們會選擇返回變量的指針,而非變量,這裏其實就是逃逸的一個經典現象

func main() {
    test()
}

func test() *int {
    i := 1
    return &i
}

逃逸分析結果:

# command-line-arguments
./main.go:7:6: can inline test
./main.go:3:6: can inline main
./main.go:4:6: inlining call to test
./main.go:4:6: main &i does not escape
./main.go:9:9: &i escapes to heap
./main.go:8:2: moved to heap: i

可以看到最後兩行指出,變量 i 逃逸到了 heap

棧空間不足逃逸

首先,我們嘗試創建一個 長度較小的 slice

func main() {
    stack()
}

func stack() {
    s := make([]int, 10, 10)
    s[0] = 1
}

逃逸分析結果:

./main.go:12:6: can inline stack
./main.go:3:6: can inline main
./main.go:4:7: inlining call to stack
./main.go:4:7: main make([]int, 10, 10) does not escape
./main.go:13:11: stack make([]int, 10, 10) does not escape

結果顯示未逃逸

然後,我們創建一個超大的slice

func main() {
    stack()
}

func stack() {
    s := make([]int, 100000, 100000)
    s[0] = 1
}

逃逸分析結果:

./main.go:12:6: can inline stack
./main.go:3:6: can inline main
./main.go:4:7: inlining call to stack
./main.go:4:7: make([]int, 100000, 100000) escapes to heap
./main.go:13:11: make([]int, 100000, 100000) escapes to heap

這時候就逃逸到了堆上了

動態類型逃逸

func main() {
    dynamic()
}

func dynamic() interface{} {
    i := 0
    return i
}

逃逸分析結果:

./main.go:18:6: can inline dynamic
./main.go:3:6: can inline main
./main.go:5:9: inlining call to dynamic
./main.go:5:9: main i does not escape
./main.go:20:2: i escapes to heap

這裏的動態類型逃逸,其實在理解了interface{}的內部結構後,還是可以歸併到 指針逃逸 這一類的,有興趣的同學可以看一下 《深入理解Go的interface》

閉包引用逃逸

func main() {
    f := fibonacci()
    for i := 0; i < 10; i++ {
        f()
    }
}
func fibonacci() func() int {
    a, b := 0, 1
    return func() int {
        a, b = b, a+b
        return a
    }
}

逃逸分析結果:

./main.go:11:9: can inline fibonacci.func1
./main.go:11:9: func literal escapes to heap
./main.go:11:9: func literal escapes to heap
./main.go:12:10: &b escapes to heap
./main.go:10:5: moved to heap: b
./main.go:12:13: &a escapes to heap
./main.go:10:2: moved to heap: a

參考

《Go 逃逸分析》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章