Go?內(nèi)聯(lián)優(yōu)化讓程序員愛不釋手
前言:
這是一篇介紹 Go 編譯器如何實(shí)現(xiàn)內(nèi)聯(lián)的文章,以及這種優(yōu)化將如何影響你的 Go 代碼。
什么是內(nèi)聯(lián)?
內(nèi)聯(lián)是將較小的函數(shù)合并到它們各自的調(diào)用者中的行為。其在不同的計(jì)算歷史時(shí)期的做法不一樣,如下:
- 早期:這種優(yōu)化通常是由手工完成的。
- 現(xiàn)在:內(nèi)聯(lián)是在編譯過程中自動(dòng)進(jìn)行的一類基本優(yōu)化之一。
為什么內(nèi)聯(lián)很重要?
內(nèi)聯(lián)是很重要的,每一門語言都必然會(huì)有。
具體的原因如下:
- 它消除了函數(shù)調(diào)用本身的開銷。
- 它允許編譯器更有效地應(yīng)用其他優(yōu)化策略。
核心來講,就是性能更好了。
函數(shù)調(diào)用的開銷
基本知識(shí)
在任何語言中調(diào)用一個(gè)函數(shù)都是有代價(jià)的。將參數(shù)編入寄存器或堆棧(取決于ABI),并在返回時(shí)反轉(zhuǎn)這一過程,這些都是開銷。
調(diào)用一個(gè)函數(shù)需要將程序計(jì)數(shù)器從指令流中的一個(gè)點(diǎn)跳到另一個(gè)點(diǎn),這可能會(huì)導(dǎo)致流水線停滯。一旦進(jìn)入函數(shù),通常需要一些前言來為函數(shù)的執(zhí)行準(zhǔn)備一個(gè)新的堆棧框架,在返回調(diào)用者之前,還需要一個(gè)類似的尾聲來退掉這個(gè)框架。
Go 中的開銷
在 Go 中,一個(gè)函數(shù)的調(diào)用需要額外的成本來支持動(dòng)態(tài)堆棧的增長(zhǎng)。在進(jìn)入時(shí),goroutine 可用的堆??臻g的數(shù)量與函數(shù)所需的數(shù)量進(jìn)行比較。
如果可用的堆棧空間不足,序言就會(huì)跳轉(zhuǎn)到運(yùn)行時(shí)邏輯,通過將堆棧復(fù)制到一個(gè)新的、更大的位置來增加堆棧。
一旦這樣做了,運(yùn)行時(shí)就會(huì)跳回到原始函數(shù)的起點(diǎn),再次進(jìn)行堆棧檢查,現(xiàn)在通過了,然后繼續(xù)調(diào)用。通過這種方式,goroutines可以從一個(gè)小的堆棧分配開始,只有在需要時(shí)才會(huì)增加。
這種檢查很便宜,只需要幾條指令,而且由于goroutine的堆棧以幾何級(jí)數(shù)增長(zhǎng),檢查很少失敗。因此,現(xiàn)代處理器中的分支預(yù)測(cè)單元可以通過假設(shè)堆棧檢查總是成功來隱藏堆棧檢查的成本。在處理器錯(cuò)誤預(yù)測(cè)堆棧檢查并不得不丟棄它在投機(jī)執(zhí)行時(shí)所做的工作的情況下,與運(yùn)行時(shí)增長(zhǎng)goroutine堆棧所需的工作成本相比,管道停滯的成本相對(duì)較小。
Go 里的優(yōu)化
雖然每個(gè)函數(shù)調(diào)用的通用組件和 Go 特定組件的開銷被使用投機(jī)執(zhí)行技術(shù)的現(xiàn)代處理器很好地優(yōu)化了,但這些開銷不能完全消除,因此每個(gè)函數(shù)調(diào)用都帶有性能成本,超過了執(zhí)行有用工作的時(shí)間。由于函數(shù)調(diào)用的開銷是固定的,較小的函數(shù)相對(duì)于較大的函數(shù)要付出更大的代價(jià),因?yàn)樗鼈兠看握{(diào)用的有用工作往往較少。
因此,消除這些開銷的解決方案必須是消除函數(shù)調(diào)用本身,Go 編譯器在某些條件下通過用函數(shù)的內(nèi)容替換對(duì)函數(shù)的調(diào)用來做到這一點(diǎn)。這被稱為內(nèi)聯(lián),因?yàn)樗购瘮?shù)的主體與它的調(diào)用者保持一致。
改善優(yōu)化的機(jī)會(huì)
Cliff Click 博士將內(nèi)聯(lián)描述為現(xiàn)代編譯器進(jìn)行的優(yōu)化,因?yàn)樗浅A總鞑ズ退来a消除等優(yōu)化的基礎(chǔ)。
實(shí)際上,內(nèi)聯(lián)允許編譯器看得更遠(yuǎn),允許它在特定函數(shù)被調(diào)用的情況下,觀察到可以進(jìn)一步簡(jiǎn)化或完全消除的邏輯。
由于內(nèi)聯(lián)可以遞歸應(yīng)用,優(yōu)化決策不僅可以在每個(gè)單獨(dú)的函數(shù)的上下文中做出,還可以應(yīng)用于調(diào)用路徑中的函數(shù)鏈。
進(jìn)行內(nèi)聯(lián)優(yōu)化
不允許內(nèi)聯(lián)
內(nèi)聯(lián)的效果可以通過這個(gè)小例子來證明:
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
}運(yùn)行這個(gè)基準(zhǔn)可以得到以下結(jié)果:
% go test -bench=.
BenchmarkMax-4 530687617 2.24 ns/op
從執(zhí)行結(jié)果來看,max(-1, i)的成本大約是 2.24ns,感覺性能不錯(cuò)。
允許內(nèi)聯(lián)
現(xiàn)在讓我們?nèi)サ?nbsp;//go:noinline pragma 的語句,再看看不允許內(nèi)聯(lián)的情況下,性能是否會(huì)改變。
如下結(jié)果:
% go test -bench=.
BenchmarkMax-4 1000000000 0.514 ns/op
兩個(gè)結(jié)果對(duì)比一看,2.24ns 和 0.51ns。差距至少一倍以上,根據(jù) benchstat 的建議,內(nèi)聯(lián)情況下,性能提高了 78%。
如下結(jié)果:
% 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)
這些改進(jìn)從何而來?
首先,取消函數(shù)調(diào)用和相關(guān)的前導(dǎo)動(dòng)作是主要的改進(jìn)貢獻(xiàn)者。其將 max 函數(shù)的內(nèi)容拉到它的調(diào)用者中,減少了處理器執(zhí)行的指令數(shù)量,并消除了幾個(gè)分支。
現(xiàn)在 max 函數(shù)的內(nèi)容對(duì)編譯器來說是可見的,當(dāng)它優(yōu)化 BenchmarkMax 時(shí),它可以做一些額外的改進(jìn)。
考慮到一旦 max 被內(nèi)聯(lián),BenchmarkMax 的主體對(duì)編譯器而言就會(huì)有所改變,與用戶端看到的并不一樣。
如下代碼:
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
}再次運(yùn)行基準(zhǔn)測(cè)試,我們看到我們手動(dòng)內(nèi)聯(lián)的版本與編譯器內(nèi)聯(lián)的版本表現(xiàn)一樣好。
如下結(jié)果:
% 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 內(nèi)聯(lián)到 BenchmarkMax 的結(jié)果,它可以應(yīng)用以前不可能的優(yōu)化方法。
例如:編譯器注意到 i 被初始化為 0,并且只被遞增,所以任何與 i 的比較都可以假定 i 永遠(yuǎn)不會(huì)是負(fù)數(shù)。因此,條件 -1 > i 將永遠(yuǎn)不會(huì)為真。
在證明了 -1 > i 永遠(yuǎn)不會(huì)為真之后,編譯器可以將代碼簡(jiǎn)化為:
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)在是一個(gè)常數(shù),編譯器可以消除無法到達(dá)的路徑,只留下如下代碼:
func BenchmarkMax(b *testing.B) {
var r int
for i := 0; i < b.N; i++ {
r = i
}
Result = r
}通過內(nèi)聯(lián)和它所釋放的優(yōu)化,編譯器已經(jīng)將表達(dá)式 r = max(-1, i) 簡(jiǎn)化為 r = i。
這個(gè)例子非常不錯(cuò),很好的體現(xiàn)了內(nèi)聯(lián)的優(yōu)化過程和性能提升的緣由。
內(nèi)聯(lián)的限制
在這篇文章中,討論了所謂的葉子內(nèi)聯(lián):將調(diào)用棧底部的一個(gè)函數(shù)內(nèi)聯(lián)到其直接調(diào)用者中的行為。
內(nèi)聯(lián)是一個(gè)遞歸的過程,一旦一個(gè)函數(shù)被內(nèi)聯(lián)到它的調(diào)用者中,編譯器就可能將產(chǎn)生的代碼內(nèi)聯(lián)到它的調(diào)用者中,依此類推。
例如如下代碼:
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ùn)行速度將會(huì)和前面的例子一樣快,因?yàn)榫幾g器能夠反復(fù)應(yīng)用上面的優(yōu)化,將代碼減少到相同的 r = i 表達(dá)式。
總結(jié)
這篇文章針對(duì)內(nèi)聯(lián)進(jìn)行了基本的概念介紹和分析,并且通過 Go 的例子進(jìn)行了一步步的剖析,讓大家對(duì)真實(shí)案例有了一個(gè)更貼切的理解。
Go 編譯器的優(yōu)化總是無處不在的。
到此這篇關(guān)于Go 內(nèi)聯(lián)優(yōu)化讓程序員愛不釋手的文章就介紹到這了,更多相關(guān)Go 內(nèi)聯(lián)優(yōu)化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言學(xué)習(xí)教程之結(jié)構(gòu)體的示例詳解
結(jié)構(gòu)體是一個(gè)序列,包含一些被命名的元素,這些被命名的元素稱為字段(field),每個(gè)字段有一個(gè)名字和一個(gè)類型。本文通過一些示例帶大家深入了解Go語言中結(jié)構(gòu)體的使用,需要的可以參考一下2022-09-09
golang調(diào)試bug及性能監(jiān)控方式實(shí)踐總結(jié)
這篇文章主要為大家介紹了golang調(diào)試bug及性能監(jiān)控方式實(shí)踐是總結(jié),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
golang優(yōu)化目錄遍歷的實(shí)現(xiàn)方法
對(duì)于go1.16的新變化,大家印象最深的可能是io包的大規(guī)模重構(gòu),但這個(gè)重構(gòu)實(shí)際上還引進(jìn)了一個(gè)優(yōu)化,這篇文章要說的就是這個(gè)優(yōu)化,所以本將給大家介紹golang是如何優(yōu)化目錄遍歷的,需要的朋友可以參考下2024-08-08
Go語言內(nèi)建函數(shù)cap的實(shí)現(xiàn)示例
cap 是一個(gè)常用的內(nèi)建函數(shù),它用于獲取某些數(shù)據(jù)結(jié)構(gòu)的容量,本文主要介紹了Go語言內(nèi)建函數(shù)cap的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2024-08-08
Go結(jié)構(gòu)體從基礎(chǔ)到應(yīng)用深度探索
本文深入探討了結(jié)構(gòu)體的定義、類型、字面量表示和使用方法,旨在為讀者呈現(xiàn)Go結(jié)構(gòu)體的全面視角,通過結(jié)構(gòu)體,開發(fā)者可以實(shí)現(xiàn)更加模塊化、高效的代碼設(shè)計(jì),這篇文章旨在為您提供關(guān)于結(jié)構(gòu)體的深入理解,助您更好地利用Go語言的強(qiáng)大功能2023-10-10
Go標(biāo)準(zhǔn)庫strconv實(shí)現(xiàn)string類型與其他基本數(shù)據(jù)類型之間轉(zhuǎn)換
這篇文章主要為大家介紹了Go標(biāo)準(zhǔn)庫strconv實(shí)現(xiàn)string類型與其他基本數(shù)據(jù)類型之間轉(zhuǎn)換示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11

