Go?內聯(lián)優(yōu)化讓程序員愛不釋手
前言:
這是一篇介紹 Go 編譯器如何實現(xiàn)內聯(lián)的文章,以及這種優(yōu)化將如何影響你的 Go 代碼。
什么是內聯(lián)?
內聯(lián)是將較小的函數(shù)合并到它們各自的調用者中的行為。其在不同的計算歷史時期的做法不一樣,如下:
- 早期:這種優(yōu)化通常是由手工完成的。
- 現(xiàn)在:內聯(lián)是在編譯過程中自動進行的一類基本優(yōu)化之一。
為什么內聯(lián)很重要?
內聯(lián)是很重要的,每一門語言都必然會有。
具體的原因如下:
- 它消除了函數(shù)調用本身的開銷。
- 它允許編譯器更有效地應用其他優(yōu)化策略。
核心來講,就是性能更好了。
函數(shù)調用的開銷
基本知識
在任何語言中調用一個函數(shù)都是有代價的。將參數(shù)編入寄存器或堆棧(取決于ABI),并在返回時反轉這一過程,這些都是開銷。
調用一個函數(shù)需要將程序計數(shù)器從指令流中的一個點跳到另一個點,這可能會導致流水線停滯。一旦進入函數(shù),通常需要一些前言來為函數(shù)的執(zhí)行準備一個新的堆??蚣?,在返回調用者之前,還需要一個類似的尾聲來退掉這個框架。
Go 中的開銷
在 Go 中,一個函數(shù)的調用需要額外的成本來支持動態(tài)堆棧的增長。在進入時,goroutine 可用的堆棧空間的數(shù)量與函數(shù)所需的數(shù)量進行比較。
如果可用的堆棧空間不足,序言就會跳轉到運行時邏輯,通過將堆棧復制到一個新的、更大的位置來增加堆棧。
一旦這樣做了,運行時就會跳回到原始函數(shù)的起點,再次進行堆棧檢查,現(xiàn)在通過了,然后繼續(xù)調用。通過這種方式,goroutines可以從一個小的堆棧分配開始,只有在需要時才會增加。
這種檢查很便宜,只需要幾條指令,而且由于goroutine的堆棧以幾何級數(shù)增長,檢查很少失敗。因此,現(xiàn)代處理器中的分支預測單元可以通過假設堆棧檢查總是成功來隱藏堆棧檢查的成本。在處理器錯誤預測堆棧檢查并不得不丟棄它在投機執(zhí)行時所做的工作的情況下,與運行時增長goroutine堆棧所需的工作成本相比,管道停滯的成本相對較小。
Go 里的優(yōu)化
雖然每個函數(shù)調用的通用組件和 Go 特定組件的開銷被使用投機執(zhí)行技術的現(xiàn)代處理器很好地優(yōu)化了,但這些開銷不能完全消除,因此每個函數(shù)調用都帶有性能成本,超過了執(zhí)行有用工作的時間。由于函數(shù)調用的開銷是固定的,較小的函數(shù)相對于較大的函數(shù)要付出更大的代價,因為它們每次調用的有用工作往往較少。
因此,消除這些開銷的解決方案必須是消除函數(shù)調用本身,Go 編譯器在某些條件下通過用函數(shù)的內容替換對函數(shù)的調用來做到這一點。這被稱為內聯(lián),因為它使函數(shù)的主體與它的調用者保持一致。
改善優(yōu)化的機會
Cliff Click 博士將內聯(lián)描述為現(xiàn)代編譯器進行的優(yōu)化,因為它是常量傳播和死代碼消除等優(yōu)化的基礎。
實際上,內聯(lián)允許編譯器看得更遠,允許它在特定函數(shù)被調用的情況下,觀察到可以進一步簡化或完全消除的邏輯。
由于內聯(lián)可以遞歸應用,優(yōu)化決策不僅可以在每個單獨的函數(shù)的上下文中做出,還可以應用于調用路徑中的函數(shù)鏈。
進行內聯(lián)優(yōu)化
不允許內聯(lián)
內聯(lián)的效果可以通過這個小例子來證明:
package main import "testing" //go:noinline 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 test -bench=.
BenchmarkMax-4 530687617 2.24 ns/op
從執(zhí)行結果來看,max(-1, i)
的成本大約是 2.24ns,感覺性能不錯。
允許內聯(lián)
現(xiàn)在讓我們去掉 //go:noinline pragma
的語句,再看看不允許內聯(lián)的情況下,性能是否會改變。
如下結果:
% go test -bench=.
BenchmarkMax-4 1000000000 0.514 ns/op
兩個結果對比一看,2.24ns 和 0.51ns。差距至少一倍以上,根據(jù) benchstat 的建議,內聯(lián)情況下,性能提高了 78%。
如下結果:
% benchstat {old,new}.txt
name old time/op new time/op delta
Max-4 2.21ns ± 1% 0.49ns ± 6% -77.96% (p=0.000 n=18+19)
這些改進從何而來?
首先,取消函數(shù)調用和相關的前導動作是主要的改進貢獻者。其將 max 函數(shù)的內容拉到它的調用者中,減少了處理器執(zhí)行的指令數(shù)量,并消除了幾個分支。
現(xiàn)在 max 函數(shù)的內容對編譯器來說是可見的,當它優(yōu)化 BenchmarkMax 時,它可以做一些額外的改進。
考慮到一旦 max 被內聯(lián),BenchmarkMax 的主體對編譯器而言就會有所改變,與用戶端看到的并不一樣。
如下代碼:
func BenchmarkMax(b *testing.B) { var r int for i := 0; i < b.N; i++ { if -1 > i { r = -1 } else { r = i } } Result = r }
再次運行基準測試,我們看到我們手動內聯(lián)的版本與編譯器內聯(lián)的版本表現(xiàn)一樣好。
如下結果:
% benchstat {old,new}.txt
name old time/op new time/op delta
Max-4 2.21ns ± 1% 0.48ns ± 3% -78.14% (p=0.000 n=18+18)
現(xiàn)在,編譯器可以獲得 max 內聯(lián)到 BenchmarkMax 的結果,它可以應用以前不可能的優(yōu)化方法。
例如:編譯器注意到 i 被初始化為 0,并且只被遞增,所以任何與 i 的比較都可以假定 i 永遠不會是負數(shù)。因此,條件 -1 > i
將永遠不會為真。
在證明了 -1 > i
永遠不會為真之后,編譯器可以將代碼簡化為:
func BenchmarkMax(b *testing.B) { var r int for i := 0; i < b.N; i++ { if false { // 注意已為 false r = -1 } else { r = i } } Result = r }
并且由于該分支現(xiàn)在是一個常數(shù),編譯器可以消除無法到達的路徑,只留下如下代碼:
func BenchmarkMax(b *testing.B) { var r int for i := 0; i < b.N; i++ { r = i } Result = r }
通過內聯(lián)和它所釋放的優(yōu)化,編譯器已經(jīng)將表達式 r = max(-1, i)
簡化為 r = i
。
這個例子非常不錯,很好的體現(xiàn)了內聯(lián)的優(yōu)化過程和性能提升的緣由。
內聯(lián)的限制
在這篇文章中,討論了所謂的葉子內聯(lián):將調用棧底部的一個函數(shù)內聯(lián)到其直接調用者中的行為。
內聯(lián)是一個遞歸的過程,一旦一個函數(shù)被內聯(lián)到它的調用者中,編譯器就可能將產(chǎn)生的代碼內聯(lián)到它的調用者中,依此類推。
例如如下代碼:
func BenchmarkMaxMaxMax(b *testing.B) { var r int for i := 0; i < b.N; i++ { r = max(max(-1, i), max(0, i)) } Result = r }
該運行速度將會和前面的例子一樣快,因為編譯器能夠反復應用上面的優(yōu)化,將代碼減少到相同的 r = i
表達式。
總結
這篇文章針對內聯(lián)進行了基本的概念介紹和分析,并且通過 Go 的例子進行了一步步的剖析,讓大家對真實案例有了一個更貼切的理解。
Go 編譯器的優(yōu)化總是無處不在的。
到此這篇關于Go 內聯(lián)優(yōu)化讓程序員愛不釋手的文章就介紹到這了,更多相關Go 內聯(lián)優(yōu)化內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Golang監(jiān)聽日志文件并發(fā)送到kafka中
這篇文章主要介紹了Golang監(jiān)聽日志文件并發(fā)送到kafka中,日志收集項目的準備中,本文主要講的是利用golang的tail庫,監(jiān)聽日志文件的變動,將日志信息發(fā)送到kafka中?,需要的朋友可以參考一下2022-04-04