simd: architecture and vector-size agnostic SIMD intrinsics under a GOEXPERIMENT
要約
概要
simd パッケージとして、アーキテクチャおよびベクタサイズに依存しないポータブルなSIMD(Single Instruction, Multiple Data)APIを GOEXPERIMENT=simd フラグの下で提供することを提案するものです。これはGo 1.26で導入された低レベルのアーキテクチャ固有SIMDパッケージ(simd/archsimd)の上位レイヤとして位置付けられる「第2レベル」のAPIです。
ステータス変更
(新規) → active
2026年5月6日のProposal Review Meeting(@aclements、@adonovan、@bradfitz、@cherrymui、@griesemer、@neild、@rolandshoemaker)において、本提案が「active(議論継続中)」として位置付けられました。広範な反対意見はなく、設計の詳細についての質問がいくつか提起された段階です。具体的なAPIの整理(特にAMD64 APIとの命名統一)が進行中であることも確認されています。
技術的背景
現状の問題点
Go 1.26では simd/archsimd パッケージにより、AMD64向けの低レベルなSIMD命令(Float32x8、Int8x16 など、具体的なベクタ幅を持つ型)が利用可能になりました。しかし、このAPIはアーキテクチャと具体的なベクタ幅に強く依存しており、以下のような問題があります。
- AVX(128bit)、AVX2(256bit)、AVX512(512bit)、ARM SVE、RISC-V RVV など、ハードウェアが異なるとベクタ幅が変わる
- コードを複数アーキテクチャ向けに手動で分岐・管理する必要がある
- 移植性のある高性能なコードを書くには事実上アセンブリに近い知識が必要
提案された解決策
simd パッケージを新設し、ベクタ幅もアーキテクチャも抽象化した「幅非依存」の型と操作を提供します。型名は要素の型と符号を反映した複数形(Int8s、Float32s、Uint64s など)で、各型が保持する要素数はプログラム実行時に x.Len() で取得します。
コンパイラは simd を使ったコードを複数のバリアント(AVX/AVX2/AVX512など)に自動的に複製・リライトし、ランタイムでCPUの能力に応じて最適なバリアントを選択します。このリライトはコンパイラの初期段階で行われ、インライン展開の恩恵を最大化します。
マスク型(Mask8s、Mask32s など)も抽象化されており、.Masked(mask) や .IfElse(mask, y) を用いた条件付き演算が可能です。また、.ToArch() メソッドにより、必要に応じてアーキテクチャ固有の型へ変換し simd/archsimd の特殊命令を利用することもできます。
これによって何ができるようになるか
移植性のある高性能なSIMDコードを、アーキテクチャを意識せずに記述できるようになります。
- 数値計算・機械学習推論: ベクタの内積・行列演算を1つのコードで全アーキテクチャに対応
- 暗号・ハッシュ処理: XOR・AND・シフト演算を高速化しつつ、アーキテクチャに縛られない実装が可能
- テキスト・バイト列処理: 比較・最大値・最小値を一括処理する高速フィルタ
コード例
// Before: アーキテクチャ固有コードでの内積計算(amd64専用)
//go:build amd64
import "simd/archsimd"
func innerProduct(x, y []float32) float32 {
// AVX2 (Float32x8) か AVX512 (Float32x16) かを手動で判断する必要がある
// アーキテクチャごとに別ファイルが必要
}
// After: simd パッケージによるポータブルな内積計算
import "simd"
func innerProduct(x, y []float32) float32 {
var acc simd.Float32s
var i int
// Len() はランタイムでCPUの最大ベクタ幅に応じた要素数を返す
for i = 0; i < len(x)-acc.Len()+1; i += acc.Len() {
u := simd.LoadFloat32Slice(x[i : i+acc.Len()])
v := simd.LoadFloat32Slice(y[i : i+acc.Len()])
acc = acc.Add(u.Mul(v))
}
// 端数処理
if i < len(x) {
acc = acc.Add(
simd.LoadFloat32SlicePart(x[i:]).Mul(
simd.LoadFloat32SlicePart(y[i:])))
}
// accをスライスに書き出して合計
s := make([]float32, acc.Len())
acc.StoreSlice(s)
var r float32
for _, e := range s {
r += e
}
return r
}
議論のハイライト
- 固定幅ベクタのサポート範囲:
simd.Uint32s(幅不定)は提案範囲内だが、simd.Uint32x8(固定8要素)のように上位ハードウェアで複数ベクタにエミュレートする「固定幅ポータブル型」は初期スコープ外とされた。@aclementsが明示的に確認している - APIの初期規模: wasm SIMDとAMD64 SIMDの「共通部分」のみを初期APIとして採用することで、将来の新アーキテクチャへの対応を容易にしている。エミュレーションによる拡張が設計の基本戦略
archsimdとの相互変換:x.ToArch() anyによりアーキテクチャ固有型へ変換しtype-switchで特殊命令を利用する「Option 1」と、ジェネリクスを使う「Option 2b」が検討された。Option 2bはジェネリックメソッドが原因でtype-checkがクラッシュするため現時点では採用困難LoadSlicePartの戻り値: 実際にロードされた要素数が返り値に含まれない点について懸念が示された。min(len(s), x.Len())で算出可能だが、誤ってx.Len()をそのまま使ってしまうミスを誘発しやすいと指摘された- 型システムへの影響: これらの型はコンパイル時にサイズが決まらない点で型パラメータに類似しており、
unsafe.Sizeofなどのツールとの相互作用を実験で確かめる必要があることが認識されている。また、内部でリライトされた関数名に@文字が含まれるため、デバッガなどのツールへの影響を調査する必要がある