網(wǎng)站首頁 編程語言 正文
前言:
這是一篇介紹 Go 編譯器如何實現(xiàn)內(nèi)聯(lián)的文章,以及這種優(yōu)化將如何影響你的 Go 代碼。
什么是內(nèi)聯(lián)?
內(nèi)聯(lián)是將較小的函數(shù)合并到它們各自的調(diào)用者中的行為。其在不同的計算歷史時期的做法不一樣,如下:
- 早期:這種優(yōu)化通常是由手工完成的。
- 現(xiàn)在:內(nèi)聯(lián)是在編譯過程中自動進行的一類基本優(yōu)化之一。
為什么內(nèi)聯(lián)很重要?
內(nèi)聯(lián)是很重要的,每一門語言都必然會有。
具體的原因如下:
- 它消除了函數(shù)調(diào)用本身的開銷。
- 它允許編譯器更有效地應(yīng)用其他優(yōu)化策略。
核心來講,就是性能更好了。
函數(shù)調(diào)用的開銷
基本知識
在任何語言中調(diào)用一個函數(shù)都是有代價的。將參數(shù)編入寄存器或堆棧(取決于ABI),并在返回時反轉(zhuǎn)這一過程,這些都是開銷。
調(diào)用一個函數(shù)需要將程序計數(shù)器從指令流中的一個點跳到另一個點,這可能會導(dǎo)致流水線停滯。一旦進入函數(shù),通常需要一些前言來為函數(shù)的執(zhí)行準(zhǔn)備一個新的堆棧框架,在返回調(diào)用者之前,還需要一個類似的尾聲來退掉這個框架。
Go 中的開銷
在 Go 中,一個函數(shù)的調(diào)用需要額外的成本來支持動態(tài)堆棧的增長。在進入時,goroutine 可用的堆棧空間的數(shù)量與函數(shù)所需的數(shù)量進行比較。
如果可用的堆棧空間不足,序言就會跳轉(zhuǎn)到運行時邏輯,通過將堆棧復(fù)制到一個新的、更大的位置來增加堆棧。
一旦這樣做了,運行時就會跳回到原始函數(shù)的起點,再次進行堆棧檢查,現(xiàn)在通過了,然后繼續(xù)調(diào)用。通過這種方式,goroutines可以從一個小的堆棧分配開始,只有在需要時才會增加。
這種檢查很便宜,只需要幾條指令,而且由于goroutine的堆棧以幾何級數(shù)增長,檢查很少失敗。因此,現(xiàn)代處理器中的分支預(yù)測單元可以通過假設(shè)堆棧檢查總是成功來隱藏堆棧檢查的成本。在處理器錯誤預(yù)測堆棧檢查并不得不丟棄它在投機執(zhí)行時所做的工作的情況下,與運行時增長goroutine堆棧所需的工作成本相比,管道停滯的成本相對較小。
Go 里的優(yōu)化
雖然每個函數(shù)調(diào)用的通用組件和 Go 特定組件的開銷被使用投機執(zhí)行技術(shù)的現(xiàn)代處理器很好地優(yōu)化了,但這些開銷不能完全消除,因此每個函數(shù)調(diào)用都帶有性能成本,超過了執(zhí)行有用工作的時間。由于函數(shù)調(diào)用的開銷是固定的,較小的函數(shù)相對于較大的函數(shù)要付出更大的代價,因為它們每次調(diào)用的有用工作往往較少。
因此,消除這些開銷的解決方案必須是消除函數(shù)調(diào)用本身,Go 編譯器在某些條件下通過用函數(shù)的內(nèi)容替換對函數(shù)的調(diào)用來做到這一點。這被稱為內(nèi)聯(lián),因為它使函數(shù)的主體與它的調(diào)用者保持一致。
改善優(yōu)化的機會
Cliff Click 博士將內(nèi)聯(lián)描述為現(xiàn)代編譯器進行的優(yōu)化,因為它是常量傳播和死代碼消除等優(yōu)化的基礎(chǔ)。
實際上,內(nèi)聯(lián)允許編譯器看得更遠(yuǎn),允許它在特定函數(shù)被調(diào)用的情況下,觀察到可以進一步簡化或完全消除的邏輯。
由于內(nèi)聯(lián)可以遞歸應(yīng)用,優(yōu)化決策不僅可以在每個單獨的函數(shù)的上下文中做出,還可以應(yīng)用于調(diào)用路徑中的函數(shù)鏈。
進行內(nèi)聯(lián)優(yōu)化
不允許內(nèi)聯(lián)
內(nèi)聯(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 }
運行這個基準(zhǔn)可以得到以下結(jié)果:
% go test -bench=.?
BenchmarkMax-4 ? 530687617 ? ? ? ? 2.24 ns/op
從執(zhí)行結(jié)果來看,max(-1, i)
的成本大約是 2.24ns,感覺性能不錯。
允許內(nèi)聯(lián)
現(xiàn)在讓我們?nèi)サ?//go:noinline pragma
?的語句,再看看不允許內(nèi)聯(lián)的情況下,性能是否會改變。
如下結(jié)果:
% go test -bench=.?
BenchmarkMax-4 ? 1000000000 ? ? ? ? 0.514 ns/op
兩個結(jié)果對比一看,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)
這些改進從何而來?
首先,取消函數(shù)調(diào)用和相關(guān)的前導(dǎo)動作是主要的改進貢獻者。其將 max 函數(shù)的內(nèi)容拉到它的調(diào)用者中,減少了處理器執(zhí)行的指令數(shù)量,并消除了幾個分支。
現(xiàn)在 max 函數(shù)的內(nèi)容對編譯器來說是可見的,當(dāng)它優(yōu)化 BenchmarkMax 時,它可以做一些額外的改進。
考慮到一旦 max 被內(nèi)聯(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 }
再次運行基準(zhǔn)測試,我們看到我們手動內(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)不會是負(fù)數(shù)。因此,條件?-1 > i
?將永遠(yuǎn)不會為真。
在證明了?-1 > i
?永遠(yuǎ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)在是一個常數(shù),編譯器可以消除無法到達的路徑,只留下如下代碼:
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)將表達式?r = max(-1, i)
?簡化為?r = i
。
這個例子非常不錯,很好的體現(xiàn)了內(nèi)聯(lián)的優(yōu)化過程和性能提升的緣由。
內(nèi)聯(lián)的限制
在這篇文章中,討論了所謂的葉子內(nèi)聯(lián):將調(diào)用棧底部的一個函數(shù)內(nèi)聯(lián)到其直接調(diào)用者中的行為。
內(nèi)聯(lián)是一個遞歸的過程,一旦一個函數(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 }
該運行速度將會和前面的例子一樣快,因為編譯器能夠反復(fù)應(yīng)用上面的優(yōu)化,將代碼減少到相同的?r = i
?表達式。
總結(jié)
這篇文章針對內(nèi)聯(lián)進行了基本的概念介紹和分析,并且通過 Go 的例子進行了一步步的剖析,讓大家對真實案例有了一個更貼切的理解。
Go 編譯器的優(yōu)化總是無處不在的。
原文鏈接:https://juejin.cn/post/7111176199174357023
相關(guān)推薦
- 2022-10-05 WPF實現(xiàn)好看的Loading動畫的示例代碼_C#教程
- 2023-11-21 高階函數(shù)HoF:用filter()方法編寫一個素數(shù)生成函數(shù)primes()
- 2022-06-01 Python使用list列表和tuple元組的方法_python
- 2022-09-22 String和StringBuilder的用法
- 2023-01-02 Python創(chuàng)建相同值數(shù)組/列表的兩種方法_python
- 2022-12-23 Android開發(fā)之線程通信詳解_Android
- 2022-07-03 C#列表List<T>、HashSet和只讀集合介紹_C#教程
- 2022-05-06 Linq中ToList()和CopyToDataTable()用法詳解_實用技巧
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支