simd: architecture and vector-size agnostic SIMD intrinsics under a GOEXPERIMENT
要約
概要
simdパッケージとして、アーキテクチャおよびベクトルサイズに依存しないポータブルなSIMD(Single Instruction, Multiple Data)イントリンシクスをGOEXPERIMENTフラグの下に追加するためのproposalです。これは#73787で提案された「二層アプローチ」における上位層(ポータブルAPI)の最初の実装であり、Go 1.26で導入されたsimd/archsimd(アーキテクチャ固有の下位API)の上に構築されます。
ステータス変更
likely_accept → accepted
2026年5月21日のProposal Review Meetingにて、前回のlikely_acceptから追加のコンセンサス変更がなかったことを受けて正式に承認されました。unsafe.Sizeofの非定数化の技術的課題についても実装方針が確認され、型パラメータの場合と同様の非定数サイズとして扱うことが合意されました。GOEXPERIMENTとしての限定公開であり、将来的な設計変更を前提とした実験的リリースとして位置づけられています。
技術的背景
現状の問題点
Go 1.26でsimd/archsimdパッケージが導入されましたが、このAPIはAMD64専用の固定幅ベクトル(Int8x16、Int32x8など)を扱います。アーキテクチャをまたいだポータブルなSIMDコードを書くことができないため、開発者はAVX2とAVX512でそれぞれ別実装を用意するか、アーキテクチャ固有のビルドタグを大量に書く必要がありました。また、ベクトル幅が実行時に決まるため(AVX使用時は256ビット、AVX512使用時は512ビットなど)、コンパイル時に決定できないという課題がありました。
提案された解決策
新しいsimdパッケージを追加し、アーキテクチャとベクトル幅に依存しないAPI群を提供します。型名はInt8s、Float32sのように元素型の複数形で表現し、実行時にハードウェアが持つ最大幅(AVX=256ビット、AVX512=512ビット、NEON=128ビットなど)を自動選択します。Int8sのベクトル長はx.Len()メソッドで実行時に取得できます。
主要なAPI:
- メモリ操作:
LoadFloat32Slice(s []float32) Float32s、StoreSliceなど - 算術演算:
Add、Sub、Mul、Div、MulAdd(積和算)、Sqrtなど - ビット演算:
And、Or、Xor、AndNot、Not、シフト演算 - 比較・マスク:
Equal、Greater等がMask8sなどのマスク型を返し、.Masked(mask)や.IfElse(mask, y)で条件付き演算を実現 - 型変換:
ToBits()でビット再解釈、ConvertToFloat32()で値変換 - アーキテクチャ固有APIとの橋渡し:
x.ToArch() anyとFloat32sFromArch[T archSimdFloat32s](x T) Float32sでsimd/archsimdとの相互変換
コンパイラはコードを複数の「バリアント」に自動分割してコンパイルし、実行時に最適なバリアントへディスパッチします。ベクトル幅ごとに異なるバイナリコードが生成されるため、内部ディスパッチのオーバーヘッドを最小化しつつ、インライン展開の機会も最大化します。
これによって何ができるようになるか
アーキテクチャ固有のコードを書かずに高性能なSIMD処理を実装できます。コードはAVX2機械とAVX512機械で自動的に異なるベクトル幅を活用し、SIMDをサポートしないプラットフォームではスカラーエミュレーションで動作します。
コード例
// Before: アーキテクチャ固有コードが必要(AMD64限定)
//go:build amd64
import "simd/archsimd"
func dotProduct(x, y []float32) float32 {
// AVX2向けのFloat32x8とAVX512向けのFloat32x16を
// それぞれ実装する必要があり、コード量が倍増する
var a archsimd.Float32x8
// ... アーキテクチャ固有の複雑な実装 ...
}
// After: simdパッケージを使ったポータブルな実装
import "simd"
func dotProduct(x, y []float32) float32 {
var a simd.Float32s
var i int
// a.Len()は実行時のSIMDレジスタ幅に応じて自動決定(4/8/16など)
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:])))
}
// 集約(Reduction)はまだsimdパッケージに含まれていないため
// ここで手動またはarchsimdへのドロップダウンが必要
return reduceSum(a)
}
活用シーン:
- 機械学習・推論: 行列積、アクティベーション関数など計算集約的な処理をAVX2/AVX512で自動最適化
- データ処理パイプライン: バイト列の検索・変換・集計を高速化し、同じコードがwasm/arm64/amd64で動作
- 暗号処理: XOR、AND演算などビット操作を多用する処理で、
simd.Uint8sを使いハードウェアの最大幅を活用
議論のハイライト
- ベクトル幅の固定サポートは現時点でスコープ外:
balasanjayから「特定の256ビット幅に固定したコードも書きたい(simd.Uint32x8のような型)」という要望が出たが、aclementsは「ハードウェアより広い幅のエミュレーションは大量の実装作業が必要」として初期バージョンのスコープ外と判断。 simd/archsimdとの相互変換API設計:x.ToArch() anyによるtype-switchアプローチ(Option 1)と、ジェネリクスを使ったx.ToArch[T ...]() (T, bool)(Option 2b)が検討されたが、ジェネリックメソッドによる実装がクラッシュを引き起こしたため、Option 1が暫定採用された。FromArchでサイズ不一致の場合はパニックとする方針。unsafe.Sizeofの非定数問題:simd.Int32sのサイズはコンパイル時に決定できない(スケーラブルアーキテクチャでは実行時まで不明)。型パラメータのunsafe.Sizeofと同様の非定数として扱う方向で合意。これはGoの型システムへの軽微な拡張となる。- メモリレイアウトとエンディアン: スライス要素0がベクトル要素0にマップされるリトルエンディアンの順序を標準とし、s390xなどビッグエンディアンのアーキテクチャでは単一命令のパーミュテーションで対応する方針。
- APIはwasmとamd64のSIMD APIの交差集合から自動生成:
simd/_gen/midway/ツールが各アーキテクチャのAPIの共通部分を自動計算してポータブルAPIを生成するため、新アーキテクチャが追加された場合にもAPIが自然に拡張される設計となっている。