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 vs AVX2 vs AVX‑512、NEON vs 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 で追加
}

ループは1回の反復で 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 が true の場所から y の要素を選択。
  • Masked(mask) – マスクが false の場所のランをゼロに。

ブール値とマスク

  • ベクタ上の And, Or, Xor, AndNot。
  • Mask 型(Mask8s, Mask16s, …)は And, Or, String, ToIntWs をサポート。

比較

  • Equal, NotEqual, Greater, GreaterEqual, Less, LessEqual – 同じ要素幅のマスクを生成。

変換とリシェイプ

  • ConvertToFloatW, ConvertToIntW, ConvertToUintW。
  • ToBits, ReshapeToUint*, BitsToFloatW, BitsToIntW – ゼロコストの再解釈。

シフトとローテート(整数ベクタのみ)

  • 16‑、32‑、64‑ビットラン用の ShiftAllLeft/Right, RotateAllLeft/Right。

注意: 初回リリースではジェネリックな還元(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) – 一部のオプション命令が欠けていてもその幅を許可;サポートされていない命令を使用するとランタイムでパニック。

実装ノート

コンパイラは、simd 型を含む関数のすべてについて、サイズ固有のコピーを生成する AST リライトを行います。これらのコピーは simd/internal/bridge にあり、具体的な archsimd 型(例:Int8x16)としてインスタンス化されます。ディスパッチはプログラム起動時に一度行われ、以降のホットループは特殊化されたバージョンをブランチなしで実行します。simd 型へのダミー参照(例:var _ simd.Uint64s)を追加することで、ディスパッチポイントを周囲のループに引き上げ、特殊化されたバージョンが使用されるようにできます。


コミュニティの反応(選択的 HN コメント)

「このケースではポータブル SIMD は非ポータブル SIMD より約 11 % 遅いが、両方とも非 SIMD より約 5 倍速い。」 – ImJasonH(wasm でのベンチマーク)

「C++ は std::simd を得ようとしている。私は、最小限のイントリンシックでベクタコードを書くことに賛成だ。」 – beached_whale

「誰もこれを求めたわけじゃないが、彼らは正しい方法で時間をかけて取り組み、Go をメモリ安全で高レベルなシステム言語として進化させ続けている。」 – u8

「SIMD をサポートする低レベルパフォーマンスを、すでにマルチコアで動いている Go プロジェクトに開く。少数の言語しか標準ライブラリに SIMD をサポートしていない。」 – qprofyeh

「インターフェース変換と型スイッチは非効率に見えるが、コンパイラがそれらを特殊化している。スイッチは反復ごとにではなく、起動時に解決される。」 – vlovich123


次に何が?

  • Go 1.28 では archsimd に SVE を追加し、最終的にはポータブル simd パッケージにも追加予定。
  • OnesCount、より豊富なマスクユーティリティ、還元プリミティブ、ベクタシャッフルなどの新しい操作が計画中。
  • 機能変異フラグにより、わずか数個のオプション命令が欠けているハードウェアでもコードを実行可能(例:PMULL をサポートしない Raspberry Pi の NEON)。
  • archsimd 内部の専用ブログ記事が近日公開予定。

最後に

Go の実験的 SIMD レイヤーは、アーキテクチャごとのアセンブリを書くという歴史的な障壁を排除し、サイズに依存しないクリーンな API を提供。さまざまな CPU でネイティブに近いパフォーマンスを発揮しつつ、すべてのプラットフォームで機能するフォールバックを保証します。この設計は C++ の Highway に類似しており、将来の拡張(例:SVE)を想定しており、高パフォーマンスでクロスプラットフォームなワークロードに最適な選択肢として Go をさらに魅力的にしています。

Sources

関連

  • プロジェクト
  • プロジェクト
  • プロジェクト
  • プロジェクト