最近 Go1.13 終於發佈了,其中一個值得關注的特性就是 defer 在大部分的場景下性能提高了30%,可是官方並無具體寫是怎麼提高的,這讓你們很是的疑惑。而我由於以前寫過《深刻理解 Go defer》 和 《Go defer 會有性能損耗,儘可能不要用?》 這類文章,所以我挺感興趣它是作了什麼改變才能獲得這樣子的結果,因此今天和你們一塊兒探索其中奧妙。html
原文地址:Go1.13 defer 的性能是如何提升的?git
$ go test -bench=. -benchmem -run=none goos: darwin goarch: amd64 pkg: github.com/EDDYCJY/awesomeDefer BenchmarkDoDefer-4 20000000 91.4 ns/op 48 B/op 1 allocs/op BenchmarkDoNotDefer-4 30000000 41.6 ns/op 48 B/op 1 allocs/op PASS ok github.com/EDDYCJY/awesomeDefer 3.234s
$ go test -bench=. -benchmem -run=none goos: darwin goarch: amd64 pkg: github.com/EDDYCJY/awesomeDefer BenchmarkDoDefer-4 15986062 74.7 ns/op 48 B/op 1 allocs/op BenchmarkDoNotDefer-4 29231842 40.3 ns/op 48 B/op 1 allocs/op PASS ok github.com/EDDYCJY/awesomeDefer 3.444s
在開場,我先以不標準的測試基準驗證了先前的測試用例,確確實實在這兩個版本中,defer
的性能獲得了提升,可是看上去彷佛不是百分百提升 30 %。github
0x0070 00112 (main.go:6) CALL runtime.deferproc(SB) 0x0075 00117 (main.go:6) TESTL AX, AX 0x0077 00119 (main.go:6) JNE 137 0x0079 00121 (main.go:7) XCHGL AX, AX 0x007a 00122 (main.go:7) CALL runtime.deferreturn(SB) 0x007f 00127 (main.go:7) MOVQ 56(SP), BP
0x006e 00110 (main.go:4) MOVQ AX, (SP) 0x0072 00114 (main.go:4) CALL runtime.deferprocStack(SB) 0x0077 00119 (main.go:4) TESTL AX, AX 0x0079 00121 (main.go:4) JNE 139 0x007b 00123 (main.go:7) XCHGL AX, AX 0x007c 00124 (main.go:7) CALL runtime.deferreturn(SB) 0x0081 00129 (main.go:7) MOVQ 112(SP), BP
從彙編的角度來看,像是 runtime.deferproc
改爲了 runtime.deferprocStack
調用,難道是作了什麼優化,咱們抱着疑問繼續看下去。golang
type _defer struct { siz int32 siz int32 // includes both arguments and results started bool heap bool sp uintptr // sp at time of defer pc uintptr fn *funcval ...
相較於之前的版本,最小單元的 _defer
結構體主要是新增了 heap
字段,用於標識這個 _defer
是在堆上,仍是在棧上進行分配,其他字段並無明確變動,那咱們能夠把聚焦點放在 defer
的堆棧分配上了,看看是作了什麼事。函數
func deferprocStack(d *_defer) { gp := getg() if gp.m.curg != gp { throw("defer on system stack") } d.started = false d.heap = false d.sp = getcallersp() d.pc = getcallerpc() *(*uintptr)(unsafe.Pointer(&d._panic)) = 0 *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer)) *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d)) return0() }
這一塊代碼挺常規的,主要是獲取調用 defer
函數的函數棧指針、傳入函數的參數具體地址以及PC(程序計數器),這塊在前文 《深刻理解 Go defer》 有詳細介紹過,這裏就再也不贅述了。oop
那這個 deferprocStack
特殊在哪呢,咱們能夠看到它把 d.heap
設置爲了 false
,也就是表明 deferprocStack
方法是針對將 _defer
分配在棧上的應用場景的。性能
那麼問題來了,它又在哪裏處理分配到堆上的應用場景呢?測試
func newdefer(siz int32) *_defer { ... d.heap = true d.link = gp._defer gp._defer = d return d }
那麼 newdefer
是在哪裏調用的呢,以下:優化
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn ... sp := getcallersp() argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn) callerpc := getcallerpc() d := newdefer(siz) ... }
很是明確,先前的版本中調用的 deferproc
方法,如今被用於對應分配到堆上的場景了。ui
deferproc
並無被去掉,而是流程被優化了。deferproc
仍是 deferprocStack
方法,他們分別是針對分配在堆上和棧上的使用場景。// src/cmd/compile/internal/gc/esc.go case ODEFER: if e.loopdepth == 1 { // top level n.Esc = EscNever // force stack allocation of defer record (see ssa.go) break }
// src/cmd/compile/internal/gc/ssa.go case ODEFER: d := callDefer if n.Esc == EscNever { d = callDeferStack } s.call(n.Left, d)
這塊結合來看,核心就是當 e.loopdepth == 1
時,會將逃逸分析結果 n.Esc
設置爲 EscNever
,也就是將 _defer
分配到棧上,那這個 e.loopdepth
到底又是何方神聖呢,咱們再詳細看看代碼,以下:
// src/cmd/compile/internal/gc/esc.go type NodeEscState struct { Curfn *Node Flowsrc []EscStep Retval Nodes Loopdepth int32 Level Level Walkgen uint32 Maxextraloopdepth int32 }
這裏重點查看 Loopdepth
字段,目前它共有三個值標識,分別是:
這個讀起來有點繞,結合咱們上述 e.loopdepth == 1
的表述來看,也就是當 defer func
是頂級函數時,將會分配到棧上。可是若在 defer func
外層出現顯式的迭代循環,又或是出現隱式迭代,將會分配到堆上。其實深層表示的仍是迭代深度的意思,咱們能夠來證明一下剛剛說的方向,顯式迭代的代碼以下:
func main() { for p := 0; p < 10; p++ { defer func() { for i := 0; i < 20; i++ { log.Println("EDDYCJY") } }() } }
查看彙編狀況:
$ go tool compile -S main.go "".main STEXT size=122 args=0x0 locals=0x20 0x0000 00000 (main.go:15) TEXT "".main(SB), ABIInternal, $32-0 ... 0x0048 00072 (main.go:17) CALL runtime.deferproc(SB) 0x004d 00077 (main.go:17) TESTL AX, AX 0x004f 00079 (main.go:17) JNE 83 0x0051 00081 (main.go:17) JMP 33 0x0053 00083 (main.go:17) XCHGL AX, AX 0x0054 00084 (main.go:17) CALL runtime.deferreturn(SB) ...
顯然,最終 defer
調用的是 runtime.deferproc
方法,也就是分配到堆上了,沒毛病。而隱式迭代的話,你能夠藉助 goto
語句去實現這個功能,再本身驗證一遍,這裏就再也不贅述了。
從分析的結果上來看,官方說明的 Go1.13 defer 性能提升 30%,主要來源於其延遲對象的堆棧分配規則的改變,措施是由編譯器經過對 defer
的 for-loop
迭代深度進行分析,若是 loopdepth
爲 1,則設置逃逸分析的結果,將分配到棧上,不然分配到堆上。
的確,我我的以爲對大部分的使用場景來說,是優化了很多,也解決了一些人吐槽 defer
性能 「差」 的問題。另外,我想從 Go1.13 起,你也須要稍微瞭解一下它這塊的機制,別隨隨便便就來個狂野版嵌套迭代 defer
,可能無法效能最大化。
若是你還想了解更多細節,能夠看看 defer
這塊的的提交內容,官方的測試用例也包含在裏面。