我們常常聽到別人說:”defer 在棧退出時執行,會有性能損耗,儘量不要用。“ 前面的博客 defer原理 我們分析了defer延遲調用的底層實現原理 。下面我們就基於那篇原理分析文章,來分析一下 defer 延遲調用的性能損耗。
基準測試
package main
import (
"sync"
"testing"
)
var lock sync.Mutex
func NoDefer() {
lock.Lock()
lock.Unlock()
}
func Defer() {
lock.Lock()
defer lock.Unlock()
}
func BenchmarkNoDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
NoDefer()
}
}
func BenchmarkDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
Defer()
}
}
benchmark的執行結果如下:
ytlou@ytlou-mac ~/Desktop/golang/golang_study/study/basic/defer master ●✚ go test -bench=. defer_latency_test.go
goos: darwin
goarch: amd64
BenchmarkNoDefer-12 90020019 11.3 ns/op
BenchmarkDefer-12 37018846 33.0 ns/op
PASS
ok command-line-arguments 2.294s
我們19款16G內存Mac上測試,結果顯示使用defer後的函數開銷確實比沒使用高了不少,這損耗用到哪裏去了呢?
defer開銷
我們回憶一下 defer原理 中對於defer延遲調用編譯成彙編之後的彙編代碼,defer 關鍵字其實涉及了一系列的連鎖調用,內部 runtime 函數的調用就至少多了三步,分別是 runtime.deferproc 一次和 runtime.deferreturn 兩次。
而這還只是在運行時的顯式動作,另外編譯器做的事也不少,例如:
- 在 deferprocStack或者deferproc 階段(註冊延遲調用),還得獲取/傳入目標函數地址、函數參數等等。
- 在 deferreturn 階段,需要在函數調用結尾處插入該方法的調用,同時若有被 defer 的函數,還需要使用 runtime·jmpdefer 進行跳轉以便於後續調用。
這一些動作途中還要涉及最小單元 _defer 的獲取/創建(在堆或棧), defer 和 recover 鏈表的邏輯處理和消耗等動作。
實際場景
defer 很多使用場景都是資源的close, 確保資源能夠在函數棧調用結束的時候釋放資源。比如下面的例子:
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)
嗯,一個請求當然沒問題,流量、併發一下子大了呢,那可能就是個災難了。你想想爲什麼?併發高的時候,持有成千上萬的 http response對象,但是又沒有及時的釋放掉,而是使用 defer延遲釋放,導致併發高的時候造成很嚴重的性能問題。
從常見的 defer + close 的使用組合來講,用之前建議先看清楚應用場景,在保證無異常的情況下確保儘早關閉纔是首選。如果只是小範圍調用很快就返回的話,偷個懶直接一套組合拳出去也未嘗不可。
結論
一個 defer 關鍵字實際上包含了不少的動作和處理,和你單純調用一個函數一條指令是沒法比的。而與對照物相比,它確確實實是有性能損耗,目前延遲調用的全部開銷大約在 30ns,但 defer 所提供的作用遠遠大於此,你從全局來看,它的損耗非常小,並且官方還不斷地在優化中。
因此,對於 “Go defer 會有性能損耗,儘量不能用?” 這個問題,我認爲該用就用,應該及時關閉就不要延遲,使用時一定要想清楚場景。
還有一點:defer使用最重要的不是性能問題,defer 最大的功能是 Panic 後依然有效。如果沒有 defer,Panic 後就會導致 unlock 丟失,從而導致死鎖了。 我覺得這個case非常經典。