Go 1.27 引入實驗性平台無關 SIMD 套件

TL;DR

Go 1.27 發布了一個實驗性的、平台無關的 simd 套件,抽象化固定大小的向量類型,讓開發者可以撰寫一次的向量程式碼,即可在 AVX/AVX2/AVX512(amd64)、NEON(arm64)和 WebAssembly 上以接近組合語言的效能執行,同時在沒有 SIMD 支援的 CPU 上會自動降級為快速的軟體模擬。


為何需要可移植的 SIMD 層?

不同 CPU 家族以截然不同的方式暴露 SIMD:

  • 向量寬度可能是固定的(例如 wasm、PowerPC、s390x 上的 128 位元)或可變的(RISC‑V V‑延伸、ARM SVE)。
  • 掩碼語意不同:有些架構有專用的掩碼暫存器(AVX‑512、RVV),有些使用向量布林值(NEON、AVX2),有些則完全沒有掩碼。
  • 指令集不僅因家族而異,也因功能旗標而異(AVX 對 AVX2 對 AVX‑512、NEON 對 SVE 等)。

由於 Go 早期的 archsimd 套件必須直接暴露這些細節,撰寫可移植的 SIMD 程式碼需要針對各架構的條件判斷與手動撰寫組合語言。新的 simd 套件將固定大小的向量從類型系統中移除,僅實作所有支援平台都能執行的操作交集,並以高效模擬填補差異。

如何啟用實驗性 API

在建置或執行程式時,設定環境變數 GOEXPERIMENT=simd,就像舊的 archsimd 實驗一樣。編譯器會根據程式啟動時偵測到的硬體,將 simd 類型的呼叫重寫為架構特定的實作或模擬的備用方案。

核心設計原則

  1. 交集優先 API – 僅公開所有目標平台共用的操作。缺少的原語透過映射至現有 SIMD 指令的模擬提供。
  2. 接近組合語言的效能 – 當原始操作符合硬體能力時,產生的程式碼速度與手寫組合語言相當。
  3. 平順降級 – 在沒有 SIMD 支援的 CPU 上(或設定 GODEBUG=simd=0 時),相同程式碼會使用純 Go 實作執行。
  4. 可讀性 – 類型命名為 simd.Uint8s、simd.Float32s 等,方法語法如同一般 Go 方法,讓程式碼對人類與大型語言模型都易於理解。

範例:向量化內積

func innerProduct(x, y []float32) float32 {
    var acc simd.Float32s
    var i int
    for i = 0; i < len(x)-acc.Len()+1; i += acc.Len() {
        u := simd.LoadFloat32s(x[i : i+acc.Len()])
        v := simd.LoadFloat32s(y[i : i+acc.Len()])
        acc = u.MulAdd(v, acc) // acc += u*v
    }
    if i < len(x) {
        u, _ := simd.LoadFloat32sPart(x[i:])
        v, _ := simd.LoadFloat32sPart(y[i:])
        acc = u.MulAdd(v, acc)
    }
    return simd.ReduceSum(acc) // Go 1.28 新增
}

迴圈每次處理 acc.Len() 個元素,其中 acc.Len() 是執行時期偵測到的向量寬度(128、256 或 512 位元)。相同原始碼在現代 x86 上編譯為 AVX‑512,在 Apple Silicon 上為 NEON,或在 SIMD 不可用時為純 Go 迴圈。


支援的操作(Go 1.27)

下表總結目前可用的方法。V 表示向量類型,M 表示掩碼類型,E 表示標量元素類型。

載入 / 廣播

  • LoadV([]E) V – 將完整切片載入至向量。
  • LoadVPart([]E) (V, int) – 載入部分切片,回傳實際載入的元素數量。
  • BroadcastV(E) V – 將標量廣播至所有通道。

儲存 / 字串表示

  • Store([]E) – 將向量寫回切片。
  • StorePart([]E) int – 儲存盡可能多的通道。
  • String() string – 人類可讀的表示方式。

算術(選用)

  • Add、Sub、Mul、Div(僅浮點數)、Neg、Abs(僅浮點數)。
  • MulAdd – 浮點數的融合乘加運算。
  • IfElse(mask, y) – 在 mask 為真時,從 y 選取元素。
  • Masked(mask) – 在掩碼為假時,將通道歸零。

布林與掩碼

  • 向量上的 And、Or、Xor、AndNot。
  • Mask 類型(Mask8s、Mask16s …)支援 And、Or、String、ToIntWs。

比較運算

  • Equal、NotEqual、Greater、GreaterEqual、Less、LessEqual – 產生與元素寬度相同的掩碼。

轉換與重塑

  • ConvertToFloatW、ConvertToIntW、ConvertToUintW。
  • ToBits、ReshapeToUint*、BitsToFloatW、BitsToIntW – 零成本的重新解釋。

移位與旋轉(僅整數向量)

  • ShiftAllLeft/Right、RotateAllLeft/Right,適用於 16、32 和 64 位元通道。

注意: 第一版缺少泛型歸約(ReduceSum)和部分以掩碼為中心的工具;這些功能預計於 Go 1.28 加入。


與架構特定程式碼的橋接

當需要的操作尚未納入可移植 API 時,開發者可透過 ToArch() 和 FromArch 助手回退至 archsimd 套件。以下為 amd64 上缺少 Int8s.OnesCount 實作的範例:

//go:build goexperiment.simd && amd64
func OnesCount(v simd.Int8s) simd.Int8s {
    switch x := v.ToArch().(type) {
    case archsimd.Int8x16:
        // 使用查表技巧處理 AVX/AVX2。
        lut := archsimd.LoadInt8x16Array(&popcnt4x16)
        mask := archsimd.BroadcastInt8x16(0x0f)
        lo := x.And(mask)
        hi := x.ToBits().ReshapeToUint16s().ShiftAllRight(4).
                ReshapeToUint8s().BitsToInt8().And(mask)
        return simd.Int8sFromArch(lut.PermuteOrZero(lo).Add(lut.PermuteOrZero(hi)))
    default:
        return OnesCountEmulated(v)
    }
}

編譯器會在編譯時期專門化此型別判斷,因為二進位檔在編譯時已知目標平台存在的 archsimd 類型。


使用 GODEBUG 進行執行時期控制

開發者可強制設定特定向量寬度或完全模擬以供測試:

  • GODEBUG=simd=0 – 始終使用模擬路徑。
  • GODEBUG=simd=128、256、512 – 要求該寬度;若不可用則恐慌。
  • GODEBUG=simd=+128(或 +256、+512) – 即使缺少某些可選指令也允許該寬度;使用不支援的指令會在執行時期恐慌。

實作注意事項

編譯器執行 AST 重寫,為所有提及 simd 類型的函式產生大小專用的複本。這些複本位於 simd/internal/bridge,並以具體的 archsimd 類型實例化(例如 Int8x16)。分派僅在程式啟動時發生一次,之後熱門迴圈會執行專用版本,不再需要分支。加入對 simd 類型的虛擬參考(例如 var _ simd.Uint64s)可將分派點提升至周圍迴圈,確保使用專用版本。


社群反應(選摘 HN 評論)

"在這種情況下,可移植的 SIMD 比非可移植的 SIMD 慢約 11 %,但兩者都比非 SIMD 快約 5 倍。" – ImJasonH(wasm 上的基準測試)

"C++ 正在引入 std::simd;我完全支持以最少的內建指令撰寫向量程式碼。" – beached_whale

"沒有人要求這個功能,但他們花時間把它做對了,並持續推動 Go 成為記憶體安全、高階的系統語言。" – u8

"這為已經支援多核心的 Go 專案開啟了許多低階效能的大門。很少有語言提供標準函式庫的 SIMD 支援。" – qprofyeh

"介面轉換和型別判斷看起來效率不高,但編譯器會將其專門化;判斷在啟動時就解決,而非每次迴圈執行。" – vlovich123


未來展望

  • Go 1.28 將為 archsimd 加入 SVE 支援,並最終納入可移植的 simd 套件。
  • 計畫新增 OnesCount 等新操作、更豐富的掩碼工具、歸約原語和向量混洗。
  • 功能變異旗標將讓程式碼在僅缺少少數可選指令的硬體上執行(例如 Raspberry Pi 的 NEON 不含 PMULL)。
  • 一篇關於 archsimd 內部實作的專題部落格文章即將推出。

總結

Go 的實驗性 SIMD 層消除了歷史上的架構特定組合語言障礙,提供一個乾淨、大小無關的 API,可在多種 CPU 上實現接近原生的效能,同時確保在任何平台上的功能降級。其設計理念與 C++ 的 Highway 相呼應,並預見未來如 SVE 等擴充,使 Go 成為高階、跨平台高效能工作負載的更具吸引力的選擇。

Sources

相關

  • 專案
  • 專案
  • 專案
  • 專案