simd: architecture and vector-size agnostic SIMD intrinsics under a GOEXPERIMENT
要約
概要
simd パッケージとして、アーキテクチャおよびベクタ幅に依存しないポータブルなSIMD(Single Instruction, Multiple Data)APIをGoの標準ライブラリに追加するproposalです。GOEXPERIMENT=simd フラグの下で実験的機能として提供され、開発者がアセンブリコードを書かずにSIMD演算を活用できるようにします。
ステータス変更
active → likely_accept
2026年5月13日のProposal Review Meetingにおいて、@aclementsがGo Proposal Reviewグループを代表して likely_accept に変更しました。主な理由は、広範な反対意見がなく詳細に関するいくつかの質問のみが残る状態であったこと、そしてこれが GOEXPERIMENT による実験的な導入であり将来の変更を前提としたものであることが明示されたためです。
技術的背景
現状の問題点
GoでSIMD命令を利用するには手書きのアセンブリコードが唯一の手段でした。この方法には重大な欠点があります。
- 書くのが難しく、保守コストが高い
- 非同期プリエンプション(goroutineの割り込み)ができない
- 小さなカーネル関数のインライン化を妨げる
- アーキテクチャ(amd64、arm64、wasm等)ごとに別々の実装が必要
Go 1.26で導入されたsimd/archsimdパッケージはアーキテクチャ固有のSIMD命令を提供しましたが、コードがアーキテクチャに依存するという問題は残っていました。
提案された解決策
simd パッケージは #73787 で説明された「二層構造アプローチ」の第二層にあたります。設計の特徴は以下の通りです。
ベクタサイズの自動選択: プログラム実行中に最適なベクタ長を自動選択します(例: AVX、AVX2、AVX512のいずれかを実行時に自動判定)。ひとつのプログラム実行内では全ベクタが同じビット幅を持ちます。
型体系: 要素型に応じた複数形の型名を使用します。
- 符号付き整数:
Int8s、Int16s、Int32s、Int64s - 符号なし整数:
Uint8s、Uint16s、Uint32s、Uint64s - 浮動小数点:
Float32s、Float64s - マスク型:
Mask8s、Mask16s、Mask32s、Mask64s
APIの導出方法:mocks.goに定義されるAPIはwasm SIMDとamd64 SIMDなど各アーキテクチャのAPIの**共通部分(intersection)**として自動生成されます。新しい演算を追加したい場合は、各アーキテクチャ固有のAPIにエミュレーションを追加することで交差集合を拡張します。
コンパイラによる多バリアント生成: SIMDに依存するコードはコンパイラによって複数のアーキテクチャバリアントに自動変換されます。内部的には@文字を含む名前が生成されるため、ソースコードから直接参照できません。
これによって何ができるようになるか
アセンブリコードを一行も書かずに、あらゆるアーキテクチャで動作するSIMD最適化コードを記述できるようになります。
コード例
// Before: 従来の書き方(手書きアセンブリが必要、またはSIMDを諦める)
func innerProduct(x, y []float32) float32 {
var r float32
for i, v := range x {
r += v * y[i]
}
return r
}
// After: simdパッケージを使ったポータブルなSIMD実装
func innerProduct(x, y []float32) float32 {
var a simd.Float32s
var i int
// ベクタ幅分ずつ処理(Len()は実行時に決まる)
for i = 0; i < len(x)-a.Len()+1; i += a.Len() {
u := simd.LoadFloat32Slice(x[i : i+a.Len()])
v := simd.LoadFloat32Slice(y[i : i+a.Len()])
a = a.Add(u.Mul(v))
}
// 端数処理
if i < len(x) {
a = a.Add(simd.LoadFloat32SlicePart(x[i:]).
Mul(simd.LoadFloat32SlicePart(y[i:])))
}
return sum(a) // sumは別途実装
}
アーキテクチャ固有の最適化が必要な場合は、ToArch() で降格し、type switch で処理を分岐できます。
//go:build amd64
func sum(x simd.Float32s) float32 {
switch a := x.ToArch().(type) {
case archsimd.Float32x8: // AVX2
a = a.AddPairsGrouped(a)
a = a.AddPairsGrouped(a)
return a.GetLo().GetElem(0) + a.GetHi().GetElem(0)
case archsimd.Float32x16: // AVX512
// ...
case archsimd.Float32x4: // SSE
// ...
}
}
議論のハイライト
- 固定ベクタ幅の対応は現時点でスコープ外:
simd.Uint32x8(幅固定の8要素)のような型も有用との意見(@balasanjay)があったが、幅の広いハードウェアでのエミュレーションは実装コストが高く、現版ではスコープ外とされた(@aclements)。将来的な追加は否定されていない。 archsimdとの相互変換API:ToArch() anyと型スイッチを組み合わせる方式(Option 1)と、ジェネリクスを使う方式(Option 2b)が検討された。現状ではOption 1を基本とし、<SimdType>FromArch[T ...]()のジェネリクス関数を組み合わせる方向が有力。FromArchが期待と異なるベクタ長を受け取った場合はパニック: エラーを返す((Int8s, bool))案も検討されたが、チェックを怠ると誤ってゼロ値を使うリスクがあるとして、パニックが正しい設計とされた(@aclements)。LoadMask<Size>Sliceはジェネレータのバグ: レビュー会議で指摘されたLoadMask<Size>Sliceは実装の自動生成における誤りであり、プロトタイプでは修正済みと確認された(@dr2chase)。- ビット再解釈の設計:
BitsNxLという別型を追加する案(@AndrewHarrisSPU)も検討されたが、型のメソッドブロートを削減できる一方で、符号付き/符号なし右シフトの区別ができないなどの問題があり、ToBits()/BitsToInts()/BitsToFloats()をUint型を介する3段階変換方式が採用された。