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