世界を動かす技術を、日本語で。

誰もが知っておくべきSIMD

2026年7月23日原文(mitchellh.com)

概要

  • SIMD は複雑そうに見えるが、基本形を覚えれば forループと同じ感覚 で書ける
  • 大規模データ処理 で劇的な高速化が可能、日常的なプログラミングにも有効
  • 一般的なSIMDコードは 5つの共通ステップ で構成
  • Zig での例を用いるが、他言語にも応用可能
  • 自動ベクトル化は万能ではなく、 手動SIMD実装の価値 は高い

SIMDは本当に難しいのか?

  • SIMD (Single Instruction, Multiple Data)は複雑だと誤解されがち
  • 多くの優秀なエンジニアが「高性能ソフト専用のニッチ最適化」として敬遠
  • 実際は 基本形 を覚えれば、forループと同じくらい自然に書ける
  • 「N個まとめて処理」するループであれば、ほとんど同じパターンで高速化可能
  • 難しい場合は無理せずスキップする判断も大切
  • 全ての開発者 が最低限のSIMD知識を持つべき

SIMDとは何か?

  • CPUが複数の値を同時に処理 できる命令セット
  • 例:1バイトずつ比較→4/8/16バイトずつ比較に変換可能
  • ループで大量データ処理時に 劇的な高速化 が可能
  • 数十バイト程度なら効果は薄いが、 数百・数千・数百万バイト なら大きな恩恵
  • simdutfやsimdjsonのような高度なアルゴリズムは不要、 基本形だけで十分効果

SIMDコードの共通パターン

  • 一般的な「N個まとめて処理」SIMDコードは 5ステップ で構成
    • 定数のベクトル化・ベクトル型の初期化
    • 入力をベクトル幅ずつループ処理
    • 各レーンで並列演算
    • 結果のリダクション(集約)や保存
    • 端数を通常ループ(スカラーテイル)で処理

実例:GhosttyでのSIMD化

  • 目的 :0xF以下の値(C0制御文字)が出るまで処理を進めるループ
  • スカラーループ例:
    • while (end < cps.len and cps[end] > 0xF) end += 1;
  • SIMD化例(Zig):
    • ベクトル幅分だけまとめて比較し、最初に条件を満たさない位置を特定
    • ARM NEONなら4倍、AVX2なら8倍、AVX-512なら16倍の並列処理
    • 実際のスループットは5倍程度向上

5ステップの詳細

  • 1. 定数のベクトル化
    • SIMD演算には「全レーン同値」の定数ベクトルが必要
    • 例:@splat(0xF)で全レーンに0xFをセット
  • 2. ベクトル幅でループ
    • 入力配列をベクトル幅(例:8)ずつループ
    • 端数は後述のスカラーテイルで処理
  • 3. SIMD演算の実行
    • 例:values > thresholdで全レーン並列比較
    • 加算・乗算・最小値なども同様に書ける
  • 4. 結果のリダクション
    • 例:全て条件を満たす場合は次のベクトルへ
    • どこか失敗したらビットマスクで最初の失敗位置を特定
    • @reduce, @bitCast, @ctzなどで集約・位置特定
  • 5. スカラーテイル処理
    • 残り要素は元のスカラーループで処理
    • SIMD非対応CPUでもこの部分がフォールバックになる

まとめ:SIMDコードの基本形

  • 定数ベクトル化

  • ベクトル幅でループ

  • 並列演算

  • リダクション・集約

  • スカラーテイル

  • ステップ4(リダクション)はアルゴリズムごとに変化

  • それ以外は様々な用途でほぼ同じ形

  • forループを見かけたら、 この5ステップに分解 してSIMD化を検討

なぜコンパイラが自動でやらないのか?

  • 一部の単純なループ では自動ベクトル化が可能
  • しかし複雑な制御やパターンは自動化が困難
  • コンパイラの自動ベクトル化は 長年の研究課題 だが、実用ではまだ不十分
  • 手動SIMD実装 なら動作が明確で予測しやすい
  • 本当に高速化が必要な場合は 明示的なSIMDコード が推奨

全ての開発者 がこの「5ステップの基本形」を知っておけば、 日常的なプログラムの高速化 に役立つ知識となる。

Hackerたちの意見

これは、Casey Muratoriが作ったゲーム『The Witness』の開発チームのために、SIMDを活用して具体的なパフォーマンス問題を解決する方法についての役立つ動画だよ。https://www.youtube.com/watch?v=Ge3aKEmZcqY

SIMDは好きなんだけど、SIMDでコードを超最適化する前に、データ構造やアクセスパターンをちゃんと考えた方がいいよ。データ指向設計の良さをずっと語ってきたから、ここにコメントをまとめておくね [1]。最適化にはいいアプローチだと思う。昔のコード(Zigで書いたやつ)でSIMDをいじってみたけど、データ構造のモデル化が最適化とは真逆だったから、壊れたエンジンのレモンに高性能なレースタイヤを履かせるようなもんだった。パフォーマンスを測ってなかったし、アロケーションの場所についても考えてなかったから、まさに「早すぎる最適化」の典型だった。今は、データをSQLのテーブルのようにモデル化して、潜在的な「プライマリキー」を考えたり、アクセスパターンに基づいてデータ構造を構築するようにしてる。例えば、昔は木構造をヒープ上の他の構造体を指す構造体としてモデル化してた:struct Tree { tag: TreeTag, children: Vec } 今は、木構造がリンクリストの悪い特性(* nノード * m子ノード)を持っていて、複数のヒープベクタの断片化(* nノード)や、セットアップ/ティアダウンの時間がひどい(この場合、Dropだけでかなりのランタイムを消費してた)。でも、木構造は色んな方法で表現できるし、常に線形化できるから、今は木のアクセス/挿入パターンを本当に考えてる。木なのか他の種類のグラフなのか、VecやVecの構造体に格納できるかどうかとか。アクセスパターンや「プライマリキー」を通して物事を見直すことで、コードがずっと速くてシンプルになった。これによって、データが均一な配列やベクタに収束するから、コンパイラがSIMDの魔法を使えるし、CPUがL1キャッシュから何百万倍も速く読み込めるようになる。で、SIMDを自分で使う必要が出てきた時には、素晴らしいブランチレスコードが書けるようになるよ。1. https://hn.algolia.com/?dateRange=all&page=0&prefix=true&que...

うん、データレイアウトやキャッシュを意識したレイアウトは、SIMDでホットループを速くするためには本当に重要だよ。それに、実際に「ホット」な部分のコードでアロケーションやvtableのルックアップ、たくさんの間接参照を避けることもすごく大事。C++のベクタは、予期しないアロケーションを呼び出す可能性がある場合には、必ずしも最適な選択肢じゃないからね。

これ。テーブルは一般的なグラフの効率的な実装なんだ。知ってる中では一番いい方法だと思う(もしグラフが特化できるなら別だけど)。

ちなみにGoプログラミングについてだけど、最後にSIMDでGoコードを最適化しようとした時、いくつかの異なるオプションがあったけど、どれもメンテナンスされてなかったり、サポートが不完全で、最初にC++でインストリンシックを使って関数を書いてアセンブリを生成してから、それをGoのアセンブリに変換するツールが必要だったんだ [1]。C++のコードは問題なく動いてたのに、Goでは関数が動かなかった。要するに、Goには本当にプロダクション向けのオプションじゃなかったってこと。これ、1、2年前の話だけど。編集すると、今はhttps://go.dev/pkg/simd/archsimd/に実験的な公式ライブラリがあるみたい。https://go.dev/doc/go1.26#simdやhttps://github.com/golang/go/issues/78902を見てみて。前回試した時から状況が変わったみたいだね。 [1] https://github.com/minio/c2goasm

すべての開発者は…最も重要なのは、SIMDを怖がらないことだね。 彼はRaph LevianやLinebenderの人たちが作ったRustのクレート「fearless_simd」を勧めるべきだと思う :) もっと真面目に言うと、RustコードにSIMDを追加したいなら、それがスタート地点だよ。 [1] https://crates.io/crates/fearless_simd

99%の開発者はSIMDを無視した方がいいと思う。ほとんどのプロジェクトにはパフォーマンスを向上させるための簡単な手がかりがたくさんあるのに、誰もそれを解決する時間を見つけられないんだよね。

それは、新しいコードに対して遅い実装をデフォルトにするべきだという意味じゃないよ。そうすると、誰も解決する時間を見つけられないような低いハンギングフルーツを増やすだけだから。

これは面白い記事だね。私はあまり低レベルな言語を使ってないから、これが重要になることはないけど(もしかして、これがJavascriptに現れることはあるのかな?)。「reduce」ステップがよくわからない。SIMDの利点を「元に戻す」ことに気をつけなきゃいけないみたいだね。確かに、8つの値を並列で比較できるけど、8つの答えを順番に見なきゃいけないなら、元に戻っちゃう。例の@reduce()関数は、すべての値が真かどうかを一つの「ステップ」で教えてくれる特別なベクタ用のものなの?

うん。 "@reduce(.And, ... )は、すべてのブール値をandで組み合わせて、単一のブール値を返すよ。" もしそれが真なら(ここでは一般的なケース)、次の8バイトを見ることになる。もしそれが偽なら、@bitcast(ブール値をビットに変換)と@ctz(最初の0を見つける)を適用して、どこが偽だったかのインデックスを取得するんだ。

誰もがSIMDを知る必要はないよ。機械的な共感は、ソフトウェアアーキテクトにとって重要なパッシブな特典で、後々のリワークを減らすのに役立つけど、ベンチマーキングやボトルネックを特定する能力の方が、日常的にはもっと重要なスキルだと思う。今、PlayStation用のボクセルスペースレンダラーを自作してるんだけど、レンダリングに使えるサイクルは限られてるから、スライドショーにならないように、ホットレンダリングループでサイクルを数えて、できるだけ並列処理してる。メインRAMからのメモリロードの待機時間を跨いでもね。STM32 MCUを使った基本的なネットワークアクセサリーカードも作ったけど、必要以上にオーバースペックだった。パフォーマンスやメモリの最適化は全く気にせず、まるでサーバークラスのハードウェアで動かしてるかのように、普通のC++を書いてた。最初に考えるべきは、何かがSIMDを活用できるかどうかじゃなくて、パフォーマンス要件が満たされているかどうかだよ(自分のハードウェアが苦しんでないと、あまり気にしなくなるのが簡単だけど…)。

機械的な共感は、ソフトウェアアーキテクトにとって重要なパッシブな特典で、後々のリワークを減らすのに役立つ ちょっと違うレベルの話をしてるかもしれないけど、「SIMDを使うべきかどうか」の反対側の話になると、データベースにおける基礎的な機械的共感はやっぱり重要だよ。例えば、行ごとの更新とバッチ処理、あるいは21kエントリーのクローズがカラムのユニコードルールを忘れてインデックスを壊すような悪いロジックとかね[0]…これは本当に重要だよ。[0] - それは実際にあった話で、怠けたボディショップがコパイロットを使わせてるのに、同じ請求時間で何も早く進まないし、経営陣は注意を払うのが馬鹿すぎる…

Hacker Newsで議論の続きを見る