開(kāi)篇
今天我們來(lái)聊聊 Golang 中的內(nèi)聯(lián)。
我們知道,函數(shù)調(diào)用本身是存在成本的。如果把一個(gè)實(shí)際調(diào)用的函數(shù)產(chǎn)生的指令,直接插入到的位置,來(lái)替換對(duì)應(yīng)的函數(shù)調(diào)用指令。就可以消除掉這部分性能損耗。但同時(shí)也要注意,我們需要維護(hù)各個(gè)模塊的可讀性,需要保證高內(nèi)聚,低耦合,不可能把所有邏輯合到一個(gè)函數(shù),這樣可讀性大大降低。
那么,既然在代碼層面做不太好,還有沒(méi)有別的招呢?
內(nèi)聯(lián)就是來(lái)做這件事的。下面我們一起來(lái)看一下。
內(nèi)聯(lián)
所謂內(nèi)聯(lián),指的是編譯期間,直接將調(diào)用函數(shù)的地方替換為函數(shù)的實(shí)現(xiàn),它可以減少函數(shù)調(diào)用的開(kāi)銷(xiāo)以提高程序的性能。內(nèi)聯(lián)函數(shù)是直接復(fù)制“鑲嵌”到主函數(shù)中去的,就是將內(nèi)聯(lián)函數(shù)的代碼直接放在內(nèi)聯(lián)函數(shù)的位置上,
這與一般函數(shù)不同,主函數(shù)在調(diào)用一般函數(shù)的時(shí)候,是指令跳轉(zhuǎn)到被調(diào)用函數(shù)的入口地址,執(zhí)行完被調(diào)用函數(shù)后,指令再跳轉(zhuǎn)回主函數(shù)上繼續(xù)執(zhí)行后面的代碼;而由于內(nèi)聯(lián)函數(shù)是將函數(shù)的代碼直接放在了函數(shù)的位置上,所以沒(méi)有指令跳轉(zhuǎn),指令按順序執(zhí)行。Go程序編譯時(shí),默認(rèn)將進(jìn)行內(nèi)聯(lián)優(yōu)化。
當(dāng)然,內(nèi)聯(lián)也并不是沒(méi)有代價(jià),這本質(zhì)是一種以空間換時(shí)間的優(yōu)化方法,其帶來(lái)的優(yōu)點(diǎn)是使CPU需要執(zhí)行的指令數(shù)變少了,不需要根據(jù)地址跳轉(zhuǎn)的過(guò)程了,不用壓棧和出棧的過(guò)程了,我們把可以復(fù)用的程序指令在調(diào)用它的地方完全展開(kāi)了。如果一個(gè)函數(shù)在很多地方都被調(diào)用了,那么就會(huì)展開(kāi)很多次,整個(gè)程序占用的空間就會(huì)變大了。
需要注意,內(nèi)聯(lián)也是有門(mén)檻的,并不是隨便一個(gè)函數(shù)調(diào)用都可以原地替換。Golang 編譯器內(nèi)部會(huì)有一套自己的判斷規(guī)則,判斷一次函數(shù)調(diào)用能否被內(nèi)聯(lián),后面的章節(jié)我們會(huì)提到。這也是為什么我們會(huì)說(shuō):
Inlining is the act of combining smaller functions into their respective callers.
這個(gè) small 的程度很關(guān)鍵。
簡(jiǎn)單小結(jié)一下,內(nèi)聯(lián)帶來(lái)的好處有兩個(gè):
解除函數(shù)調(diào)用的開(kāi)銷(xiāo),以空間換時(shí)間;
支持編譯器更有效地應(yīng)用其他優(yōu)化策略。
函數(shù)調(diào)用開(kāi)銷(xiāo)
一個(gè)goroutine會(huì)有一個(gè)單獨(dú)的棧,棧又會(huì)包含多個(gè)棧幀,棧幀是函數(shù)調(diào)用時(shí)在棧上為函數(shù)所分配的區(qū)域。函數(shù)調(diào)用存在一些固定開(kāi)銷(xiāo):
創(chuàng)建棧幀;
讀寫(xiě)寄存器;
棧溢出檢測(cè)。
內(nèi)聯(lián)什么時(shí)候最有效
函數(shù)執(zhí)行的開(kāi)銷(xiāo) vs 函數(shù)調(diào)用的開(kāi)銷(xiāo)。這兩個(gè)開(kāi)銷(xiāo)的比值會(huì)很大程度上決定【內(nèi)聯(lián)】的效果。
內(nèi)聯(lián)其實(shí)就是把函數(shù)調(diào)用這份固定開(kāi)銷(xiāo)給消除了,所以尤其對(duì)于函數(shù)體極其簡(jiǎn)單的函數(shù)有效果。如果你的函數(shù)執(zhí)行了一系列復(fù)雜邏輯,開(kāi)銷(xiāo)遠(yuǎn)超【函數(shù)調(diào)用】本身,這里的優(yōu)化就微不足道了。
內(nèi)聯(lián)雖然可以減少函數(shù)調(diào)用的開(kāi)銷(xiāo),但是也可能因?yàn)榇嬖谥貜?fù)代碼,從而導(dǎo)致 CPU 緩存命中率降低,所以并不能盲目追求過(guò)度的內(nèi)聯(lián),需要結(jié)合 profile 結(jié)果來(lái)具體分析。
Golang 編譯器對(duì)內(nèi)聯(lián)的要求
參考官方 wiki:github.com/golang/go/w…[1]
想要內(nèi)聯(lián),方法本身必須滿足以下條件:
函數(shù)足夠簡(jiǎn)單,當(dāng)解析AST時(shí),Go申請(qǐng)了80個(gè)節(jié)點(diǎn)作為內(nèi)聯(lián)的預(yù)算。每個(gè)節(jié)點(diǎn)都會(huì)消耗一個(gè)預(yù)算。函數(shù)的開(kāi)銷(xiāo)不能超過(guò)這個(gè)預(yù)算;
不能包含閉包,defer,recover,select;
不能以 go:noinline 或 go:unitptrescapes 開(kāi)頭;
必須有函數(shù)體;
其他等復(fù)雜要求,詳細(xì)可見(jiàn)src/cmd/compile/internal/gc/inl.go相關(guān)內(nèi)容。我們可以使用 gcflags 參數(shù)來(lái)判斷能不能內(nèi)聯(lián)。
內(nèi)聯(lián)的實(shí)現(xiàn)原理建議大家看看這篇文章:gocompiler.shizhz.me/8.-golang-b…[2]
如何禁止內(nèi)聯(lián)
單個(gè)函數(shù)級(jí)別:在函數(shù)定義前一行添加//go:noinline;
全局編譯級(jí)別:可通過(guò)-gcflags="-l"選項(xiàng)全局禁用內(nèi)聯(lián),與一個(gè)-l禁用內(nèi)聯(lián)相反,如果傳遞兩個(gè)或兩個(gè)以上的-l則會(huì)打開(kāi)內(nèi)聯(lián),并啟用更激進(jìn)的內(nèi)聯(lián)策略。
gcflags
go build 時(shí)可以使用 -gcflags 指定編譯選項(xiàng),-gcflags 參數(shù)的格式是:
-gcflags="pattern=arg list"
pattern 是選擇包的模式,arg list 是空格分割的編譯選項(xiàng),如果編譯選項(xiàng)中含有空格,可以使用引號(hào)包起來(lái)。
如:-gcflags="all=-N -l" 代表的是表示主模塊和它所有的依賴都禁用【編譯器優(yōu)化】和【內(nèi)聯(lián)】。更多編譯選項(xiàng)參照 go tool compile --help
Use -gcflags -m to observe the result of escape analysis and inlining decisions for the gc toolchain.
使用 go build 編譯時(shí),我們可以使用參數(shù)-gflags="-m"運(yùn)行,可顯示被內(nèi)聯(lián)的函數(shù),使用運(yùn)行參數(shù)-gflags="-m -m"可以看到原因。類(lèi)似:
./main.go:14:6:cannotinlinexxx:unhandledopXXX /ins.go:9:6:cannotinlinexxx:functiontoocomplex:cost104exceedsbudget80
我們可以用下面的命令分析變量是否逃逸:
gorun-gcflags'-m-l'main.go
-m 其實(shí)是打印優(yōu)化策略的語(yǔ)義,實(shí)際上最多總共可以用 4 個(gè) -m,但是信息量較大,一般用 1 個(gè)就可以了;
-l 會(huì)禁用函數(shù)內(nèi)聯(lián),在這里禁用掉內(nèi)聯(lián)能更好的觀察逃逸情況,減少干擾
內(nèi)聯(lián)后堆棧信息還對(duì)不對(duì)
內(nèi)聯(lián)會(huì)將函數(shù)調(diào)用的過(guò)程抹掉,這會(huì)引入一個(gè)新的問(wèn)題:代碼的堆棧信息還能否保證。其實(shí)這一點(diǎn)不用擔(dān)心,Golang 內(nèi)部會(huì)為每個(gè)存在內(nèi)聯(lián)優(yōu)化的 goroutine 維持一個(gè)內(nèi)聯(lián)樹(shù)(inlining tree),該樹(shù)可通過(guò) -gcflags="-d pctab=pctoinline" 命令查看,Go在生成的代碼中映射了內(nèi)聯(lián)函數(shù)。并且,也映射了行號(hào)。這張表被嵌入到了二進(jìn)制文件中,所以在運(yùn)行時(shí)可以得到準(zhǔn)確的堆棧信息。
審核編輯:湯梓紅
-
寄存器
+關(guān)注
關(guān)注
31文章
5397瀏覽量
122653 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4363瀏覽量
63770 -
編譯器
+關(guān)注
關(guān)注
1文章
1651瀏覽量
49711
原文標(biāo)題:初探 Golang 內(nèi)聯(lián)
文章出處:【微信號(hào):LinuxHub,微信公眾號(hào):Linux愛(ài)好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
Golang接口的作用和應(yīng)用場(chǎng)景
基于SUIF的函數(shù)內(nèi)聯(lián)技術(shù)
C++如何處理內(nèi)聯(lián)虛函數(shù)
內(nèi)聯(lián)函數(shù)詳解
內(nèi)聯(lián)函數(shù)和外聯(lián)函數(shù)有什么區(qū)別

使用golang channel的諸多特性和技巧

評(píng)論