一文帶大家了解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í),不會(huì)將函數(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, 用來檢測(cè)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)行嘗試,請(qǐng)不要?jiǎng)h除CreateUser方法上的//go:noinline,因?yàn)槔又械?code>CreateUser太簡(jiǎn)短了,編譯器會(huì)把它也內(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 // 增加棧指針, 減小??臻g
user.go:23 0x460325 RET // return
// ...觀察這一次的代碼可以發(fā)現(xiàn),ValidateName函數(shù)的邏輯直接被內(nèi)嵌到了CreateUser方法里展開了。我們?cè)谏傻膮R編代碼里也搜索不到ValidateName相關(guān)的符號(hào)了。 現(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ù)會(huì)被內(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,懶惰的類型檢查 會(huì)被內(nèi)聯(lián)
- 使用
-N -l來告訴編譯器禁止內(nèi)聯(lián) - 使用
-m啟用診斷輸出
也就是說,只要我們的函數(shù)/方法足夠小,就可能會(huì)被內(nèi)聯(lián)。 因此,很多人會(huì)使用許多小的函數(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來禁止編譯器對(duì)其進(jìn)行內(nèi)聯(lián)優(yōu)化,并將其并將其更名為ValidateNameNoInline。同時(shí)我們也復(fù)制了CreateUser方法,新的方法內(nèi)部使用ValidateNameNoInline來驗(yàn)證name參數(shù),除此之外所有的地方都和原方法相同。
我們來寫兩個(gè)Benchmark測(cè)試一下:
package user
import "testing"
// BenchmarkCreateUser 測(cè)試內(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 測(cè)試函數(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)
}
}
}測(cè)試結(jié)果如下:
# 內(nèi)聯(lián)版本的基準(zhǔn)測(cè)試結(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)測(cè)試結(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),就可能會(huì)被編譯器執(zhí)行內(nèi)聯(lián)優(yōu)化。
在某些場(chǎng)景下,我們可能不希望函數(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)化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
深入分析Go?實(shí)現(xiàn)?MySQL?數(shù)據(jù)庫事務(wù)
本文深入分析了Go語言實(shí)現(xiàn)MySQL數(shù)據(jù)庫事務(wù)的原理和實(shí)現(xiàn)方式,包括事務(wù)的ACID特性、事務(wù)的隔離級(jí)別、事務(wù)的實(shí)現(xiàn)方式等。同時(shí),本文還介紹了Go語言中的事務(wù)處理機(jī)制和相關(guān)的API函數(shù),以及如何使用Go語言實(shí)現(xiàn)MySQL數(shù)據(jù)庫事務(wù)。2023-06-06
淺析go語言如何實(shí)現(xiàn)協(xié)程的搶占式調(diào)度的
go語言通過GMP模型實(shí)現(xiàn)協(xié)程并發(fā),為了避免單協(xié)程持續(xù)持有線程導(dǎo)致線程隊(duì)列中的其他協(xié)程饑餓問題,設(shè)計(jì)者提出了一個(gè)搶占式調(diào)度機(jī)制,本文會(huì)基于一個(gè)簡(jiǎn)單的代碼示例對(duì)搶占式調(diào)度過程進(jìn)行深入講解剖析2024-04-04
Go?channel實(shí)現(xiàn)批量讀取數(shù)據(jù)
Go中的?channel?其實(shí)并沒有提供批量讀取數(shù)據(jù)的方法,需要我們自己實(shí)現(xiàn)一個(gè),使用本文就來為大家大家介紹一下如何通過Go?channel實(shí)現(xiàn)批量讀取數(shù)據(jù)吧2023-12-12
Golang設(shè)計(jì)模式中的橋接模式詳細(xì)講解
橋接模式是一種結(jié)構(gòu)型設(shè)計(jì)模式,通過橋接模式可以將抽象部分和它的實(shí)現(xiàn)部分分離,本文主要介紹了GoLang橋接模式,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2023-01-01
Go語言實(shí)現(xiàn)將中文轉(zhuǎn)化為拼音功能
這篇文章主要為大家詳細(xì)介紹了Go語言中如何實(shí)現(xiàn)將中文轉(zhuǎn)化為拼音功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-01-01

