上個月在 @polaris @軒脈刃 的全棧技術(shù)群里看到一個小伙伴問 “說 defer 在棧退出時執(zhí)行蜀肘,會有性能損耗,盡量不要用冰单,這個怎么解?”灸促。
恰好前段時間寫了一篇 《深入理解 Go defer》 去詳細(xì)剖析 defer
關(guān)鍵字诫欠。那么這一次簡單結(jié)合前文對這個問題進(jìn)行探討一波,希望對你有所幫助浴栽,但在此之前希望你花幾分鐘荒叼,自己思考一下答案,再繼續(xù)往下看典鸡。
測試
func DoDefer(key, value string) {
defer func(key, value string) {
_ = key + value
}(key, value)
}
func DoNotDefer(key, value string) {
_ = key + value
}
基準(zhǔn)測試:
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")
}
}
輸出結(jié)果:
$ 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
從結(jié)果上來被廓,使用 defer
后的函數(shù)開銷確實(shí)比沒使用高了不少,這損耗用到哪里去了呢萝玷?
想一下
$ 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
關(guān)鍵字其實(shí)涉及了一系列的連鎖調(diào)用嫁乘,內(nèi)部 runtime
函數(shù)的調(diào)用就至少多了三步,分別是 runtime.deferproc
一次和 runtime.deferreturn
兩次球碉。
而這還只是在運(yùn)行時的顯式動作蜓斧,另外編譯器做的事也不少,例如:
- 在
deferproc
階段(注冊延遲調(diào)用)睁冬,還得獲取/傳入目標(biāo)函數(shù)地址挎春、函數(shù)參數(shù)等等。 - 在
deferreturn
階段,需要在函數(shù)調(diào)用結(jié)尾處插入該方法的調(diào)用直奋,同時若有被defer
的函數(shù)能庆,還需要使用runtime·jmpdefer
進(jìn)行跳轉(zhuǎn)以便于后續(xù)調(diào)用。
這一些動作途中還要涉及最小單元 _defer
的獲取/生成脚线, defer
和 recover
鏈表的邏輯處理和消耗等動作搁胆。
Q&A
最后討論的時候有提到 “問題指的是本來就是用來執(zhí)行 close() 一些操作的,然后說盡量不能用殉挽,例子就把 defer db.close() 前面的 defer 刪去了” 這個疑問丰涉。
這是一個比較類似 “教科書” 式的說法,在一些入門教程中會潛移默化的告訴你在資源控制后加個 defer
延遲關(guān)閉一下斯碌。例如:
resp, err := http.Get(...)
if err != nil {
return err
}
defer resp.Body.Close()
但是一定得這么寫嗎一死?其實(shí)并不,很多人給出的理由都是 “怕你忘記” 這種說辭傻唾,這沒有毛病投慈。但需要認(rèn)清場景,假設(shè)我的應(yīng)用場景如下:
resp, err := http.Get(...)
if err != nil {
return err
}
defer resp.Body.Close()
// do something
time.Sleep(time.Second * 60)
嗯冠骄,一個請求當(dāng)然沒問題伪煤,流量、并發(fā)一下子大了呢凛辣,那可能就是個災(zāi)難了抱既。你想想為什么?從常見的 defer
+ close
的使用組合來講扁誓,用之前建議先看清楚應(yīng)用場景防泵,在保證無異常的情況下確保盡早關(guān)閉才是首選。如果只是小范圍調(diào)用很快就返回的話蝗敢,偷個懶直接一套組合拳出去也未嘗不可捷泞。
結(jié)論
一個 defer
關(guān)鍵字實(shí)際上包含了不少的動作和處理,和你單純調(diào)用一個函數(shù)一條指令是沒法比的寿谴。而與對照物相比锁右,它確確實(shí)實(shí)是有性能損耗,目前延遲調(diào)用的全部開銷大約在 50ns讶泰,但 defer
所提供的作用遠(yuǎn)遠(yuǎn)大于此咏瑟,你從全局來看,它的損耗非常小痪署,并且官方還不斷地在優(yōu)化中响蕴。
因此,對于 “Go defer 會有性能損耗惠桃,盡量不能用浦夷?” 這個問題辖试,我認(rèn)為該用就用,應(yīng)該及時關(guān)閉就不要延遲劈狐,在 hot paths 用時一定要想清楚場景罐孝。
補(bǔ)充
最后補(bǔ)充上柴大的回復(fù):“不是性能問題,defer 最大的功能是 Panic 后依然有效肥缔。如果沒有 defer莲兢,Panic 后就會導(dǎo)致 unlock 丟失,從而導(dǎo)致死鎖了”续膳,非常經(jīng)典改艇。