Go 新老派之爭:實驗特性 SIMD 要來了,但 Rob Pike 反對!
最近 Go 社區(qū)有個大動作,SIMD 指令集終于要在 Go1.26 中以實驗特性的形式落地了。這對性能敏感的程序有訴求的同學(xué)來說,絕對是個好消息。
圖片
但這事兒在社區(qū)里引發(fā)了不小的爭議。Go 語言的聯(lián)合創(chuàng)始人 Rob Pike 直接 “開噴” 了。提案的作者正是現(xiàn)任 Go 技術(shù)負(fù)責(zé)人 Austin Clements。新老負(fù)責(zé)人對上了,這就有點意思了。
今天我們就來聊聊這個充滿爭議的提案到底是怎么回事。
背景
先說說為什么會有《proposal: simd: CPU feature vet check under GOEXPERIMENT=simd[1]》提案。
Go1.26 準(zhǔn)備引入一個simd包,里面全是針對特定架構(gòu)的 SIMD 內(nèi)聯(lián)函數(shù)。
圖片
這些函數(shù)直接對應(yīng)底層的 CPU 指令,性能拉滿,但問題也來了:如果硬件不支持對應(yīng)的 CPU 特性,這些函數(shù)就會 panic。
舉個例子,你在代碼里用了 AVX-512 的指令,結(jié)果跑在一臺老服務(wù)器上,CPU 根本不支持 AVX-512,那就只能看著程序崩潰了。
更麻煩的是,CPU 特性這個東西太復(fù)雜了。就拿 Intel 的 AVX-512 來說,光是 AVX-512 就有 21 個不同的特性標(biāo)志。雖然每個內(nèi)聯(lián)函數(shù)都會在文檔里寫明需要哪些特性,但在實際代碼里手動跟蹤這些依賴關(guān)系,實在太容易出錯了。
就算你測試覆蓋率做到 100%,如果 CI 環(huán)境的硬件剛好支持某個特性,測試全過了,結(jié)果到生產(chǎn)環(huán)境換了臺機器,照樣 panic 給你看。這就很尷尬了。
Austin Clements 的解決方案
現(xiàn)任 Go 技術(shù)負(fù)責(zé)人 Austin Clements 提出了一個挺聰明的方案:通過編譯器指令標(biāo)注 CPU 特性需求,然后用 vet 工具做靜態(tài)檢查。
簡單來說,就是讓開發(fā)者明確標(biāo)注哪些函數(shù)需要什么 CPU 特性,然后編譯器幫你檢查調(diào)用這些函數(shù)之前有沒有做好特性檢測。這樣在編譯期就能發(fā)現(xiàn)問題,而不是等到運行時才爆炸。
核心就是一個新的指令注釋:
//cpu:requires <feature> [&& <feature> ...]
func F(...) { ... }這個指令告訴編譯器:調(diào)用F函數(shù)之前,必須確保列出的 CPU 特性都可用。
特性名稱遵循 simd 包里特性檢查方法的命名規(guī)范,比如 X86.AVX 或者 X86.AVX512。
來看個實際例子:
//cpu:requires X86.AVX
func ProcessWithAVX(data []float64) {
// 這里可以安全使用AVX指令
// 使用AVX進行向量化計算
}
func Main() {
if !simd.X86.AVX() {
log.Fatal("CPU不支持AVX")
}
// 到這里編譯器就知道AVX是可用的
ProcessWithAVX(myData)
}vet 工具會分析整個調(diào)用鏈,確保每次調(diào)用 ProcessWithAVX 之前都做了 AVX 的特性檢查。如果你忘了檢查,編譯時就會報錯。
工作原理
vet 檢查的實現(xiàn)其實挺有意思的,它會對每個調(diào)用帶注釋函數(shù)的函數(shù)做流分析。
具體來說,假設(shè)函數(shù) G 調(diào)用了帶注釋的函數(shù) F,vet 會檢查在 G 的控制流中,所有到達(dá) F 調(diào)用點的路徑上,都必須先通過對應(yīng)的特性檢查。
而且 Go 核心團隊還考慮到了 CPU 特性之間的依賴關(guān)系。例如:一個 CPU 要支持 AVX2,那它肯定也支持 AVX。
vet 工具會理解這種隱含關(guān)系,不需要你重復(fù)標(biāo)注:
//cpu:requires X86.AVX2
func ProcessWithAVX2(data []float64) {
// AVX2肯定包含AVX
// 所以這里可以安全使用AVX和AVX2的指令
}
func Worker() {
if !simd.X86.AVX2() {
return
}
// vet知道AVX2隱含了AVX支持
ProcessWithAVX2(myData)
}函數(shù)值和閉包的處理
這個提案還考慮到了函數(shù)值和閉包的情況,設(shè)計得挺周到的。
對于函數(shù)值,vet 會檢查在捕獲函數(shù)的那個點,是否滿足了被捕獲函數(shù)的特性需求:
//cpu:requires X86.AVX
func H() {
// 使用AVX指令
}
var callback func()
func I() {
if !simd.X86.AVX() {
return
}
// 這里可以安全地把H賦值給callback
// 因為AVX特性已經(jīng)檢查過了
callback = H
}閉包就更有意思了。因為 CPU 特性在程序運行期間是不變的,所以閉包可以 "捕獲" 當(dāng)前已確認(rèn)的 CPU 特性:
var task func()
func J() {
if !simd.X86.AVX() {
return
}
task = func() {
// 這個閉包里可以假設(shè)AVX是可用的
// 即使它可能在J返回之后才被調(diào)用
// 因為CPU特性在程序生命周期內(nèi)是靜態(tài)的
}
}這個設(shè)計很巧妙,利用了 CPU 特性的靜態(tài)性質(zhì),讓閉包也能安全地使用 SIMD 指令。
接口的權(quán)衡
接口就比較棘手了。
最保守的做法是,如果一個類型的任何方法有特性需求,那在任何地方把這個類型轉(zhuǎn)成接口都得檢查這些需求。但這樣太嚴(yán)格了,而且接口值后面還可能被類型斷言到更寬的接口,要完全靜態(tài)檢查幾乎不可能。
Austin 最后決定:干脆不檢查接口賦值。
他們的理由是,SIMD 代碼通常對性能要求極高,很少會用接口這種動態(tài)分派的方式。就算真的出問題了,大不了方法調(diào)用時 panic,也不會比現(xiàn)在更糟。
不過對于泛型的約束,就可以做靜態(tài)檢查了:
type Processor[T any] interface {
Process(data []T)
}
//cpu:requires X86.AVX2
func (f *FastImpl) Process(data []float64) {
// 使用AVX2指令
}
func UseProcessor[T Processor[float64]](p T "T Processor[float64]") {
// 泛型這里可以靜態(tài)檢查
p.Process(data)
}Go 創(chuàng)始人:Rob Pike 的擔(dān)憂
直接反對
這時候 Rob Pike 站出來了。作為 Go 語言的聯(lián)合創(chuàng)始人,老爺子發(fā)話可不輕松。
圖片
他在 issue #76175 下面直接評論說:
"This opens the door to an ever-expanding world of complexity, incompatibility and run-time surprise. It feels un-Go-like to me."
(這打開了一扇通往復(fù)雜性、不兼容性和運行時意外的不斷擴張世界的大門。感覺不太像 Go 的風(fēng)格。)
緊接著他又補了一刀:
"Ditto - especially- for #73787, which I fear I did not see when it was proposed."
(同樣的問題也適用于 #73787,我擔(dān)心當(dāng)時提案的時候沒看到。)
這個 issues #73787[2] 就是 SIMD 包本身的提案。換句話說,Rob Pike 不僅反對這個 vet 檢查方案,連整個 SIMD 包的引入他都有保留意見。
圖片
不過顯然,#73787 這個提案在現(xiàn)任掌門人的把持下是通過的:
圖片
擔(dān)憂的問題
Rob Pike 的擔(dān)憂不是沒有道理的。他一直秉持 Go 應(yīng)該保持簡單的理念。在他看來:
- 復(fù)雜性:引入 CPU 特性檢查意味著開發(fā)者要理解各種 CPU 指令集的差異,這本身就增加了心智負(fù)擔(dān)。21 個 AVX-512 特性標(biāo)志,誰能記得住?
- 不兼容性:。不同硬件支持的特性不一樣,同一份代碼在不同機器上表現(xiàn)可能完全不同。這和 Go"一次編譯,到處運行"的理念有點沖突。
- 運行時意外:。雖然有 vet 檢查,但接口調(diào)用、反射等動態(tài)特性還是可能繞過靜態(tài)檢查,最后還是會在運行時 panic。
說白了,Rob Pike 擔(dān)心的是:這個特性會讓 Go 變得不再是那個簡單、可預(yù)測的 Go。
Go 新老派之爭:簡單和性能
這個爭議在社區(qū)里引發(fā)了不小的討論。支持者和反對者都有充分的理由。
這次爭議反映出了 Go 語言發(fā)展的一個深層次矛盾:簡單性和性能之間的取舍。
Rob Pike 代表的是 Go 的老派哲學(xué):"Less is more",保持簡單,不要什么都往語言里塞。
Austin Clements 代表的是 Go 的新需求:隨著 Go 應(yīng)用場景的擴展,用戶對性能的要求越來越高,語言必須跟上時代。
Go 未來的方向
不管爭議如何,提案還是在推進。Austin 團隊計劃做一些擴展:
1、需求表達(dá)式:支持用&&和||組合多個特性,比如"要么 AVX2 要么 AVX512":
//cpu:requires X86.AVX2 || X86.AVX512
func ProcessOptimized(data []float64) {
// 兩種指令集都能用
}2、自定義特性檢查函數(shù):讓用戶也能寫特性檢查函數(shù):
//cpu:ensures X86.AVX2
func CheckAVX2() bool {
return simd.X86.AVX2()
}3、動態(tài)接口調(diào)用檢查:雖然靜態(tài)檢查接口不太現(xiàn)實,但可以在編譯器層面做點文章。為有特性需求的方法生成包裝函數(shù),在包裝函數(shù)里做動態(tài)檢查。直接調(diào)用方法時跳過這些檢查,通過接口調(diào)用時就會觸發(fā)動態(tài)檢查。
4、基于特性的優(yōu)化:編譯器可以根據(jù)cpu:requires指令做更激進的優(yōu)化。知道某個函數(shù)一定運行在支持 AVX2 的環(huán)境,就可以生成更優(yōu)化的代碼。
總結(jié)
這個提案解決的是個實實在在的問題,但也確實觸及了 Go 語言設(shè)計哲學(xué)的核心爭議。
Go 新老負(fù)責(zé)人的分歧,本質(zhì)上反映的是 Go 語言在簡單性和功能性之間的權(quán)衡。這不是第一次,也肯定不是最后一次。
目前這個特性還在 GOEXPERIMENT=simd 的實驗階段,預(yù)計在 Go1.26 會以實驗特性正式亮相。不管你是站 Austin 這邊還是 Rob 這邊,都值得關(guān)注一下這個特性的落地效果。
說不定幾年后回頭看,我們會發(fā)現(xiàn)這次爭議是 Go 語言發(fā)展史上的一個重要轉(zhuǎn)折點。就像當(dāng)年泛型的爭議一樣,最后還是落地了。但怎么樣,只有自己的知道了。
參考資料
[1] proposal: simd: CPU feature vet check under GOEXPERIMENT=simd: https://github.com/golang/go/issues/76175
[2] 73787: https://github.com/golang/go/issues/73787


























