Go編譯原理之函數(shù)內(nèi)聯(lián)
前言
在前一篇文章中分享了編譯器優(yōu)化的變量捕獲部分,本文分享編譯器優(yōu)化的另一個(gè)內(nèi)容—函數(shù)內(nèi)聯(lián)。函數(shù)內(nèi)聯(lián)是指將將較小的函數(shù)內(nèi)容,直接放入到調(diào)用者函數(shù)中,從而減少函數(shù)調(diào)用的開銷
函數(shù)內(nèi)聯(lián)概述
我們知道每一個(gè)高級(jí)編程語言的函數(shù)調(diào)用,成本都是在與需要為它分配棧內(nèi)存來存儲(chǔ)參數(shù)、返回值、局部變量等等,Go的函數(shù)調(diào)用的成本在于參數(shù)與返回值棧復(fù)制、較小的棧寄存器開銷以及函數(shù)序言部分的檢查棧擴(kuò)容(Go語言中的棧是可以動(dòng)態(tài)擴(kuò)容的,因?yàn)镚o在分配棧內(nèi)存不是逐漸增加的,而是一次性分配,這樣是為了避免訪問越界,它會(huì)一次性分配,當(dāng)檢查到分配的棧內(nèi)存不夠用時(shí),它會(huì)擴(kuò)容一個(gè)足夠大的??臻g,并將原來?xiàng)V械膬?nèi)容拷貝過來)
下邊寫一段代碼,通過Go的基準(zhǔn)測試來測一下函數(shù)內(nèi)聯(lián)帶來的效率提升
import "testing" //go:noinline //禁用內(nèi)聯(lián)。如果要開啟內(nèi)聯(lián),將該行注釋去掉即可 func max(a, b int) int { if a > b { return a } return b } var Result int func BenchmarkMax(b *testing.B) { var r int for i:=0; i< b.N; i++ { r = max(-1, i) } Result = r }
在編譯的過程中,Go的編譯器其實(shí)會(huì)計(jì)算函數(shù)內(nèi)聯(lián)花費(fèi)的成本,所以只有簡單的函數(shù),才會(huì)觸發(fā)函數(shù)內(nèi)聯(lián)。在后邊函數(shù)內(nèi)聯(lián)的源碼實(shí)現(xiàn)中,我們可以看到下邊這些情況不會(huì)被內(nèi)聯(lián):
- 遞歸函數(shù)
- 函數(shù)前有如下注釋的:
go:noinline
、go:norace
、go:nocheckptr
、go:uintptrescapes
等 - 沒有函數(shù)體
- 函數(shù)聲明的抽象語法樹中節(jié)點(diǎn)數(shù)大于5000(我的Go版本是1.16.6)(也就是函數(shù)內(nèi)部語句太多的情況,也不會(huì)被內(nèi)聯(lián))
- 函數(shù)中包含閉包(
OCLOSURE
)、range(ORANGE
)、select(OSELECT
)、go(OGO
)、defer(ODEFER
)、type(ODCLTYPE
)、返回值是函數(shù)(ORETJMP
)的,都不會(huì)內(nèi)聯(lián)
我們也可以構(gòu)建或編譯的時(shí)候,通過參數(shù)去控制它是否可以內(nèi)聯(lián)。如果希望程序中所有的函數(shù)都不執(zhí)行內(nèi)聯(lián)操作
go build -gcflags="-l" xxx.go go tool compile -l xxx.go
同樣我們?cè)诰幾g時(shí),也可以查看哪些函數(shù)內(nèi)聯(lián)了,哪些函數(shù)沒內(nèi)聯(lián),以及原因是什么
go tool compile -m=2 xxx.go
看一個(gè)例子
package main func test1(a, b int) int { return a+b } func step(n int) int { if n < 2 { return n } return step(n-1) + step(n-2) } func main() { test1(1, 2) step(5) }
可以看到test1這個(gè)函數(shù)是可以內(nèi)聯(lián)的,因?yàn)樗暮瘮?shù)體很簡單。step這個(gè)函數(shù)因?yàn)槭沁f歸函數(shù),所以它不會(huì)進(jìn)行內(nèi)聯(lián)
函數(shù)內(nèi)聯(lián)底層實(shí)現(xiàn)
這里邊其實(shí)每一個(gè)函數(shù)調(diào)用鏈都很深,我這里不會(huì)一行一行的解釋代碼的含義,僅僅會(huì)將一些核心的方法拿出來介紹一下,感興趣的小伙伴可以自己去調(diào)試一下(前邊有發(fā)相關(guān)文章)(Go源碼調(diào)試方法)
還是前邊提到多次的Go編譯入口文件,你可以在入口文件中找到這段代碼
Go編譯入口文件:src/cmd/compile/main.go -> gc.Main(archInit) // Phase 5: Inlining if Debug.l != 0 { // 查找可以內(nèi)聯(lián)的函數(shù) visitBottomUp(xtop, func(list []*Node, recursive bool) { numfns := numNonClosures(list) for _, n := range list { if !recursive || numfns > 1 { caninl(n) } else { ...... } inlcalls(n) } }) } for _, n := range xtop { if n.Op == ODCLFUNC { devirtualize(n) } }
下邊就看一下每個(gè)方法都在做哪些事情
visitBottomUp
該方法有兩個(gè)參數(shù):
xtop
:前邊已經(jīng)見過它了,它存放的是每個(gè)聲明語句的抽象語法樹的根節(jié)點(diǎn)數(shù)組- 第二個(gè)參數(shù)是一個(gè)函數(shù)(該函數(shù)也有兩個(gè)參數(shù),一個(gè)是滿足是函數(shù)類型聲明的抽象語法樹根節(jié)點(diǎn)數(shù)組,一個(gè)是bool值,true表示是遞歸函數(shù),false表示不是遞歸函數(shù))
進(jìn)入到visitBottomUp方法中,你會(huì)發(fā)現(xiàn)它主要是遍歷xtop,并對(duì)每個(gè)抽象語法樹的根節(jié)點(diǎn)調(diào)用了visit
這個(gè)方法(僅針對(duì)是函數(shù)類型聲明的抽象語法樹)
func visitBottomUp(list []*Node, analyze func(list []*Node, recursive bool)) { var v bottomUpVisitor v.analyze = analyze v.nodeID = make(map[*Node]uint32) for _, n := range list { if n.Op == ODCLFUNC && !n.Func.IsHiddenClosure() { //是函數(shù),并且不是閉包函數(shù) v.visit(n) } } }
而visit
方法的核心是調(diào)用了inspectList
方法,通過inspectList
對(duì)抽象語法樹按照深度優(yōu)先搜索進(jìn)行遍歷,并將每一個(gè)節(jié)點(diǎn)作為inspectList
方法的第二個(gè)參數(shù)(是一個(gè)函數(shù))的參數(shù),比如驗(yàn)證這個(gè)函數(shù)里邊是否有遞歸調(diào)用等(具體就是下邊的switch case)
func (v *bottomUpVisitor) visit(n *Node) uint32 { if id := v.nodeID[n]; id > 0 { // already visited return id } ...... v.stack = append(v.stack, n) inspectList(n.Nbody, func(n *Node) bool { switch n.Op { case ONAME: if n.Class() == PFUNC { ...... } case ODOTMETH: fn := asNode(n.Type.Nname()) ...... } case OCALLPART: fn := asNode(callpartMethod(n).Type.Nname()) ...... case OCLOSURE: if m := v.visit(n.Func.Closure); m < min { min = m } } return true }) v.analyze(block, recursive) } return min }
后邊通過調(diào)用visitBottomUp
的第二個(gè)參數(shù)傳遞的方法,對(duì)抽象語法樹進(jìn)行內(nèi)聯(lián)的判斷及內(nèi)聯(lián)操作,具體就是caninl
和inlcalls
這兩個(gè)方法
caninl
該方法的作用就是驗(yàn)證是函數(shù)類型聲明的抽象語法樹是否可以內(nèi)聯(lián)
這個(gè)方法的實(shí)現(xiàn)很簡單,首先是通過很多的if語句驗(yàn)證函數(shù)前邊是否有像go:noinline
等這種標(biāo)記
func caninl(fn *Node) { if fn.Op != ODCLFUNC { Fatalf("caninl %v", fn) } if fn.Func.Nname == nil { Fatalf("caninl no nname %+v", fn) } var reason string // reason, if any, that the function was not inlined ...... // If marked "go:noinline", don't inline if fn.Func.Pragma&Noinline != 0 { reason = "marked go:noinline" return } // If marked "go:norace" and -race compilation, don't inline. if flag_race && fn.Func.Pragma&Norace != 0 { reason = "marked go:norace with -race compilation" return } ...... // If fn has no body (is defined outside of Go), cannot inline it. if fn.Nbody.Len() == 0 { reason = "no function body" return } visitor := hairyVisitor{ budget: inlineMaxBudget, extraCallCost: cc, usedLocals: make(map[*Node]bool), } if visitor.visitList(fn.Nbody) { reason = visitor.reason return } if visitor.budget < 0 { reason = fmt.Sprintf("function too complex: cost %d exceeds budget %d", inlineMaxBudget-visitor.budget, inlineMaxBudget) return } n.Func.Inl = &Inline{ Cost: inlineMaxBudget - visitor.budget, Dcl: inlcopylist(pruneUnusedAutos(n.Name.Defn.Func.Dcl, &visitor)), Body: inlcopylist(fn.Nbody.Slice()), } ...... }
這里邊還有一個(gè)主要的方法就是visitList
,它是用來驗(yàn)證函數(shù)里邊是否有我們上邊提到的go、select、range等等這些語句。對(duì)于滿足內(nèi)聯(lián)條件的,它會(huì)將改寫該函數(shù)聲明抽閑語法樹的內(nèi)聯(lián)字段(Inl
)
inlcalls
該方法中就是具體的內(nèi)聯(lián)操作,比如將函數(shù)的參數(shù)和返回值轉(zhuǎn)換為調(diào)用者中的聲明語句等。里邊的調(diào)用和實(shí)現(xiàn)都比較復(fù)雜,這里不粘代碼了,大家可自行去看。函數(shù)內(nèi)聯(lián)的核心方法都在如下文件中
src/cmd/compile/internal/gc/inl.go
以上就是Go編譯原理之函數(shù)內(nèi)聯(lián)的詳細(xì)內(nèi)容,更多關(guān)于Go編譯原理函數(shù)內(nèi)聯(lián)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
在ubuntu下構(gòu)建go語言開發(fā)環(huán)境的方法
這篇文章主要介紹了在ubuntu下構(gòu)建go語言開發(fā)環(huán)境的方法,需要的朋友可以參考下2014-10-10Go語言對(duì)字符串進(jìn)行SHA1哈希運(yùn)算的方法
這篇文章主要介紹了Go語言對(duì)字符串進(jìn)行SHA1哈希運(yùn)算的方法,實(shí)例分析了Go語言針對(duì)字符串操作的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-03-03Golang多線程下載器實(shí)現(xiàn)高效快速地下載大文件
Golang多線程下載器是一種高效、快速地下載大文件的方法。Golang語言天生支持并發(fā)和多線程,可以輕松實(shí)現(xiàn)多線程下載器的開發(fā)。通過使用Golang的協(xié)程和通道,可以將下載任務(wù)分配到多個(gè)線程中并行處理,提高了下載的效率和速度2023-05-05