内联函数和编译器对Go代码的优化
什么是內聯函數
圖片版權:Renee French.在很多講 Go 語言底層的技術資料和博客里都會提到內聯函數這個名詞,也有人把內聯函數說成代碼內聯、函數展開、展開函數等等,其實想表達的都是 Go 語言編譯器對函數調用的優化,編譯器會把一些函數的調用直接替換成被調函數的函數體內的代碼在調用處展開,以減少函數調用帶來的時間消耗。它是Go語言編譯器對代碼進行優化的一個常用手段。
內聯函數并不是 Go 語言編譯器獨有的,很多語言的編譯器在編譯代碼時都會做內聯函數優化,維基百科對內聯函數的解釋如下 (我把重點需要關注的信息特意進行了加粗):
在計算機科學中,內聯函數(有時稱作在線函數或編譯時期展開函數)是一種編程語言結構,用來建議編譯器對一些特殊函數進行內聯擴展(有時稱作在線擴展);也就是說建議編譯器將指定的函數體插入并取代每一處調用該函數的地方(上下文),從而節省了每次調用函數帶來的額外時間開支。但在選擇使用內聯函數時,必須在程序占用空間和程序執行效率之間進行權衡,因為過多的比較復雜的函數進行內聯擴展將帶來很大的存儲資源開支。另外還需要特別注意的是對遞歸函數的內聯擴展可能引起部分編譯器的無窮編譯。
Note:內聯優化一般用于能夠快速執行的函數,因為在這種情況下函數調用的時間消耗顯得更為突出,同時內聯體量小的函數也不會明顯增加編譯后的執行文件占用的空間。
Go 語言里的內聯函數
舉個例子來說,假設我有下面這樣一個算兩數之和的程序(不要緊張,不是算法題-兩數之和...)
package?main import?"fmt" func?main()?{x?:=?20y?:=?5res?:=?add(x,?y)fmt.Println(res) }func?add(x?int,?y?int)?int?{return?x?+?y }上面的函數非常簡單,add 函數對兩個參數進行加和,編譯器在編譯上面的 Go 代碼時會做內聯優化,把 add 函數的函數體直接在調用處展開,等價于上面的 Go 代碼是這么編寫的。
package?main import?"fmt" func?main()?{x?:=?20y?:=?5//?內聯函數,?或者叫把函數展開在調用處res?:=?x?+?y?fmt.Println(res) }func?add(x?int,?y?int)?int?{return?x?+?y }告訴編譯器不對函數進行內聯
在源碼編譯的匯編代碼里我們也看不到對 add 函數的調用,不過我們可以通過在 add 函數上增加一個特殊的注釋來告訴編譯器不要對它進行內聯優化
//?注意下面這行注釋,"//"后面不要有空格 //go:noinline func?add(x?int,?y?int)?int?{return?x?+?y }怎么驗證這個注釋真實有效,能讓編譯器不對add函數做內聯優化呢?我們可以用 go tool compile -S scratch.go 打印出的 Go 代碼被編譯成的匯編代碼,在匯編代碼里我們可以發現對add函數的調用。
0x0053 00083 (scratch.go:6) CALL "".add(SB)這也反向證明了,正常情況下 Go 編譯器會對 add 函數進行內聯優化。
編譯器會對代碼做哪些優化
除了分析編譯后的匯編源碼外,我們還可以通過給 go build 命令傳遞 ?-gcflags -m 參數
$?go?build?-gcflags?--help [.......] //?傳遞?-m?選項會輸出編譯器對代碼的優化 -m????print?optimization?decisions讓編譯器告訴我們它在編譯 Go 代碼對代碼都做了哪些優化。
接下用 -gcflags -m 構建一下我們的小程序
$?go?build?-gcflags?-m?scratch.go./scratch_16.go:10:6:?can?inline?add ./scratch_16.go:6:12:?inlining?call?to?add ./scratch_16.go:7:13:?inlining?call?to?fmt.Println ./scratch_16.go:7:13:?res?escapes?to?heap ./scratch_16.go:7:13:?main?[]interface?{}?literal?does?not?escape ./scratch_16.go:7:13:?io.Writer(os.Stdout)?escapes?to?heap通過終端的輸出可以了解到,編譯器判斷 add函數可以進行內聯,也對 add 函數進行了內聯,除此之外還對fmt.Println 進行了內聯優化。還有一個 io.Writer(os.Stdout) escapes to heap 的輸出代表的是 io 對象逃逸到了堆上,堆逃逸是另外一種優化,在先前 Go內存管理系列的文章 -- Go內存管理之代碼的逃逸分析 有詳細說過。
哪些函數不會被內聯
那么 Go 的編譯器是不是會對所有的體量小,執行快的函數都會進行內聯優化呢?我查查了資料發現 Go 在決策是否要對函數進行內聯時有一個標準:
函數體內包含:閉包調用,select ,for ,defer,go 關鍵字的的函數不會進行內聯。并且除了這些,還有其它的限制。當解析AST時,Go申請了80個節點作為內聯的預算。每個節點都會消耗一個預算。比如,a = a + 1這行代碼包含了5個節點:AS, NAME, ADD, NAME, LITERAL。以下是對應的SSA 輸出:
當一個函數的開銷超過了這個預算,就無法內聯。
以上描述翻譯自文章:https://medium.com/a-journey-with-go/go-inlining-strategy-limitation-6b6d7fc3b1be
總結
內聯是高性能編程的一種重要手段。每個函數調用都有開銷:創建棧幀,讀寫寄存器,這些開銷可以通過內聯避免,對性能的提升大概在5~6%左右。但內聯對函數體進行拷貝也會增大編譯后二進制文件的大小,不過好在使用Go語言編程時,編譯器會幫助我們決策哪些函數可以內聯,大大降低了使用者的心智負擔 。
關于編譯器編譯時對Go代碼做的優化,推薦閱讀我的另一篇文章:
Go內存管理之代碼的逃逸分析
今天的文章就到這里啦,如果喜歡我的文章就幫我點個贊吧,我會每周通過技術文章分享我的所學所見和第一手實踐經驗,感謝你的支持。微信搜索關注公眾號「網管叨bi叨」每周教會你一個進階知識,還有專門寫給開發工程師的Kubernetes入門教程。
- END -
關注公眾號,獲取更多精選技術原創文章
總結
以上是生活随笔為你收集整理的内联函数和编译器对Go代码的优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 推荐几个硬核的号主
- 下一篇: 几个常见的 slice 错误