一文帶大家了解Go語言中的內(nèi)聯(lián)優(yōu)化
內(nèi)聯(lián)優(yōu)化是一種常見的編譯器優(yōu)化策略,通俗來講,就是把函數(shù)在它被調(diào)用的地方展開,這樣可以減少函數(shù)調(diào)用所帶來的開銷(棧的創(chuàng)建、參數(shù)的拷貝等)。
當(dāng)函數(shù)/方法被內(nèi)聯(lián)時(shí),具體是什么樣的表現(xiàn)呢?
觀察內(nèi)聯(lián)
舉個(gè)例子,現(xiàn)在有以下代碼
// ValidateName 驗(yàn)證給定的用戶名是否合法 // //go:noinline func ValidateName(name string) bool { // AX: 字符串指針 BX: 字符串長度 if len(name) < 1 { return false } else if len(name) > 12 { return false } return true } //go:noinline func (s *Server) CreateUser(name string, password string) error { if !ValidateName(name) { return errors.New("invalid name") } // ... return nil } type Server struct{}
為了便于理解,我為函數(shù)和方法增加了//go:noinline
注釋。Go編譯器在遇到該注釋時(shí),不會將函數(shù)/方法進(jìn)行內(nèi)聯(lián)處理。我們先看一下禁止內(nèi)聯(lián)時(shí),該段代碼生成的匯編指令:
// ... // ValidateName函數(shù) // 此時(shí): // AX寄存器: 指向name字符串?dāng)?shù)組的指針 // BX寄存器: name字符串的長度 TEXT github.com/bootun/example/user.ValidateName(SB) github.com/bootun/example/user/user.go user.go:9 0x4602c0 MOVQ AX, 0x8(SP) // 保存name字符串的指針到棧上(后面沒有用到) user.go:10 0x4602c5 TESTQ BX, BX // BX & BX, 用來檢測BX是否為0, 等價(jià)于:CMPQ 0, BX user.go:10 0x4602c8 JE 0x4602d9 // 如果為0則跳轉(zhuǎn)到0x4602d9 user.go:12 0x4602ca CMPQ $0xc, BX // 比較常數(shù)12和name的長度 user.go:12 0x4602ce JLE 0x4602d3 // 小于等于12則跳轉(zhuǎn)到0x4602d3 user.go:13 0x4602d0 XORL AX, AX // return false user.go:13 0x4602d2 RET user.go:15 0x4602d3 MOVL $0x1, AX // return true user.go:15 0x4602d8 RET user.go:11 0x4602d9 XORL AX, AX // return false user.go:11 0x4602db RET // CreateUser方法 TEXT github.com/bootun/example/user.(*Server).CreateUser(SB) /github.com/bootun/example/user/user.go // 省略了一些函數(shù)調(diào)用前的準(zhǔn)備工作(寄存器賦值等操作) user.go:20 0x460300 CALL user.ValidateName(SB) user.go:20 0x460305 TESTL AL, AL user.go:20 0x460307 JE 0x460317 user.go:24 0x460309 XORL AX, AX user.go:24 0x46030b XORL BX, BX user.go:24 0x46030d MOVQ 0x10(SP), BP user.go:24 0x460312 ADDQ $0x18, SP user.go:24 0x460316 RET errors.go:62 0x460317 LEAQ 0x9302(IP), AX errors.go:62 0x46031e NOPW errors.go:62 0x460320 CALL runtime.newobject(SB) // ...
上面的匯編里只截取了最關(guān)鍵的兩段: ValidateName
函數(shù)和CreateUser
方法。
看不懂匯編的同學(xué)也沒關(guān)系,注意看CreateUser
方法內(nèi)有一行user.go:20
CALL user.ValidateName
, 說明在CreateUser
方法內(nèi)調(diào)用了ValidateName
函數(shù),剛好和我們的代碼能夠?qū)?yīng)的上。
現(xiàn)在讓我們?nèi)サ粼创aValidateName
函數(shù)上的//go:noinline
再次編譯后查看生成的匯編指令:
如果你想使用文章里的代碼進(jìn)行嘗試,請不要?jiǎng)h除CreateUser
方法上的//go:noinline
,因?yàn)槔又械?code>CreateUser太簡短了,編譯器會把它也內(nèi)聯(lián)優(yōu)化掉,不方便我們進(jìn)行試驗(yàn)和觀察
// CreateUser函數(shù) // 此時(shí): // AX寄存器: 方法Recever,即Server結(jié)構(gòu)體 // BX寄存器: name字符串的指針 // CX寄存器: name字符串的長度 TEXT github.com/bootun/example/user.(*Server).CreateUser(SB) /github.com/bootun/example/user/user.go // ... user.go:18 0x4602d4 MOVQ BX, 0x28(SP) // 保存name字符串的指針到棧上 user.go:19 0x4602d9 TESTQ CX, CX // 驗(yàn)證name的長度是否為0 user.go:9 0x4602dc JE 0x4602e6 // 為0則跳轉(zhuǎn)到0x4602e6 user.go:9 0x4602de NOPW user.go:11 0x4602e0 CMPQ $0xc, CX // 比較常數(shù)12和字符串的長度 user.go:11 0x4602e4 JLE 0x460318 // 小于等于則跳轉(zhuǎn)到0x460318繼續(xù)執(zhí)行(name合法) errors.go:62 0x4602e6 LEAQ 0x9333(IP), AX // 構(gòu)造錯(cuò)誤返回 errors.go:62 0x4602ed CALL runtime.newobject(SB) errors.go:62 0x4602f2 MOVQ $0xc, 0x8(AX) // ... user.go:23 0x460318 XORL AX, AX // AX = 0 user.go:23 0x46031a XORL BX, BX // BX = 0 user.go:23 0x46031c MOVQ 0x10(SP), BP // 恢復(fù)BP寄存器 user.go:23 0x460321 ADDQ $0x18, SP // 增加棧指針, 減小棧空間 user.go:23 0x460325 RET // return // ...
觀察這一次的代碼可以發(fā)現(xiàn),ValidateName
函數(shù)的邏輯直接被內(nèi)嵌到了CreateUser
方法里展開了。我們在生成的匯編代碼里也搜索不到ValidateName
相關(guān)的符號了。 現(xiàn)在的代碼等價(jià)于:
func (s *Server) CreateUser(name string, password string) error { if len(name) < 1 { return errors.New("invalid name") } else if len(name) > 12 { return errors.New("invalid name") } return nil }
什么樣的函數(shù)會被內(nèi)聯(lián)?
內(nèi)聯(lián)相關(guān)的代碼在cmd/compile/internal/inline/inl.go
里,屬于編譯器的一部分。在該文件的最上面有這樣一段注釋, 里面很好的概括了內(nèi)聯(lián)的控制和規(guī)則:
// The Debug.l flag controls the aggressiveness. Note that main() swaps level 0 and 1, // making 1 the default and -l disable. Additional levels (beyond -l) may be buggy and // are not supported. // 0: disabled // 1: 80-nodes leaf functions, oneliners, panic, lazy typechecking (default) // 2: (unassigned) // 3: (unassigned) // 4: allow non-leaf functions // // At some point this may get another default and become switch-offable with -N. // // The -d typcheckinl flag enables early typechecking of all imported bodies, // which is useful to flush out bugs. // // The Debug.m flag enables diagnostic output. a single -m is useful for verifying
總結(jié)一下上面這段話中的核心部分:
- 80個(gè)節(jié)點(diǎn)的葉子函數(shù),oneliners,panic,懶惰的類型檢查 會被內(nèi)聯(lián)
- 使用
-N -l
來告訴編譯器禁止內(nèi)聯(lián) - 使用
-m
啟用診斷輸出
也就是說,只要我們的函數(shù)/方法足夠小,就可能會被內(nèi)聯(lián)。 因此,很多人會使用許多小的函數(shù)組合來代替大段代碼提升性能。比如我們經(jīng)常使用的互斥鎖(標(biāo)準(zhǔn)庫中sync
包里的Mutex
)就利用了這一點(diǎn), 我們平時(shí)使用的Lock
方法一共就只有這幾行:
func (m *Mutex) Lock() { // Fast path: grab unlocked mutex. if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { if race.Enabled { race.Acquire(unsafe.Pointer(m)) } return } // Slow path (outlined so that the fast path can be inlined) m.lockSlow() }
注意倒數(shù)第三行的注釋:outlined so that the fast path can be inlined
,利用這一特性,Lock
里的FastPath就能被內(nèi)聯(lián)到我們的程序里而不需要額外的函數(shù)調(diào)用,從而提升代碼的性能。
函數(shù)內(nèi)聯(lián)部分的入口是函數(shù)inline.InlinePackage, 想要深入了解的小伙伴可以去看一看。
內(nèi)聯(lián)能為我的程序帶來多少性能上的提升?
前面介紹了這么多內(nèi)聯(lián),連標(biāo)準(zhǔn)庫都刻意使用內(nèi)聯(lián)來提升Go程序的性能,那么內(nèi)聯(lián)究竟能為我們帶來多少性能上的提升呢?
我們來擴(kuò)充一下文章開篇提到的例子:
package user import ( "errors" ) func ValidateName(name string) bool { if len(name) < 1 { return false } else if len(name) > 12 { return false } return true } //go:noinline func ValidateNameNoInline(name string) bool { if len(name) < 1 { return false } else if len(name) > 12 { return false } return true } func (s *Server) CreateUser(name string, password string) error { if !ValidateName(name) { return errors.New("invalid name") } return nil } // CreateUserNoInline 使用的是禁止內(nèi)聯(lián)版本的 ValidateName func (s *Server) CreateUserNoInline(name string, password string) error { if !ValidateNameNoInline(name) { return errors.New("invalid name") } return nil } type Server struct{}
我們復(fù)制了以ValidateName
函數(shù),在上面標(biāo)注上//go:noinline
來禁止編譯器對其進(jìn)行內(nèi)聯(lián)優(yōu)化,并將其并將其更名為ValidateNameNoInline
。同時(shí)我們也復(fù)制了CreateUser
方法,新的方法內(nèi)部使用ValidateNameNoInline
來驗(yàn)證name
參數(shù),除此之外所有的地方都和原方法相同。
我們來寫兩個(gè)Benchmark測試一下:
package user import "testing" // BenchmarkCreateUser 測試內(nèi)聯(lián)過的函數(shù)的性能 func BenchmarkCreateUser(b *testing.B) { srv := Server{} for i := 0; i < b.N; i++ { if err := srv.CreateUser("bootun", "123456"); err != nil { b.Logf("err: %v", err) } } } // BenchmarkValidateNameNoInline 測試函數(shù)禁止內(nèi)聯(lián)后的性能 func BenchmarkValidateNameNoInline(b *testing.B) { srv := Server{} for i := 0; i < b.N; i++ { if err := srv.CreateUserNoInline("bootun", "123456"); err != nil { b.Logf("err: %v", err) } } }
測試結(jié)果如下:
# 內(nèi)聯(lián)版本的基準(zhǔn)測試結(jié)果(BenchmarkCreateUser)
goos: windows
goarch: amd64
pkg: github.com/bootun/example/user
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkCreateUser
BenchmarkCreateUser-16 1000000000 0.2279 ns/op
PASS
# 禁止內(nèi)聯(lián)版本的基準(zhǔn)測試結(jié)果(BenchmarkValidateNameNoInline)
goos: windows
goarch: amd64
pkg: github.com/bootun/example/user
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkValidateNameNoInline
BenchmarkValidateNameNoInline-16 733243102 1.635 ns/op
PASS
可以看到,禁止內(nèi)聯(lián)后每次操作耗費(fèi)1.6納秒,而內(nèi)聯(lián)后只需要0.22納秒(因機(jī)器而異)。從比例上看,內(nèi)聯(lián)優(yōu)化帶來的收益還是很可觀的。
我需要做什么來啟用內(nèi)聯(lián)優(yōu)化嗎
當(dāng)然不需要,在Go編譯器中,內(nèi)聯(lián)優(yōu)化是默認(rèn)啟用的,如果你的函數(shù)符合文中提到的內(nèi)聯(lián)優(yōu)化的策略(比如函數(shù)很小),并且沒有顯式的禁用內(nèi)聯(lián),就可能會被編譯器執(zhí)行內(nèi)聯(lián)優(yōu)化。
在某些場景下,我們可能不希望函數(shù)進(jìn)行內(nèi)聯(lián)(比如使用dlv
進(jìn)行DEBUG時(shí),或者查看程序生成的匯編代碼時(shí)),可以使用go build -gcflags='-N -l' xxx.go
來禁用內(nèi)聯(lián)優(yōu)化。
編譯器默認(rèn)優(yōu)化出來的代碼可能比較難以閱讀和理解,不方便我們進(jìn)行調(diào)試和學(xué)習(xí)。
-gcflags
是傳遞給go編譯器gc
的命令行標(biāo)志, go build
背后做了很多事,也不止用到了gc
一個(gè)程序。使用go build -x main.go
可以查看編譯過程中的詳細(xì)步驟。
以上就是一文帶大家了解Go語言中的內(nèi)聯(lián)優(yōu)化的詳細(xì)內(nèi)容,更多關(guān)于Go內(nèi)聯(lián)優(yōu)化的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go?http請求排隊(duì)處理實(shí)戰(zhàn)示例
這篇文章主要為大家介紹了Go?http請求排隊(duì)處理實(shí)戰(zhàn)實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07通過Golang實(shí)現(xiàn)linux命令ls命令(命令行工具構(gòu)建)
這篇文章主要為大家詳細(xì)介紹了如何通過Golang實(shí)現(xiàn)一個(gè)linux命令ls命令(命令行工具構(gòu)建),文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的可以了解一下2023-01-01在Colaboratory上運(yùn)行Go程序的詳細(xì)過程
這篇文章主要介紹了在Colaboratory上運(yùn)行Go程序,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08深入理解Go語言設(shè)計(jì)模式之函數(shù)式選項(xiàng)模式
在 Go 語言中,函數(shù)選項(xiàng)模式(Function Options Pattern)是一種常見且強(qiáng)大的設(shè)計(jì)模式,用于構(gòu)建可擴(kuò)展、易于使用和靈活的 API,本文就來看看它的具體用法吧2023-05-05Golang記錄、計(jì)算函數(shù)執(zhí)行耗時(shí)、運(yùn)行時(shí)間的一個(gè)簡單方法
這篇文章主要介紹了Golang記錄、計(jì)算函數(shù)執(zhí)行耗時(shí)、運(yùn)行時(shí)間的一個(gè)簡單方法,本文直接給出代碼實(shí)例,需要的朋友可以參考下2015-07-07Golang基于內(nèi)存的鍵值存儲緩存庫go-cache
go-cache是一個(gè)內(nèi)存中的key:value store/cache庫,適用于單機(jī)應(yīng)用程序,本文主要介紹了Golang基于內(nèi)存的鍵值存儲緩存庫go-cache,具有一定的參考價(jià)值,感興趣的可以了解一下2025-03-03