最近試著最佳化掉一些小對象分配,發現一個很詭異的問題:這段代碼會在堆上指派至。
package mainimport ( "fmt")func main() { var a [1]int c := a[:] fmt.Println(c)}
看彙編代碼
go tool compile -S test.golang"".main t=1 size=336 value=0 args=0x0 locals=0x98 0x0000 00000 (test.go:7) TEXT "".main(SB), $152-0 0x0000 00000 (test.go:7) MOVQ (TLS), CX 0x0009 00009 (test.go:7) LEAQ -24(SP), AX 0x000e 00014 (test.go:7) CMPQ AX, 16(CX) 0x0012 00018 (test.go:7) JLS 320 0x0018 00024 (test.go:7) SUBQ $152, SP 0x001f 00031 (test.go:7) FUNCDATA $0, gclocals·7d2d5fca80364273fb07d5820a76fef4(SB) 0x001f 00031 (test.go:7) FUNCDATA $1, gclocals·6e96661712a005168eba4ed6774db961(SB) 0x001f 00031 (test.go:8) LEAQ type.[1]int(SB), BX 0x0026 00038 (test.go:8) MOVQ BX, (SP) 0x002a 00042 (test.go:8) PCDATA $0, $0 0x002a 00042 (test.go:8) CALL runtime.newobject(SB) 0x002f 00047 (test.go:8) MOVQ 8(SP), AX 0x0034 00052 (test.go:9) CMPQ AX, $0 0x0038 00056 (test.go:9) JEQ $1, 313 0x003e 00062 (test.go:9) MOVQ $1, DX 0x0045 00069 (test.go:9) MOVQ $1, CX
注意到有調用newobject。其中test.go:8說明變數a的記憶體是在堆上分配的!
在Go的FAQ裡面有這麼一段解釋:
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.
它說從正確性的角度,使用者不用關心記憶體在哪裡分配的就是了。一般來說,如果有地方用到了那個地址,那麼變數就會在堆上分配了。比如C中不能,但Go中可以這麼幹:
type struct T { xxx}func f() *T { var ret T return &ret}
變數ret的記憶體會在堆上分配的,Go的編譯器會決定在哪(堆or棧)分配記憶體,保證程式的正確性。
Go的編譯器很聰明(自作聰明),它還會做逃逸分析(escape analysis),如果它發現變數的範圍沒有跑出太遠,它就可以在棧上分配空間而不是堆。比如這段代碼,就不會在堆上分配記憶體,即使我們用new分配。
const Width, Height = 640, 480type Cursor struct { X, Y int}func Center(c *Cursor) { c.X += Width / 2 c.Y += Height / 2}func CenterCursor() { c := new(Cursor) Center(c) fmt.Println(c.X, c.Y)}
驗證一下:
go tool compile -m test.gotest.go:17: can inline Centertest.go:24: inlining call to Centertest.go:25: c.X escapes to heaptest.go:25: c.Y escapes to heaptest.go:23: CenterCursor new(Cursor) does not escapetest.go:25: CenterCursor ... argument does not escapetest.go:17: Center c does not escape
參數-m是列印出編譯最佳化。從輸出上看,它說new(Cursor)沒有escape,於是在棧上分配了。等價於C的寫法:
void CenterCursor() { struct Cursor c; Center(&c);}
再看另一個代碼,跟開始那段代碼的區別,一個用的fmt.Println,一個用的println。
package mainfunc main() { var a [1]int c := a[:] println(c)}
但是!!! 這段代碼a是在棧上分配的。而最上面那段,卻是在堆上分配的。
綜上,Go一方面會把一些,看上去會在棧上分配的東西,移到堆上分配;另一方面又會把看上去會在堆上分配的東西,在棧上分配。由編譯最佳化那邊做逃逸分析來控制。
這最佳化太詭異了,最後實在沒法直觀地知道,哪些會在堆上分配,哪些會在棧上分配。老實說,我非常不喜歡這個feature。Go自做主張弄一些背後我無法確定的事情。順帶一提吧,另一個Go中我不喜歡的feature是init函數。