Go Escape analysis issues of strings builder

在分享 Go string to slice 議題 文章後,有朋友(感謝@陳孝思)在社團裡面分享另一個議題,關於在 strings package 的 builder 中有避免 escape analysis 的方式。基於這個分享,我就去查閱了相關 source code 和 issues ,並且整理成此文章。

Escape analysis

escape analysis 是協助 Go compiler 判斷要將 variable 分配在 goroutine stack 還是 heap 的方式,而由於變數的分配位置會關係到 garbage collection 進而影響效能,因此 escape analysis 也是 Gopher 所在意的議題之一。

平常我們在 build Go 程式時,可以透過調整 compiler 的 flag 來印出 escape analysis 的過程結果。

1# -m: print optimization decisions
2go run -gcflags='-m' main.go

如果想要知道 compiler 的所有 flag 資訊,可以透過 cmd

1go tool compile -help

就可以知道各支援的 flags 和詳細說明。

escape analysis 是非常強大的分析工具,但也因為如此,如果 escape analysis 將可能不需要在 heap 的變數判定成要分配在 heap 區域,就會產生效能上的疑慮,因此就產生一些 hack workaround 做法,來規避 escape analysis 的分析。

Hack workaround of Go strings builder

在 Go strings/builder.go 裡面就有一個 hack workaround 的例子:

 1// noescape hides a pointer from escape analysis.  noescape is
 2// the identity function but escape analysis doesn't think the
 3// output depends on the input. noescape is inlined and currently
 4// compiles down to zero instructions.
 5func noescape(p unsafe.Pointer) unsafe.Pointer {
 6	x := uintptr(p)
 7	return unsafe.Pointer(x ^ 0)
 8    // x^0 implicitly convert expressions to an integer
 9}
10
11func (b *Builder) copyCheck() {
12	if b.addr == nil {
13		// This hack works around a failing of Go's escape analysis
14		// that was causing b to escape and be heap allocated.
15		b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
16	} else if b.addr != b {
17		panic("strings: illegal use of non-zero Builder copied by value")
18	}
19}

首先來看 strings Builder struct,為了防止使用者誤用 Builder,因此在 Builder struct 中會有一個 addr pointer 指向自己。

1type Builder struct {
2	addr *Builder // of receiver, to detect copies by value
3	buf  []byte
4}

在每次執行 Builder method 前,透過 copyCheck() 檢查 addr pointer 來判斷當前這個 Builder 是不是 copy 來的。

A self-referential Issue of escape analysis

但這樣的做法,卻產生 self-reference 問題,導致 escape analysis 會誤判,進而把有 self-reference 的變數都歸類在 heap 分配。

早在 2014 年 的 7921 issue cmd/compile, bytes: bootstrap array causes bytes.Buffer to always be heap-allocated 就有提到此問題,而其中舉出一些簡單的 self-reference 例子,都會導致 escape analysis 誤判,例如:

 1type B struct {
 2	p *int
 3	n int
 4}
 5
 6func (b *B) X() {
 7	b.p = &b.n
 8}
 9
10func (b *B) Y() {
11	n := 4
12	b.p = &n
13}
14
15func main() {
16	var a, b B
17	a.X() // escape
18	b.Y() // non-escape
19}

觀察 escap analysis 結果:

1./main.go:13:7: b does not escape
2./main.go:14:2: moved to heap: n
3./main.go:19:6: moved to heap: a

Y() 是把一個在 heap 的變數 n assign 給 struct member ,所以不會影想 escap analysis 判斷,但反之 X() 是自身 struct member reference,導致 escap analysis 誤將變數歸在 heap 中。

雖然從 2014 年發現這個 issue 到現在,目前 Go 1.14 版本還是存在此問題,不過根據 issue 裡面的討論,看來還是有進展的,在 escap analysis 被重新改寫後,大神們是有機會重新 review 此問題並解決。

結語

self-reference 所產生的問題,會根據各語言實作方式不同,而有不同層面的影響,而對 Go 來說,則是會影響 escap analysis 判斷結果,也算是一個不錯的學習。