探究 Go 語言 defer 語句的三種機制

Golang 的 1.13 版本 與 1.14 版本對 defer 進行了兩次優化,使得 defer 的性能開銷在大部分場景下都獲得大幅下降,其中到底經歷了什麼原理?git

這是由於這兩個版本對 defer 各加入了一項新的機制,使得 defer 語句在編譯時,編譯器會根據不一樣版本與狀況,對每一個 defer 選擇不一樣的機制,以更輕量的方式運行調用。github

堆上分配

在 Golang 1.13 以前的版本中,全部 defer 都是在堆上分配,該機制在編譯時會進行兩個步驟:golang

  1. defer 語句的位置插入 runtime.deferproc,當被執行時,延遲調用會被保存爲一個 _defer 記錄,並將被延遲調用的入口地址及其參數複製保存,存入 Goroutine 的調用鏈表中。
  2. 在函數返回以前的位置插入 runtime.deferreturn,當被執行時,會將延遲調用從 Goroutine 鏈表中取出並執行,多個延遲調用則以 jmpdefer 尾遞歸調用方式連續執行。

這種機制的主要性能問題存在於每一個 defer 語句產生記錄時的內存分配,以及記錄參數和完成調用時參數移動的系統調用開銷。編程

棧上分配

Go 1.13 版本新加入 deferprocStack 實現了在棧上分配的形式來取代 deferproc,相比堆上分配,棧上分配在函數返回後 _defer 便獲得釋放,省去了內存分配時產生的性能開銷,只需適當維護 _defer 的鏈表便可。微信

編譯器有本身的邏輯去選擇使用 deferproc 仍是 deferprocStack,大部分狀況下都會使用後者,性能會提高約 30%。不過在 defer 語句出如今了循環語句裏,或者沒法執行更高階的編譯器優化時,亦或者同一個函數中使用了過多的 defer 時,依然會使用 deferproc函數

開放編碼

Go 1.14 版本繼續加入了開發編碼(open coded),該機制會將延遲調用直接插入函數返回以前,省去了運行時的 deferprocdeferprocStack 操做,在運行時的 deferreturn 也不會進行尾遞歸調用,而是直接在一個循環中遍歷全部延遲函數執行。性能

這種機制使得 defer開銷幾乎能夠忽略,惟一的運行時成本就是存儲參與延遲調用的相關信息,不過使用此機制須要一些條件:學習

  1. 沒有禁用編譯器優化,即沒有設置 -gcflags "-N"
  2. 函數內 defer 的數量不超過 8 個,且返回語句與延遲語句個數的乘積不超過 15;
  3. defer 不是在循環語句中。

該機制還引入了一種元素 —— 延遲比特(defer bit),用於運行時記錄每一個 defer 是否被執行(尤爲是在條件判斷分支中的 defer),從而便於判斷最後的延遲調用該執行哪些函數。優化

延遲比特的原理: 同一個函數內每出現一個 defer 都會爲其分配 1 個比特,若是被執行到則設爲 1,不然設爲 0,當到達函數返回以前須要判斷延遲調用時,則用掩碼判斷每一個位置的比特,若爲 1 則調用延遲函數,不然跳過。ui

爲了輕量,官方將延遲比特限制爲 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 語言本來:
changkun.de/golang/zh-c…

[2] 峯雲就她了 - go1.14實現defer性能大幅度提高原理:
xiaorui.cc/archives/65…

[3] 34481-opencoded-defers:
github.com/golang/prop…


本文屬於原創,首發於微信公衆號「面向人生編程」,如需轉載請後臺留言。

關注後回覆如下信息獲取更多資源 回覆【資料】獲取 Python / Java 等學習資源 回覆【插件】獲取爬蟲經常使用的 Chrome 插件 回覆【知乎】獲取最新知乎模擬登陸
相關文章
相關標籤/搜索