原文地址:Go defer 會有性能損耗,儘可能不要用?html
上個月在 @polaris @軒脈刃 的全棧技術羣裏看到一個小夥伴問 「說 defer 在棧退出時執行,會有性能損耗,儘可能不要用,這個怎麼解?」。git
剛好前段時間寫了一篇 《深刻理解 Go defer》 去詳細剖析 defer
關鍵字。那麼這一次簡單結合前文對這個問題進行探討一波,但願對你有所幫助,但在此以前但願你花幾分鐘,本身思考一下答案,再繼續往下看。github
func DoDefer(key, value string) { defer func(key, value string) { _ = key + value }(key, value) } func DoNotDefer(key, value string) { _ = key + value }
基準測試:golang
func BenchmarkDoDefer(b *testing.B) { for i := 0; i < b.N; i++ { DoDefer("煎魚", "https://github.com/EDDYCJY/blog") } } func BenchmarkDoNotDefer(b *testing.B) { for i := 0; i < b.N; i++ { DoNotDefer("煎魚", "https://github.com/EDDYCJY/blog") } }
輸出結果:segmentfault
$ 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
從結果上來,使用 defer
後的函數開銷確實比沒使用高了很多,這損耗用到哪裏去了呢?併發
$ go tool compile -S main.go "".main STEXT size=163 args=0x0 locals=0x40 ... 0x0059 00089 (main.go:6) MOVQ AX, 16(SP) 0x005e 00094 (main.go:6) MOVQ $1, 24(SP) 0x0067 00103 (main.go:6) MOVQ $1, 32(SP) 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 0x0084 00132 (main.go:7) ADDQ $64, SP 0x0088 00136 (main.go:7) RET 0x0089 00137 (main.go:6) XCHGL AX, AX 0x008a 00138 (main.go:6) CALL runtime.deferreturn(SB) 0x008f 00143 (main.go:6) MOVQ 56(SP), BP 0x0094 00148 (main.go:6) ADDQ $64, SP 0x0098 00152 (main.go:6) RET ...
咱們在前文提到 defer
關鍵字其實涉及了一系列的連鎖調用,內部 runtime
函數的調用就至少多了三步,分別是 runtime.deferproc
一次和 runtime.deferreturn
兩次。函數
而這還只是在運行時的顯式動做,另外編譯器作的事也很多,例如:性能
deferproc
階段(註冊延遲調用),還得獲取/傳入目標函數地址、函數參數等等。deferreturn
階段,須要在函數調用結尾處插入該方法的調用,同時如有被 defer
的函數,還須要使用 runtime·jmpdefer
進行跳轉以便於後續調用。這一些動做途中還要涉及最小單元 _defer
的獲取/生成, defer
和 recover
鏈表的邏輯處理和消耗等動做。測試
最後討論的時候有提到 「問題指的是原本就是用來執行 close() 一些操做的,而後說盡可能不能用,例子就把 defer db.close() 前面的 defer 刪去了」 這個疑問。優化
這是一個比較相似 「教科書」 式的說法,在一些入門教程中會潛移默化的告訴你在資源控制後加個 defer
延遲關閉一下。例如:
resp, err := http.Get(...) if err != nil { return err } defer resp.Body.Close()
可是必定得這麼寫嗎?其實並不,不少人給出的理由都是 「怕你忘記」 這種說辭,這沒有毛病。但須要認清場景,假設個人應用場景以下:
resp, err := http.Get(...) if err != nil { return err } defer resp.Body.Close() // do something time.Sleep(time.Second * 60)
嗯,一個請求固然沒問題,流量、併發一會兒大了呢,那可能就是個災難了。你想一想爲何?從常見的 defer
+ close
的使用組合來說,用以前建議先看清楚應用場景,在保證無異常的狀況下確保儘早關閉纔是首選。若是隻是小範圍調用很快就返回的話,偷個懶直接一套組合拳出去也何嘗不可。
一個 defer
關鍵字實際上包含了很多的動做和處理,和你單純調用一個函數一條指令是無法比的。而與對照物相比,它確確實實是有性能損耗,目前延遲調用的所有開銷大約在 50ns,但 defer
所提供的做用遠遠大於此,你從全局來看,它的損耗很是小,而且官方還不斷地在優化中。
所以,對於 「Go defer 會有性能損耗,儘可能不能用?」 這個問題,我認爲該用就用,應該及時關閉就不要延遲,在 hot paths 用時必定要想清楚場景。
補充上柴大的回覆:「不是性能問題,defer 最大的功能是 Panic 後依然有效。若是沒有 defer,Panic 後就會致使 unlock 丟失,從而致使死鎖了」,很是經典。