首頁  >  文章  >  後端開發  >  聊聊分析golang的逃逸

聊聊分析golang的逃逸

藏色散人
藏色散人轉載
2021-07-01 14:56:152208瀏覽

自:http://www.agardner.me/golang/garbage/collection/gc/escape/analysis/ 2015/10/18/go-escape-analysis.html

#垃圾回收是Go的一個很方便的特性--其自動的記憶體管理使程式碼更整潔,同時減少記憶體洩漏的可能性。但是,由於垃圾回收需要周期性的停止程序從而去收集不用的對象,不可避免的會增加額外開銷。 Go編譯器是智慧的,它會自動決定一個變數是應該分配在堆上從而在將來便於回收,還是直接分配到函數的堆疊空間。對於分配到堆疊上的變量,其與分配到堆上的變量不同之處在於:隨著函數的返回,棧空間會被銷毀,從而棧上的變量被直接銷毀,不需要額外的垃圾回收開銷。

Go的逃逸分析相對於Java虛擬機器的HotSpot來說更為基礎。基本規則就是,如果一個變數的引用從聲明它的函數中返出去了,則發生“逃逸”,因為它有可能在函數外被別的內容使用,所以必須分配到堆上。如下幾種情況會比較複雜:

  • 函數呼叫其他函數
  • 引用作為結構體的成員變數
  • 切片和映射
  • #Cgo指向變數的指標

為了實現逃逸分析,Go會在編譯階段建構函式呼叫關係圖,同時追蹤入參和傳回值的流程。一個函數如果只是引用一個參數,但這個引用並沒有返出函數的話,這個變數也不會逃逸。如果一個函數回傳了一個引用,但是這個引用被堆疊中的其他函數解除或沒有傳回此引用,則也不會逃逸。為了論證幾個例子,可以在編譯時加上-gcflags '-m'參數,這個參數會印出逃逸分析的詳細資訊:

package main

type S struct {}

func main() {
    var x S
    _ = identity(x)
}

func identity(x S) S {
    return x
}

你可以執行go run -gcflags '-m -l'(註:原文中略了go程式碼檔名)來編譯這個程式碼,-l參數是防止函數identity被內聯(換個時間再討論內聯這個話題)。你將會看到沒有任何輸出! Go使用值傳遞,所以main函數中的x這個變數總是會被拷貝到函數identity的堆疊空間。通常情況下沒有使用引用的程式碼都是透過堆疊空間來分配記憶體。所以不涉及逃逸分析。下面試下困難一點的:

package main

type S struct {}

func main() {
    var x S
    y := &x
    _ = *identity(y)
}

func identity(z *S) *S {
    return z
}

其對應的輸出是:

./escape.go:11: leaking param: z to result ~r1
./escape.go:7: main &x does not escape

第一行顯示了變數z的「流經」:入參直接作為返回值返回了。但是函數identity沒有拿走去z這個引用,所以沒有發生變數逃逸。在main函數傳回後沒有任何對x的參考存在,所以x這個變數可以在main函數的堆疊空間進行記憶體分配。
第三次實驗:

package main

type S struct {}

func main() {
  var x S
  _ = *ref(x)
}

func ref(z S) *S {
  return &z
}

其輸出為:

./escape.go:10: moved to heap: z
./escape.go:11: &z escapes to heap

現在有了逃脫發生。記住Go是值傳遞的,所以z是變數x的拷貝。函數ref傳回一個對z的引用,所以z不能在堆疊中分配,否則當函數ref返回時,引用會指向何處呢?於是它逃逸到了堆中。其實執行完ref回到main函數中後,main函數丟棄了這個引用而不是解除引用,但是Go的逃逸分析還不夠機智去識別這種情況。
值得注意的是,在這種情況下,如果我們不停止引用,編譯器將內聯ref
如果結構體成員定義的是引用又會怎麼樣呢?

package main

type S struct {
  M *int
}

func main() {
  var i int
  refStruct(i)
}

func refStruct(y int) (z S) {
  z.M = &y
  return z
}

其輸出為:

./escape.go:12: moved to heap: y
./escape.go:13: &y escapes to heap

在這種情況下,儘管引用是結構體的成員,但Go仍然會追蹤引用的流向。由於函數refStruct接受引用並將其傳回,因此y必須逃逸。比較如下這個例子:

package main

type S struct {
  M *int
}

func main() {
  var i int
  refStruct(&i)
}

func refStruct(y *int) (z S) {
  z.M = y
  return z
}

其輸出為:

./escape.go:12: leaking param: y to result z
./escape.go:9: main &i does not escape

儘管在main函數中對i變數做了引用操作,並傳遞到了函數refStruct中,但是這個引用的範圍沒有超出其宣告它的堆疊空間。這和之前的程式語意上有細微的差別,這個會更有效率:在上一個程式中,變數i必須分配在main函數的堆疊中,然後作為參數拷貝到函數refStruct中,並將拷貝的這一份分配在堆上。而在這個例子中,i只被分配一次,然後將引用到處傳遞。

再來看一個有點彎彎繞的例子:

package main

type S struct {
  M *int
}

func main() {
  var x S
  var i int
  ref(&i, &x)
}

func ref(y *int, z *S) {
  z.M = y
}

其輸出為:

./escape.go:13: leaking param: y
./escape.go:13: ref z does not escape
./escape.go:9: moved to heap: i
./escape.go:10: &i escapes to heap
./escape.go:10: main &x does not escape

问题在于,y被赋值给了一个入参结构体的成员。Go并不能追溯这种关系(go只能追溯输入直接流向输出),所以逃逸分析失败了,所以变量只能分配到堆上。由于Go的逃逸分析的局限性,许多变量会被分配到堆上,请参考此链接,这里面记录了许多案例(从Go1.5开始)。

最后,来看下映射和切片是怎样的呢?请记住,切片和映射实际上只是具有指向堆内存的指针的Go结构:slice结构是暴露在reflect包中(SliceHeader
)。map结构就更隐蔽了:存在于hmap。如果这些结构体不逃逸,将会被分配到栈上,但是其底层的数组或者哈希桶中的实际数据会被分配到堆上去。避免这种情况的唯一方法是分配一个固定大小的数组(例如[10000]int)。

如果你剖析过你的程序堆使用情况(https://blog.golang.org/pprof
),并且想减少垃圾回收的消耗,可以将频繁分配到堆上的变量移到栈上,可能会有较好的效果。进一步研究HotSpot JVM是如何进行逃逸分析的会是一个不错的话题,可以参考这个链接,这个里面主要讲解了栈分配,以及有关何时可以消除同步的检测。

更多golang相关技术文章,请访问golang教程栏目!

以上是聊聊分析golang的逃逸的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除