プログラミング
Goにおけるプラットフォーム非依存SIMD
Platform-independent SIMD in Go (go.dev)
要約
Go 1.26および1.27で、SIMD(Single Instruction Multiple Data)操作のための実験的なAPIが導入されました。これにより、CPUの並列処理能力を最大限に活用し、計算負荷の高いタスクのパフォーマンスを大幅に向上させることが可能になります。今回のAPIは、様々なアーキテクチャ間の差異を吸収し、プラットフォームに依存しない統一的なインターフェースを提供することを目指しています。
全文翻訳
Go 1.26および1.27には、SIMD(Single Instruction Multiple Data)操作のための実験的なAPIが含まれています。
SIMDは多くの最新CPUに搭載されているネイティブ機能であり、ソフトウェアが非常に高速にデータのベクトルに対して均一な操作を実行できるようにします。例えば、単一の命令で8組のfloat64値を加算するといったことが可能です。これにより、暗号化からデータ処理、AIに至るまで、計算負荷の高い多くのタスクを大幅に高速化できます。実際、GoのGreen Teaガベージコレクタでさえ、ライブオブジェクトのスキャンを高速化するためにSIMDを利用しています。
これらの新しい実験的APIが登場する前は、Goからこの機能にアクセスする唯一の方法はGoアセンブリで記述することでした。これは真にパフォーマンスが重要な計算カーネルに対してのみ価値があり、SIMDの恩恵を受けられるはずの多くのソフトウェアがCPUの能力を十分に活用できていない状況でした。
Go 1.27ではamd64用のSIMD APIが導入され、Go 1.27ではarm64(特にNEON)とwasm用のAPIが追加されました。しかし、SIMD APIの基本的な課題は、プラットフォーム間の膨大な差異です。サポートする操作だけでなく、ベクトルの表現方法にさえ違いがあります。一部のプラットフォームは、通常128ビットから512ビットの固定サイズのベクトルを提供しますが、他のプラットフォームでは、ベクトルのサイズはビルド時に不明であり、プログラム起動時にクエリする必要があります。これらのプラットフォームの広範な機能への完全なアクセスを提供するために、これらのAPIはアーキテクチャ依存のarchsimdパッケージに存在します。
しかし、Go 1.27はこれらのアーキテクチャ依存APIを超えて、C++のHighwayに loosely 基づいた、実験的で完全にポータブルな、プラットフォームおよびサイズに依存しないSIMDインターフェースを導入しています。目標は、SIMDサポートのあるプラットフォームで、一度書けばアセンブリ言語に近いパフォーマンスを発揮する「simd」コードをサポートし、SIMDサポートが(まだ)ないプラットフォームでは、有能なエミュレーションを提供することです。
simdパッケージは現在、amd64でAVX、AVX2、AVX512を、arm64でNEONを、wasmのSIMD命令をサポートしています。
動機:SIMDアーキテクチャ間の差異
SIMDアーキテクチャはいくつかの次元で異なります。一部は単一の固定ベクトルサイズ(wasm、PowerPC、s390x、128ビット)を提供します。一部は複数の固定ベクトルサイズ(amd64は128、256、512ビット、loong64は128、256ビット)を提供します。Riscv64は128ビットから65536ビットの範囲でサイズが指定されていないベクトルをサポートしますが、長さは2のべき乗に制限されます。Arm64は1つの固定サイズ(128ビット、NEON)と1つの可変サイズ(128〜2048ビット、2のべき乗のみ、SVE)をサポートします。
特定のアーキテクチャインスタンスで、そのインスタンスが実際にサポートするサイズを決定するには、機能チェックが必要です。amd64ですが、AVX、AVX2、またはAVX512ですか?Arm64ですが、NEONまたはSVEですか?SVEの場合、どのくらいの大きさですか?どのバージョンのSVEですか:SVE、SVE2、またはSVE2.1ですか?
異なるSIMDアーキテクチャは、ベクトルマスキングの処理方法が異なります。ベクトルでは、ベクトル全体でのif-then-elseはマスクを使用して実装できます。操作を実行しますが、マスクが「true」である場所にのみ結果(またはロード、ストア)を割り当てます。一部のSIMDバリアントはマスクを提供しません。すべての操作はすべての要素に対して実行され、「マスキング」はベクトルビットマスクとベクトルブール演算(wasm、AVX、AVX2、NEON)で行われます。一部は特別なマスクレジスタを提供し、1つのビットが1つのベクトル要素に対する操作を制御します(AVX512およびRVV)。他のもの(SVE)は、ベクトルバイトごとに1ビットを割り当てますが、各要素のマスクビットの最下位ビットがマスク操作を制御します。AVX2は、マスクとしてプレーンなベクトルを使用し、最上位ビットが操作を制御するマスク付きロードとストアもサポートします。
3つ目の差異は、操作自体にあります。各アーキテクチャは、ベクトル要素を並べ替えるための独自のプリミティブを提供します。一部は定数入力を必要とし、他は可変入力をサポートします。異なるSIMDアーキテクチャは、異なる暗号化関連操作をサポートします。基本的な算術演算でさえ、サポートが異なる場合があります。たとえば、wasmは64ビット整数のベクトルの比較を欠いています。
特定のアーキテクチャ上の特定のベクトル長であっても、命令サポートはチェックする必要がある「機能」に依存します。
Goのアーキテクチャ依存のarchsimdパッケージは、アーキテクチャ間で可能な限り均一になるように設計されていましたが、これらの多くの癖が残っており、マルチプラットフォームSIMDの設計、記述、テストを困難なものにしています。archsimdパッケージで、異なるアーキテクチャがより似ているように見せるためにさらに多くのことを行うことはできますが、効率を犠牲にすることなく、ある程度までしかできません。
概要
新しいsimdパッケージは、型システムから固定サイズベクトルを削除し、すべての異なるプラットフォームの共通部分にある操作のみをサポートすることで、これらの違いを隠します。そして、共通部分のギャップを、他のSIMD命令による効率的なエミュレーションで埋めます。目標は、ベクトル化された実装から恩恵を受ける多くのデータ処理アルゴリズムをサポートするのに十分な操作セット(ただし、特定のベクトルサイズに依存しない)、ハードウェアと一致するソースコード操作の場合、アセンブリ言語と同等の効率、そうでなければ可能な限り最良のエミュレーション、そして読みやすく理解しやすい(LLMがコードを書く場合でも、特に)ことです。
SIMD命令がないプラットフォーム、またはarchsimdでサポートされていないプラットフォームでは、すべての操作がエミュレートされるため、simdパッケージを使用して書かれたコードは常に実行されます。
この実験的なパッケージを使用するには、実験的なarchsimdパッケージを使用する場合と同様に、ビルド時にGOEXPERIMENT=simdを設定します。
simdベクトルの型は、単に大文字で複数形になったプリミティブ型です。例えば、simd.Uint8sやsimd.Float32sです。ベクトルはスライスからロードおよびストアされます。例えば:
// innerProduct は x と y の内積を返します。
func innerProduct(x, y []float32) float32 {
var a simd.Float32s
var i int
for i = 0; i < len(x)-a.Len()+1; i += a.Len() {
u := simd.LoadFloat32s(x[i : i+a.Len()])
v := simd.LoadFloat32s(y[i : i+a.Len()])
a = u.MulAdd(v, a)
}
if i < len(x) {
u, _ := simd.LoadFloat32sPart(x[i:])
v, _ := simd.LoadFloat32sPart(y[i:])
a = u.MulAdd(v, a)
}
return sum(a)
}
// sum は x の要素のスカラー合計を返します。
func sum(x simd.Float32s) float32 {
s := make([]float32, x.Len())
x.Store(s)
var r float32
for _, e := range s {
r += e
}
return r
}
この例は、このパッケージの最初の実験リリースにおける制限の1つも示しています。ベクトル全体にわたる要素の合計を行う共通の方法がないため、Go 1.27のsimdではサポートされていません。ただし、ReduceSumは次のリリースで登場するため、sumは単にsimd.ReduceSumに置き換えることができます。
SIMD比較はマスク値を生成します。これは対応するベクトル要素幅に固有であるため、Int8sの比較はMask8sなどを生成し、マスク値を使用してベクトルを選択およびフィルタリングできます。
Go 1.27時点でのサポートされているsimdパッケージの操作
この表では、VとUはベクトル型、Mはマスク型、Eはスカラー型、Wは幅を表します。
パッケージレベルのロード/ブロードキャスト関数
関数 Int8s Int16s Int32s Int64s Uint8s Uint16s Uint32s Uint64s Float32s Float64s
LoadV([]E) V Y Y Y Y Y Y Y Y Y Y
LoadVPart([]E) (V, int) Y Y Y Y Y Y Y Y Y Y
BroadcastV(E) V Y Y Y Y Y Y Y Y Y Y
ストア/文字列操作 (x V).Method(...)
Int8s Int16s Int32s Int64s Uint8s Uint16s Uint32s Uint64s Float32s Float64s
Store(s []E) Y Y Y Y Y Y Y Y Y Y
StorePart(s []E) int Y Y Y Y Y Y Y Y Y Y
String() string Y Y Y Y Y Y Y Y Y Y
算術演算 (x V).Method(...)
V Int8s Int16s Int32s Int64s Uint8s Uint16s Uint32s Uint64s Float32s Float64s
Abs() V Y Y Y Y
Add(y V) V Y Y Y Y Y Y Y Y Y Y
AddSaturated(y V) V Y Y Y Y Y Y Y Y
Average(y V) V Y Y Y Y Y Y Y Y
Div(y V) V Y Y
IfElse(mask MaskWs, y V) V Y Y Y Y Y Y Y Y Y Y
Len() int Y Y Y Y Y Y Y Y Y Y
Masked(mask MaskWs) V Y Y Y Y Y Y Y Y Y Y
Max(y V) V Y Y Y Y Y Y Y Y Y Y
Min(y V) V Y Y Y Y Y Y Y Y Y Y
Mul(y V) V Y Y Y Y Y Y Y Y Y Y
MulAdd(y V, z V) V Y Y Y Y Y Y Y Y Y Y
Neg() V Y Y Y Y
Not() V Y Y Y Y
Or(y V) V Y Y Y Y Y Y Y Y
Sqrt() V Y Y
Sub(y V) V Y Y Y Y Y Y Y Y Y Y
SubSaturated(y V) V Y Y Y Y Y Y Y Y
Xor(y V) V Y Y Y Y Y Y Y Y
ブール演算とベクトルマスキング