目錄
defer 效能最佳化30%
以前和現在比較
defer 最小單元:_defer
deferprocStack
deferproc
小結
優化在哪裡
循環呼叫 defer
显式循环
隐式循环
总结
首頁 後端開發 Golang 使用Go defer時要注意這兩處!

使用Go defer時要注意這兩處!

Jul 10, 2021 pm 03:09 PM
golang

在 Go 語言中 defer 是一個非常有趣的關鍵字特性。例子如下:

package main

import "fmt"

func main() {
    defer fmt.Println("煎鱼了")

    fmt.Println("脑子进")
}
登入後複製

輸出結果是:

脑子进
煎鱼了
登入後複製

在前幾天我的讀者群內有小夥伴討論起了下面這個問題:

使用Go defer時要注意這兩處!

簡單來講,問題就是針對在for 迴圈裡搞defer 關鍵字,是否會造成什麼效能影響

因為在Go 語言的底層資料結構設計上defer 是鍊錶的資料結構:

使用Go defer時要注意這兩處!

大家擔心如果循環過大defer 鍊錶會巨長,不夠「精益求精」。又或是猜想會不會 Go defer 的設計和 Redis 資料結構設計類似,自己做了優化,其實沒啥大影響?

今天這篇文章,我們就來探索循環 Go defer,造成底層鍊錶過長會不會帶來什麼問題,若有,具體有什麼影響?

開始吸魚之路。

defer 效能最佳化30%

在早年Go1.13 時曾經對defer 進行了一輪效能最佳化,在大部分場景下提高了defer 30% 的效能:

使用Go defer時要注意這兩處!

我們來回顧一下Go1.13 的變更,看看Go defer 優化在了哪裡,這是問題的關鍵點。

以前和現在比較

在Go1.12 及以前,呼叫Go defer 時彙編程式碼如下:

    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
登入後複製

在Go1.13 及以後,呼叫Go defer 時彙編程式碼如下:

    0x006e 00110 (main.go:4)    MOVQ    AX, (SP)
    0x0072 00114 (main.go:4)    CALL    runtime.deferprocStack(SB)
    0x0077 00119 (main.go:4)    TESTL    AX, AX
    0x0079 00121 (main.go:4)    JNE    139
    0x007b 00123 (main.go:7)    XCHGL    AX, AX
    0x007c 00124 (main.go:7)    CALL    runtime.deferreturn(SB)
    0x0081 00129 (main.go:7)    MOVQ    112(SP), BP
登入後複製

從彙編的角度來看,像是原本呼叫runtime.deferproc 方法改成了呼叫runtime.deferprocStack 方法,難道做了什麼優化?

我們抱著疑問繼續看下去。

defer 最小單元:_defer

相較於先前的版本,Go defer 的最小單元_defer 結構體主要是新增了heap字段:

type _defer struct {
    siz     int32
    siz     int32 // includes both arguments and results
    started bool
    heap    bool
    sp      uintptr // sp at time of defer
    pc      uintptr
    fn      *funcval
    ...
登入後複製

該字段用於標識這個_defer 是在堆上,還是在堆疊上進行分配,其餘字段並沒有明確變更,那我們可以把聚焦點放在defer 的堆疊分配上了,看看做了什麼事。

deferprocStack

func deferprocStack(d *_defer) {
    gp := getg()
    if gp.m.curg != gp {
        throw("defer on system stack")
    }
    
    d.started = false
    d.heap = false
    d.sp = getcallersp()
    d.pc = getcallerpc()

    *(*uintptr)(unsafe.Pointer(&d._panic)) = 0
    *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
    *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))

    return0()
}
登入後複製

這塊程式碼挺常規的,主要是取得呼叫defer 函數的函數堆疊指標、傳入函數的參數具體位址以及PC(程式計數器),這塊在前文《深入理解Go defer》 有詳細介紹過,這裡就不再贅述了。

這個 deferprocStack 特殊在哪呢?

可以看到它把d.heap 設定為了false,也就是代表deferprocStack 方法是針對將_defer 分配在堆疊上的應用場景的。

deferproc

問題來了,它又在哪裡處理分配到堆上的應用場景?

func newdefer(siz int32) *_defer {
    ...
    d.heap = true
    d.link = gp._defer
    gp._defer = d
    return d
}
登入後複製

具體的newdefer 是在哪裡呼叫的呢,如下:

func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
    ...
    sp := getcallersp()
    argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
    callerpc := getcallerpc()

    d := newdefer(siz)
    ...
}
登入後複製

非常明確,先前的版本中呼叫的deferproc 方法,現在被用來對應分配到堆上的場景了。

小結

  • 可以確定的是 deferproc 並沒有被去掉,而是流程被最佳化了。
  • Go 編譯器會根據應用程式場景去選擇使用 deferprocdeferprocStack 方法,他們分別是針對分配在堆上和堆疊上的使用場景。

優化在哪裡

主要最佳化在於其defer 物件的堆疊分配規則的改變,措施是:
編譯器對deferfor-loop 迭代深度進行分析。

// src/cmd/compile/internal/gc/esc.go
case ODEFER:
    if e.loopdepth == 1 { // top level
        n.Esc = EscNever // force stack allocation of defer record (see ssa.go)
        break
    }
登入後複製

如果 Go 編譯器偵測到循環深度(loopdepth)為 1,則設定逃逸分析的結果,將分配到堆疊上,否則分配到堆疊上。

// src/cmd/compile/internal/gc/ssa.go
case ODEFER:
    d := callDefer
    if n.Esc == EscNever {
        d = callDeferStack
    }
    s.call(n.Left, d)
登入後複製

以此免去了先前頻繁呼叫 systemstackmallocgc 等方法所帶來的大量效能開銷,來達到大部分場景提高效能的作用。

循環呼叫 defer

回到問題本身,知道了 defer 最佳化的原理後。那「循環裡搞defer 關鍵字,是否會造成什麼效能影響?」

最直接的影響就是這大約30% 的效能優化直接全無,且由於姿勢不正確,理論上defer 既有的開銷(鍊錶變長)也變大,效能變差。

因此我們要避免以下兩個場景的程式碼:

  • 显式循环:在调用 defer 关键字的外层有显式的循环调用,例如:for-loop 语句等。
  • 隐式循环:在调用 defer 关键字有类似循环嵌套的逻辑,例如:goto 语句等。

显式循环

第一个例子是直接在代码的 for 循环中使用 defer 关键字:

func main() {
    for i := 0; i <p>这个也是最常见的模式,无论是写爬虫时,又或是 Goroutine 调用时,不少人都喜欢这么写。</p><p>这属于显式的调用了循环。</p><h3 id="隐式循环">隐式循环</h3><p>第二个例子是在代码中使用类似 <code>goto</code> 关键字:</p><pre class="brush:php;toolbar:false">func main() {
    i := 1
food:
    defer func() {}()
    if i == 1 {
        i -= 1
        goto food
    }
}
登入後複製

这种写法比较少见,因为 goto 关键字有时候甚至会被列为代码规范不给使用,主要是会造成一些滥用,所以大多数就选择其实方式实现逻辑。

这属于隐式的调用,造成了类循环的作用。

总结

显然,Defer 在设计上并没有说做的特别的奇妙。他主要是根据实际的一些应用场景进行了优化,达到了较好的性能。

虽然本身 defer 会带一点点开销,但并没有想象中那么的不堪使用。除非你 defer 所在的代码是需要频繁执行的代码,才需要考虑去做优化。

否则没有必要过度纠结,在实际上,猜测或遇到性能问题时,看看 PProf 的分析,看看 defer 是不是在相应的 hot path 之中,再进行合理优化就好。

所谓的优化,可能也只是去掉 defer 而采用手动执行,并不复杂。在编码时避免踩到 defer 的显式和隐式循环这 2 个雷区就可以达到性能最大化了。

更多golang相关技术文章,请访问golang教程栏目!

以上是使用Go defer時要注意這兩處!的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

如何使用 Golang 安全地讀取和寫入檔案? 如何使用 Golang 安全地讀取和寫入檔案? Jun 06, 2024 pm 05:14 PM

在Go中安全地讀取和寫入檔案至關重要。指南包括:檢查檔案權限使用defer關閉檔案驗證檔案路徑使用上下文逾時遵循這些準則可確保資料的安全性和應用程式的健全性。

如何為 Golang 資料庫連線配置連線池? 如何為 Golang 資料庫連線配置連線池? Jun 06, 2024 am 11:21 AM

如何為Go資料庫連線配置連線池?使用database/sql包中的DB類型建立資料庫連線;設定MaxOpenConns以控制最大並發連線數;設定MaxIdleConns以設定最大空閒連線數;設定ConnMaxLifetime以控制連線的最大生命週期。

golang框架的優缺點比較 golang框架的優缺點比較 Jun 05, 2024 pm 09:32 PM

Go框架憑藉高效能和並發性優勢脫穎而出,但也存在一些缺點,例如相對較新、開發者生態系統較小、缺乏某些功能。此外,快速變化和學習曲線可能因框架而異。 Gin框架以其高效路由、內建JSON支援和強大的錯誤處理而成為建立RESTfulAPI的熱門選擇。

Golang框架與Go框架:內部架構與外部特性對比 Golang框架與Go框架:內部架構與外部特性對比 Jun 06, 2024 pm 12:37 PM

GoLang框架與Go框架的差異體現在內部架構與外部特性。 GoLang框架基於Go標準函式庫,擴充其功能,而Go框架由獨立函式庫組成,以實現特定目的。 GoLang框架更靈活,Go框架更容易上手。 GoLang框架在效能上稍有優勢,Go框架的可擴充性更高。案例:gin-gonic(Go框架)用於建立RESTAPI,而Echo(GoLang框架)用於建立Web應用程式。

Golang 框架中的錯誤處理最佳實務有哪些? Golang 框架中的錯誤處理最佳實務有哪些? Jun 05, 2024 pm 10:39 PM

最佳實踐:使用明確定義的錯誤類型(errors套件)建立自訂錯誤提供更多詳細資訊適當記錄錯誤正確傳播錯誤,避免隱藏或抑制根據需要包裝錯誤以添加上下文

如何在 Golang 中將 JSON 資料保存到資料庫中? 如何在 Golang 中將 JSON 資料保存到資料庫中? Jun 06, 2024 am 11:24 AM

可以透過使用gjson函式庫或json.Unmarshal函數將JSON資料儲存到MySQL資料庫中。 gjson函式庫提供了方便的方法來解析JSON字段,而json.Unmarshal函數需要一個目標類型指標來解組JSON資料。這兩種方法都需要準備SQL語句和執行插入操作來將資料持久化到資料庫中。

如何解決golang框架中常見的安全問題? 如何解決golang框架中常見的安全問題? Jun 05, 2024 pm 10:38 PM

如何在Go框架中解決常見的安全問題隨著Go框架在Web開發中的廣泛採用,確保其安全至關重要。以下是解決常見安全問題的實用指南,附帶範例程式碼:1.SQL注入使用預編譯語句或參數化查詢來防止SQL注入攻擊。例如:constquery="SELECT*FROMusersWHEREusername=?"stmt,err:=db.Prepare(query)iferr!=nil{//Handleerror}err=stmt.QueryR

如何找出 Golang 正規表示式符合的第一個子字串? 如何找出 Golang 正規表示式符合的第一個子字串? Jun 06, 2024 am 10:51 AM

FindStringSubmatch函數可找出正規表示式匹配的第一個子字串:此函數傳回包含匹配子字串的切片,第一個元素為整個匹配字串,後續元素為各個子字串。程式碼範例:regexp.FindStringSubmatch(text,pattern)傳回符合子字串的切片。實戰案例:可用於匹配電子郵件地址中的域名,例如:email:="user@example.com",pattern:=@([^\s]+)$獲取域名match[1]。

See all articles