Golang 的 1.13 版本 與 1.14 版本對 defer 進行了兩次優化,使得 defer 的性能開銷在大部分場景下都得到大幅降低,其中到底經歷了什么原理?
這是因為這兩個版本對 defer 各加入了一項新的機制,使得 defer 陳述句在編譯時,編譯器會根據不同版本與情況,對每個 defer 選擇不同的機制,以更輕量的方式運行呼叫,
堆上分配
在 Golang 1.13 之前的版本中,所有 defer 都是在堆上分配,該機制在編譯時會進行兩個步驟:
- 在
defer陳述句的位置插入runtime.deferproc,當被執行時,延遲呼叫會被保存為一個_defer記錄,并將被延遲呼叫的入口地址及其引數復制保存,存入 Goroutine 的呼叫鏈表中, - 在函式回傳之前的位置插入
runtime.deferreturn,當被執行時,會將延遲呼叫從 Goroutine 鏈表中取出并執行,多個延遲呼叫則以 jmpdefer 尾遞回呼叫方式連續執行,
這種機制的主要性能問題存在于每個 defer 陳述句產生記錄時的記憶體分配,以及記錄引數和完成呼叫時引數移動的系統呼叫開銷,
堆疊上分配
Go 1.13 版本新加入 deferprocStack 實作了在堆疊上分配的形式來取代 deferproc,相比后者,堆疊上分配在函式回傳后 _defer 便得到釋放,省去了記憶體分配時產生的性能開銷,只需適當維護 _defer 的鏈表即可,
編譯器有自己的邏輯去選擇使用 deferproc 還是 deferprocStack,大部分情況下都會使用后者,性能會提升約 30%,不過在 defer 陳述句出現在了回圈陳述句里,或者無法執行更高階的編譯器優化時,亦或者同一個函式中使用了過多的 defer 時,依然會使用 deferproc,
開放編碼
Go 1.14 版本繼續加入了開發編碼(open coded),該機制會將延遲呼叫直接插入函式回傳之前,省去了運行時的 deferproc 或 deferprocStack 操作,在運行時的 deferreturn 也不會進行尾遞回呼叫,而是直接在一個回圈中遍歷所有延遲函式執行,
這種機制使得 defer 的開銷幾乎可以忽略,唯一的運行時成本就是存盤參與延遲呼叫的相關資訊,不過使用此機制需要一些條件:
- 沒有禁用編譯器優化,即沒有設定
-gcflags "-N"; - 函式內
defer的數量不超過 8 個,且回傳陳述句與延遲陳述句個數的乘積不超過 15; defer不是在回圈陳述句中,
該機制還引入了一種元素 —— 延遲位元(defer bit),用于運行時記錄每個 defer 是否被執行(尤其是在條件判斷分支中的 defer),從而便于判斷最后的延遲呼叫該執行哪些函式,
延遲位元的原理:
同一個函式內每出現一個 defer 都會為其分配 1 個位元,如果被執行到則設為 1,否則設為 0,當到達函式回傳之前需要判斷延遲呼叫時,則用掩碼判斷每個位置的位元,若為 1 則呼叫延遲函式,否則跳過,
為了輕量,官方將延遲位元限制為 1 個位元組,即 8 個位元,這就是為什么不能超過 8 個 defer 的原因,若超過依然會選擇堆疊分配,但顯然大部分情況不會超過 8 個,
用代碼演示如下:
deferBits = 0 // 延遲位元初始值 00000000
deferBits |= 1<<0 // 執行第一個 defer,設定為 00000001
_f1 = f1 // 延遲函式
_a1 = a1 // 延遲函式的引數
if cond {
// 如果第二個 defer 被執行,則設定為 00000011,否則依然為 00000001
deferBits |= 1<<1
_f2 = f2
_a2 = a2
}
...
exit:
// 函式回傳之前,倒序檢查延遲位元,通過掩碼逐位進行與運算,來判斷是否呼叫函式
// 假如 deferBits 為 00000011,則 00000011 & 00000010 != 0,因此呼叫 f2
// 否則 00000001 & 00000010 == 0,不呼叫 f2
if deferBits & 1<<1 != 0 {
deferBits &^= 1<<1 // 移位為下次判斷準備
_f2(_a2)
}
// 同理,由于 00000001 & 00000001 != 0,呼叫 f1
if deferBits && 1<<0 != 0 {
deferBits &^= 1<<0
_f1(_a1)
}
總結
以往 Golang defer 陳述句的性能問題一直飽受詬病,最近正式發布的 1.14 版本終于為這個爭議畫上了階段性的句號,如果不是在特殊情況下,我們不需要再計較 defer 的性能開銷,
參考資料
[1] Ou Changkun - Go 語言原本:
https://changkun.de/golang/zh-cn/part2runtime/ch09lang/defer/
[2] 峰云就她了 - go1.14實作defer性能大幅度提升原理:
http://xiaorui.cc/archives/6579
[3] 34481-opencoded-defers:
https://github.com/golang/proposal/blob/master/design/34481-opencoded-defers.md
本文屬于原創,首發于微信公眾號「面向人生編程」,如需轉載請后臺留言,

關注后回復以下資訊獲取更多資源
回復【資料】獲取 Python / Java 等學習資源
回復【插件】獲取爬蟲常用的 Chrome 插件
回復【知乎】獲取最新知乎模擬登錄
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/38575.html
標籤:Go
上一篇:Golang自學系列
下一篇:docker常用命令
