Go語言defer語句的三種機(jī)制整理
Golang 的 1.13 版本 與 1.14 版本對 defer 進(jìn)行了兩次優(yōu)化,使得 defer 的性能開銷在大部分場景下都得到大幅降低,其中到底經(jīng)歷了什么原理?
這是因?yàn)檫@兩個版本對 defer 各加入了一項(xiàng)新的機(jī)制,使得 defer 語句在編譯時,編譯器會根據(jù)不同版本與情況,對每個 defer 選擇不同的機(jī)制,以更輕量的方式運(yùn)行調(diào)用。
堆上分配
在 Golang 1.13 之前的版本中,所有 defer 都是在堆上分配,該機(jī)制在編譯時會進(jìn)行兩個步驟:
- 在 defer 語句的位置插入 runtime.deferproc,當(dāng)被執(zhí)行時,延遲調(diào)用會被保存為一個 _defer 記錄,并將被延遲調(diào)用的入口地址及其參數(shù)復(fù)制保存,存入 Goroutine 的調(diào)用鏈表中。
- 在函數(shù)返回之前的位置插入 runtime.deferreturn,當(dāng)被執(zhí)行時,會將延遲調(diào)用從 Goroutine 鏈表中取出并執(zhí)行,多個延遲調(diào)用則以 jmpdefer 尾遞歸調(diào)用方式連續(xù)執(zhí)行。
這種機(jī)制的主要性能問題存在于每個 defer 語句產(chǎn)生記錄時的內(nèi)存分配,以及記錄參數(shù)和完成調(diào)用時參數(shù)移動的系統(tǒng)調(diào)用開銷。
棧上分配
Go 1.13 版本新加入 deferprocStack 實(shí)現(xiàn)了在棧上分配的形式來取代 deferproc,相比后者,棧上分配在函數(shù)返回后 _defer 便得到釋放,省去了內(nèi)存分配時產(chǎn)生的性能開銷,只需適當(dāng)維護(hù) _defer 的鏈表即可。
編譯器有自己的邏輯去選擇使用 deferproc 還是 deferprocStack,大部分情況下都會使用后者,性能會提升約 30%。不過在 defer 語句出現(xiàn)在了循環(huán)語句里,或者無法執(zhí)行更高階的編譯器優(yōu)化時,亦或者同一個函數(shù)中使用了過多的 defer 時,依然會使用 deferproc。
開放編碼
Go 1.14 版本繼續(xù)加入了開發(fā)編碼(open coded),該機(jī)制會將延遲調(diào)用直接插入函數(shù)返回之前,省去了運(yùn)行時的 deferproc 或 deferprocStack 操作,在運(yùn)行時的 deferreturn 也不會進(jìn)行尾遞歸調(diào)用,而是直接在一個循環(huán)中遍歷所有延遲函數(shù)執(zhí)行。
這種機(jī)制使得 defer 的開銷幾乎可以忽略,唯一的運(yùn)行時成本就是存儲參與延遲調(diào)用的相關(guān)信息,不過使用此機(jī)制需要一些條件:
- 沒有禁用編譯器優(yōu)化,即沒有設(shè)置 -gcflags "-N";
- 函數(shù)內(nèi) defer 的數(shù)量不超過 8 個,且返回語句與延遲語句個數(shù)的乘積不超過 15;
- defer 不是在循環(huán)語句中。
該機(jī)制還引入了一種元素 —— 延遲比特(defer bit),用于運(yùn)行時記錄每個 defer 是否被執(zhí)行(尤其是在條件判斷分支中的 defer),從而便于判斷最后的延遲調(diào)用該執(zhí)行哪些函數(shù)。
延遲比特的原理:
同一個函數(shù)內(nèi)每出現(xiàn)一個 defer 都會為其分配 1 個比特,如果被執(zhí)行到則設(shè)為 1,否則設(shè)為 0,當(dāng)?shù)竭_(dá)函數(shù)返回之前需要判斷延遲調(diào)用時,則用掩碼判斷每個位置的比特,若為 1 則調(diào)用延遲函數(shù),否則跳過。
為了輕量,官方將延遲比特限制為 1 個字節(jié),即 8 個比特,這就是為什么不能超過 8 個 defer 的原因,若超過依然會選擇堆棧分配,但顯然大部分情況不會超過 8 個。
用代碼演示如下:
deferBits = 0 // 延遲比特初始值 00000000
deferBits |= 1<<0 // 執(zhí)行第一個 defer,設(shè)置為 00000001
_f1 = f1 // 延遲函數(shù)
_a1 = a1 // 延遲函數(shù)的參數(shù)
if cond {
// 如果第二個 defer 被執(zhí)行,則設(shè)置為 00000011,否則依然為 00000001
deferBits |= 1<<1
_f2 = f2
_a2 = a2
}
...
exit:
// 函數(shù)返回之前,倒序檢查延遲比特,通過掩碼逐位進(jìn)行與運(yùn)算,來判斷是否調(diào)用函數(shù)
// 假如 deferBits 為 00000011,則 00000011 & 00000010 != 0,因此調(diào)用 f2
// 否則 00000001 & 00000010 == 0,不調(diào)用 f2
if deferBits & 1<<1 != 0 {
deferBits &^= 1<<1 // 移位為下次判斷準(zhǔn)備
_f2(_a2)
}
// 同理,由于 00000001 & 00000001 != 0,調(diào)用 f1
if deferBits && 1<<0 != 0 {
deferBits &^= 1<<0
_f1(_a1)
}
總結(jié)
以往 Golang defer 語句的性能問題一直飽受詬病,最近正式發(fā)布的 1.14 版本終于為這個爭議畫上了階段性的句號。如果不是在特殊情況下,我們不需要再計(jì)較 defer 的性能開銷。
參考資料
[2] 峰云就她了 - go1.14實(shí)現(xiàn)defer性能大幅度提升原理
到此這篇關(guān)于Go語言defer語句的三種機(jī)制整理的文章就介紹到這了,更多相關(guān)探究Go語言defer語句的三種機(jī)制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang中按照結(jié)構(gòu)體的某個字段排序?qū)嵗a
在任何編程語言中,關(guān)乎到數(shù)據(jù)的排序都會有對應(yīng)的策略,下面這篇文章主要給大家介紹了關(guān)于golang中按照結(jié)構(gòu)體的某個字段排序的相關(guān)資料,需要的朋友可以參考下2022-05-05
Go?Singleflight導(dǎo)致死鎖問題解決分析
這篇文章主要為大家介紹了Go?Singleflight導(dǎo)致死鎖問題解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
Golang?urfave/cli庫簡單應(yīng)用示例詳解
這篇文章主要為大家介紹了Golang?urfave/cli庫簡單應(yīng)用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
一文帶你了解Golang中interface的設(shè)計(jì)與實(shí)現(xiàn)
本文就來詳細(xì)說說為什么說?接口本質(zhì)是一種自定義類型,以及這種自定義類型是如何構(gòu)建起?go?的?interface?系統(tǒng)的,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-01-01
解決電腦用GoLand太卡將VsCode定制成Go IDE步驟過程
這篇文章主要為大家介紹了解決電腦用GoLand太卡,將VsCode定制成Go IDE步驟過程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11
在?Go?語言中使用?regexp?包處理正則表達(dá)式的操作
正則表達(dá)式是處理字符串時一個非常強(qiáng)大的工具,而?Go?語言的?regexp?包提供了簡單而強(qiáng)大的接口來使用正則表達(dá)式,本文將介紹如何在?Go?中使用?regexp?包來編譯和執(zhí)行正則表達(dá)式,以及如何從文本中匹配和提取信息,感興趣的朋友一起看看吧2023-12-12

