SIMDって何?8つのOSSから学ぶ、速くする場所とデータの並べ方
前半から少しずつレベルを上げます。最初の章だけでも「SIMDってこういうものか」と分かるように書きました。途中で難しくなったら、そこで読み終えて大丈夫です。後半で分からない言葉が出てきたら、気になった箇所をあとでAIに聞きながら読むのもおすすめです。
1章:SIMDって何?まずは同じ作業をまとめて行う
1.1 いつもの計算をまとめてみる
4つの数にそれぞれ10を足すとします。
入力: 1 2 3 4
出力: 11 12 13 14
普通のプログラムでは、1個ずつ処理します。
for (int i = 0; i < 4; ++i) {
out[i] = in[i] + 10;
}
CPUには、複数の数をひとまとめにして足し算する命令があります。それを使う考え方がSIMDです。
通常: 1個目を足す → 2個目を足す → 3個目を足す → 4個目を足す
SIMD: [1, 2, 3, 4] + [10, 10, 10, 10]
「同じ命令を複数のデータへ同時に適用する」と覚えれば、最初は十分です。SIMDはSingle Instruction, Multiple Dataの略で、ベクトル化と呼ばれたりもします。Intelのベクトル化入門にも、この考え方が簡潔に説明されています。
1.2 ごく単純な使い方
SIMDを直接使うC++の例を、雰囲気が伝わる形で書くとこうなります。実際の型や関数名はCPU命令セットや使うライブラリによって異なります。
// 説明用の疑似コード: 4個のfloatをまとめて扱う
vector4 a = load4(input); // input[0..3] をまとめて読む
vector4 b = add(a, splat(10)); // 4つ全部に10を足す
store4(output, b); // 4個まとめて書く
ここでのvector4は説明用です。実際のC++ではCPU命令を直接扱う型や、SIMDを抽象化するライブラリの型を使います。基本は「まとめて読む、まとめて計算する、まとめて書く」です。
SIMDでまとめる個数はいつも4個とは限りません。CPUによって一度に扱える数や命令が異なります。そこで後半では、幅を抽象化する方法や、CPUに合う実装を選ぶ方法を見ます。
1.3 なぜ速くなる?どれくらい速くなる?
通常の計算では、CPUは1回の命令で少数の値を処理します。SIMD命令は、1つの命令で複数の値(レーン)に同じ演算を行うため、たとえば4レーンなら、条件が揃えば1命令で4個分の加算を進められます。ここで速くなる理由は「仕事を4回分まとめて実行し、命令の回数を減らせる」ことです。
ただし、4レーンだからプログラム全体が4倍速くなる、とは限りません。SIMDにできない処理やメモリアクセス、データの並べ替え、端数処理などは残ります。仮に実行時間の半分だけがSIMD化でき、その部分が8倍速くなったとしても、全体の改善は2倍未満です。
期待できる速さは、対象カーネル、データ量、CPU、コンパイラによって変わります。理論上のレーン数ぶんの向上は、条件のよい部分処理に対する上限の目安です。アプリ全体では、効果がほとんどない場合から数倍になる場合まで幅があります。Intelもベクトル化の説明で、理論値と実アプリの速度向上を区別し、測定を勧めています(ベクトル化の効果を認識・測定する、Amdahlの法則を含む性能分析手法)。
1.4 どんな作業がまとめやすい?
画像の全画素を少し明るくする、数千個の数を足し合わせる、文字列から特定の文字を探す、といった処理は同じルールを大量のデータへ適用します。
一方、1個目の結果を見て2個目の処理が決まる仕事や、データごとに行き先が違う仕事は、そのままではまとめにくくなります。コンパイラの自動ベクトル化も、ループの依存関係やメモリ配置を見て安全性と採算性を判断します。LLVMのベクトル化解説では、ループと連続命令列に対する方式を紹介しています。
ここまでのまとめ
SIMDとは、同じ計算を複数のデータへまとめて適用するCPUの仕組みです。「まとめて読む・計算する・書く」と理解すれば、まずは十分です。速さの目安はレーン数だけで決まらず、SIMD化できる処理の割合とデータの扱いにも左右されます。
今回取り上げるOSS
- Google Highway:C++のSIMD抽象化ライブラリです。
- SimSIMD:C/C++を中心にベクトル距離・内積を高速計算します。
- pulp:RustのポータブルSIMDライブラリです。
- OpenCV.js:C++製OpenCVをJavaScript/WebAssemblyから使う仕組みです。
- Pillow-SIMD:C拡張で画像処理を高速化するPython画像ライブラリです。
- simdjson:C++の高速JSONパーサーです。
- SwissTable(Abseil):C++のハッシュテーブル設計・実装です。
- Intel Embree:C++を中心とするレイトレーシング用カーネル群です。
2章:SIMD、GPU、それ以外。どの高速化を考える?
2.1 SIMDを試す価値がある処理
次のような処理はSIMDの候補です。
- 画像や音声を先頭から同じ式で処理している
- 配列同士を足す、掛ける、比べる
- ベクトルの内積や距離を何度も計算する
- バイト列から特定の文字やビットパターンを探す
- たくさんのレイや図形に同じ交差計算を行う
目安は「同じ処理を、連続したデータに、何度も行っているか」です。
2.2 遅さの原因によって別の改善を選ぶ
SIMDは万能の高速化手段ではありません。遅さの原因を見極めることが大事です。次の表は、遅さの原因と、先に考えるべき改善策の例です。
| 遅さの原因・仕事の形 | 先に考えること |
|---|---|
| 毎回メモリを確保している | 使い回す、まとめて確保する |
| データベースやネットワークを待っている | 問い合わせ回数、通信量、キャッシュを見直す |
| 同じ結果を何度も計算している | キャッシュ化を考える |
| 一つの計算が前の結果を待つ | アルゴリズムを変え、依存をほどく |
| 多数の独立した計算を長時間続けられる | GPUやスレッド並列を検討する |
| データがメモリ上に散らばっている | 配列化、インデックス化、アクセスを整理する |
SIMDはCPUの計算命令をまとめる手段です。ネットワーク待ち、ファイル読み書き、不要な処理を直接解決するものではありません。
GPUが有力になりやすい仕事
GPUは多数の計算を同時に走らせ、全体の処理量を稼ぐ装置です。グラフィックス、巨大な行列・テンソル計算、機械学習の学習や推論、大きな画像・動画の一括処理が代表例です。入力が大きく、GPU上にデータを置いたまま何段もの計算を続けられるほど、GPUの処理量を活かしやすくなります。
ただし、「この分野ならどんな入力でも必ずGPUが速い」とは言えません。同じ行列計算でも、サイズ、GPUやCPUの世代、ライブラリ、データ転送の有無で結果は変わります。CPUは少数の処理を低い待ち時間で進め、GPUは多数の軽い処理で総処理量を伸ばす設計、と捉えるとよいでしょう。
CPUでもGPUでも実装できる仕事
中規模の画像処理、数値シミュレーション、音声処理、データ分析などは、条件によってCPUでもGPUでも実装できます。計算だけでなく、データをどこに置くかも考えます。
CPU上のデータ ──転送──> GPU ──計算──> CPUへ結果を戻す
少量のデータをGPUへ送り、すぐCPUへ戻すと、転送や起動にかかる時間が計算を上回ることがあります。最初にまとめてGPUへ送り、複数の処理をGPU上で続けてから結果を戻せるなら、転送コストを薄めやすくなります。CUDA公式ガイドも、ホストとGPU間の転送を減らし、小さな転送をまとめるよう勧めています。CUDA C++ Best Practices Guide
GPUを用意しにくい環境ではCPU SIMDが候補になる
サーバー構成、組み込み機器、ブラウザ、配布先PC、コストや電力の制約によって、GPUを前提にできないことがあります。その場合、CPUの複数コアを使う並列化やSIMDが候補です。GPUのないサーバーで大量のJSONを読む、CPU上で画像を少量ずつ加工する、といった例があります。simdjsonやPillow-SIMDは、CPUでデータ並列性を使う設計例です。
GPUがある分野でもCPU SIMDは役立ちます。前処理、短いデータ、GPU転送前の変換など、CPU側で完結する処理に使えます。GPUとCPU SIMDは、処理段階ごとに分担できます。
CPU SIMDが実用上有利になりやすい場面
「SIMDがGPUより常に速い分野」とは決められません。ただし、次の条件ではCPU SIMDが有力になりやすいです。
- 数個から小さな塊のデータをすぐ処理して返す。GPUへの転送・起動コストを回収しにくい。
- CPUメモリ上の小さなデータを一度だけ処理する。計算量より転送量が目立ちやすい。
- CPU上で分岐や検索を続ける。SwissTableのような検索では、CPU SIMDで候補を絞り、候補キーをCPU上ですぐ確認できる。
- GPUがない、またはGPU利用が許されない環境。
小さなJSONを1件ずつ読む処理や、短いリアルタイム音声処理も、低遅延が大事でデータがCPU上にあるならCPU SIMDが有力です。処理を大量にまとめてGPU上で続けられる場合には結果が変わり得ます。CPUは少数処理の遅延を抑え、GPUは多数の処理量を伸ばすという違いと、CPU/GPU間の転送コストを踏まえて比べます。CUDA C++ Best Practices Guide
2.3 迷ったら、まず測る
「遅い」と感じても、原因が計算とは限りません。まず処理時間を測り、どの関数に時間を使っているか調べます。SIMDやGPUのためのデータ変換が、計算時間の節約を上回る場合もあります。比較するときは、データ準備、転送、起動、計算、後処理まで含めます。
ここまでのまとめ
大量の同型演算ならSIMDやGPUを候補にします。GPUは大きな仕事をまとめて処理できると有利になりやすく、少量・低遅延でCPU上にデータがあるならCPU SIMDが有力です。待ち時間や確保が主因なら、別の改善を先に考えます。
3章:SIMDのためにデータをどう並べる?
ここからは、実際にSIMDを学びたい方向けに進みます。CPUがまとめて複数の値を計算できても、値がメモリ上で離れていたらまとめて読む準備が必要です。
3.1 すでに連続しているデータ
配列のように同じ種類の値が続いている形は、SIMDと相性がよいです。
float values[] = { ... } → [x0][x1][x2][x3][x4][x5]...
画像の1行、音声サンプル、数値配列が例です。Google Highway、pulp、SimSIMDの基本カーネルは、このような連続配列をよく扱います。
3.2 フィールドを分ける(AoSからSoAへ)
位置と速度を持つ粒子を、次のように持っているとします。
struct Particle { float x, y, z, vx, vy, vz; };
Particle p[N];
xだけを計算するとき、xの値はメモリ上で飛び飛びです。フィールドごとに配列を分けると、同じ値を続けて読めます。
struct Particles {
float x[N], y[N], z[N];
float vx[N], vy[N], vz[N];
};
前者はAoS(Array of Structures、構造体の配列)、後者はSoA(Structure of Arrays、配列の構造体)です。大切なのは、同じ計算に使う値が隣り合っているかです。SoAへの変換時間や、ほかの処理で扱いにくくなるコストも含めて判断します。
3.3 値を小さくして、一命令に多く詰める
画素は赤・緑・青・透明度をそれぞれ8ビットで持つことがあります。1要素が小さいので、1命令に多くの画素を載せられます。
数値計算でも、元データをint8やfloat16で保存し、計算時にint16やfloat32へ広げて積和する方法があります。SimSIMDは型ごとのカーネルや累積型を使い分け、Pillow-SIMDは画素をunpack(広げる)、計算し、pack(詰め直す)処理を使います。
詰めれば必ず速いわけではありません。値の範囲、丸め誤差、変換コストを確認します。
3.4 SIMD用の中間データを作る
データをそのまま並べ替えにくい場合、SIMDで扱いやすい中間表現を作る方法があります。
- simdjsonは入力をブロックに分け、構造文字の位置をbitmaskやindexにします。
- SwissTable(Abseil)は値本体と別に制御byteを持ち、候補slotを先に絞ります。
- Intel Embreeはレイの属性をレーンに並べた内部表現やBVHを交差計算へ渡します。
中間データを作ると準備、メモリ使用量、後処理が増えます。SIMDカーネルだけでなく処理全体を測ります。4章では、これらの考え方を実装パターンに分けて見ます。
4章:SIMDを実装する方法は一つではない
ここでは「コードをどの程度CPU命令に近づけて書くか」で整理します。まず幅を抽象化する方法、次に命令を直接使う方法、最後に言語・実行環境の境界を見ます。SIMDの対象と、命令の書き方は別の判断です。
4.1 幅を抽象化し、SIMDを狙う:Google Highwayとpulp
Google Highway(C++)とpulp(Rust)は、CPU命令名を毎行書かず、ロード、加算、FMA、maskなどの操作でカーネルを書けるようにします。Highwayはタグと関数で幅・ISAを扱い、pulpはtraitを使ってSIMD実装を渡し、sliceのベクトル部分と末尾部分を分けます。
for each block of N values:
load N values
add N values
store N results
process the remaining values
同じアルゴリズムを別のCPU向けに展開しやすくなります。複数CPUで動かし、長く保守する汎用カーネルに向きます。ただし抽象化しただけで必ずSIMDになるわけではありません。連続データ、コンパイラが理解できるループ、末尾処理が必要です。
4.2 intrinsicで命令を直接選ぶ:SimSIMDとPillow-SIMD
SimSIMD(C/C++)は距離や内積、Pillow-SIMD(C)は画像のホットループに対象を絞ります。AVX2、SSE、NEONなどのintrinsic(組み込み関数)を明示し、ロード、比較、積和、shuffle、packを組み合わせます。
// AVX2でfloat32を8要素ずつ内積する考え方
__m256 acc = _mm256_setzero_ps();
for (size_t i = 0; i + 8 <= n; i += 8) {
__m256 a8 = _mm256_loadu_ps(a + i);
__m256 b8 = _mm256_loadu_ps(b + i);
acc = _mm256_fmadd_ps(a8, b8, acc);
}
直接書くと命令やデータの詰め方を制御できます。その分、CPUごとの実装、fallback、末尾処理、正しさの確認が必要です。対象CPUと処理が明確で、プロファイル上重要だと分かった後に向きます。
4.3 WebAssembly:OpenCV.jsとWasm SIMD
OpenCV.js(OpenCV公式リポジトリ)では、JavaScriptから画像データを扱いますが、画像処理カーネルの多くはC++側にあります。WebAssembly SIMDを使えるビルド・実行環境なら、C++のHAL(ハードウェア差を吸収する層)からWasm SIMDへつながります。
WASM SIMDを直接書く例(C)
WebAssembly SIMDは128ビット幅のベクトル命令を提供します。EmscriptenではC/C++からwasm_simd128.hを使い、-msimd128を付けてコンパイルできます。
#include <wasm_simd128.h>
#include <stddef.h>
void add_f32(const float* a, const float* b, float* out, size_t n) {
size_t i = 0;
for (; i + 4 <= n; i += 4) {
v128_t va = wasm_v128_load(a + i);
v128_t vb = wasm_v128_load(b + i);
v128_t sum = wasm_f32x4_add(va, vb);
wasm_v128_store(out + i, sum);
}
for (; i < n; ++i) out[i] = a[i] + b[i];
}
ビルド例はemcc add.c -O3 -msimd128 -o add.jsです。生成されたJavaScript glue codeとWasmモジュールをブラウザやNode.jsで動かします。ブラウザ側の対応とfallbackの要否も確認します。Emscriptenは通常のC/C++ループを自動ベクトル化する方法も用意しているため、intrinsicを必ず書く必要はありません。Emscripten: Using SIMD with WebAssembly
画像カーネル自体が速くても、JavaScriptとの受け渡し、データコピー、Matの確保が支配的なら全体は速くなりません。Web向けではブラウザ、ビルド設定、メモリ境界も設計の一部です。
4.4 Python:NumPyやNumbaからCPU SIMDを使う
Pythonでは通常、SIMDレジスタを直接操作せず、配列ライブラリやJITコンパイラを通して利用します。
import numpy as np
a = np.arange(1_000_000, dtype=np.float32)
b = np.full_like(a, 10.0)
out = np.empty_like(a)
np.add(a, b, out=out)
Pythonコードは配列演算を指定し、SIMDを実行するのはNumPy内部のカーネルです。実際の命令はNumPyのビルド、CPU、型、入力サイズに依存します。NumPy公式資料は、CPU機能に応じた複数カーネルと実行時dispatchを説明しています。NumPy CPU/SIMD optimizations
Numbaでは、Python関数をCPU向けufuncへコンパイルできます。
import numpy as np
from numba import vectorize, float32
@vectorize([float32(float32, float32)], target="cpu")
def add_one_pair(x, y):
return x + y
out = add_one_pair(a, b)
NumbaもSIMD命令を直接指定する書き方ではなく、コンパイラへ最適化を任せます。SIMD命令が生成されたか、速くなったかは環境に依存するため確認が必要です。Numba vectorize documentation
4章のまとめ
抽象化、intrinsic、Wasm、Python配列APIは、SIMDの書き方が異なります。どれを選んでも、データ配置、処理量、末尾、呼び出し境界を考える必要があります。次章では、元のアルゴリズム自体をSIMD向きへ組み替える例を見ます。
5章:一見SIMDにしにくい仕事をどう組み替える?
SIMDは、数値配列だけに使うものではありません。処理全体をそのままベクトル化できなくても、候補をまとめて判定する部分を作る方法があります。
5.1 文字を分類してから文法を読む:simdjson
simdjsonは入力文字をブロック単位で分類し、JSONの構造文字候補をbitmaskやindexにします。その後のstageで文法と値を解釈します。
SIMDが効くのは文字分類、UTF-8検査、構造位置の抽出です。文法の依存関係、数値変換、文字列の利用まで一度にSIMDになるわけではありません。入力paddingや中間indexを用意する分のコストもあります。詳しい前段・stage 1・後段・stage 2の流れはAppendix Aで説明します。
5.2 キーの前にmetadataを見る:SwissTable
SwissTable(Abseil)は値とは別に制御byte列を持ち、ハッシュの一部で候補を絞ります。SIMDが効くのは、連続した制御byte群の比較と状態判定です。ハッシュ計算、probeの制御、候補に対する実際のoperator==は残ります。
つまり、キー比較そのものをSIMDで速くするより、キー比較を行う候補数を減らします。Appendix Bでは、検索の前段・SIMD段階・後段に分けて説明します。
5.3 数式は揃え、探索の発散は別に扱う:Intel Embree
Intel Embreeはレイトレーシング向けに、レイと境界箱・三角形などの交差式をベクトル型で計算します。laneごとの有効・無効をmaskで保ち、SSEやAVXなどCPU機能に応じたカーネルを選びます。
SIMDに向くのは交差判定の数式です。レイごとにBVHの違う枝へ進む探索、ポインタ追跡、ユーザーcallbackはレーンの動きが揃いにくく、SIMDの効率を落とします。数式カーネルと、そこへたどり着く探索の両方を設計します。
5章のまとめ
simdjsonとSwissTableは、SIMD化しにくそうな処理の一部を見つけ、候補抽出や中間表現を設けています。Embreeは計算をベクトル化しながら、不規則な探索の限界を扱います。SIMDの対象を見つけるために、データ構造や処理順序を変えるのも設計手段です。
6章:直接書く?抽象化に任せる?
どれか一つが常に正解ではありません。
| 方針 | 合う状況 | 負担 |
|---|---|---|
| 高水準のSIMD抽象(Highway、pulpの考え方) | 複数CPUで動かしたい、長く保守する汎用カーネル | 抽象APIやビルド構成を理解する |
| コンパイラにベクトル化を促す通常ループ | 単純な配列ループで、まず低コストに試したい | 生成命令を測定・確認する |
| intrinsicを直接使う | 対象CPUとホットカーネルが明確で、細部を制御したい | ISAごとの実装、fallback、保守が増える |
| データ構造・処理段階から変える | 分岐、検索、メモリ配置が性能を決める | 変換や中間データ、API設計も変わる |
新しいプログラムなら、まず正しいscalar版とテストを用意し、処理時間を測ります。単純なループはコンパイラの自動ベクトル化を試します。複数ISA対応が必要なら抽象化を検討し、特定カーネルが支配的だと分かったらintrinsicを検討します。候補を減らせるなら、SwissTableやsimdjsonのようにデータ構造や処理段階を変える方が効く場合もあります。
7章:新しいプログラムを設計するときのチェックポイント
- どの関数が実際に遅いか測ったか
- 同じ処理を繰り返す部分があるか
- データは連続しているか。必要ならSoAや中間表現へ変えられるか
- 1要素を小さくするなら、値の範囲・精度・overflowを許容できるか
- SIMDの末尾部分(tail)をどう処理するか
- CPUが違っても動くfallbackがあるか
- データ変換やdispatchを含む全体時間を測ったか
- 小さい入力、ランダム入力、分岐が偏る入力も測ったか
8章:まとめ
SIMDは、同じ計算を複数のデータへまとめて行う仕組みです。まずは配列加算で「まとめて読む・計算する・書く」と理解すれば十分です。
使いどころは、処理の形と遅さの原因から考えます。大量の同型演算ならSIMDやGPUを候補にし、GPUは大きな仕事をまとめて実行できると有利になりやすく、少量・低遅延でCPUにデータがあるならCPU SIMDが有力です。待ち時間や確保が主因なら別の改善を考えます。
深く学ぶなら、データの並べ方、候補の絞り方、CPU差の吸収、SIMD化しない境界まで設計します。8つのOSSの実例が示すのは、命令だけでなく、SIMDが働けるデータと処理の形を作る工夫です。
Appendix:上級者向け解説とコード
本文ではパターンと要点を紹介しました。このAppendixでは、simdjsonとSwissTableについて、前段・SIMD処理・後段を含む全体の流れを追います。続けて、抽象化カーネルやlane maskのコード例を載せます。コードは要約・短縮例です。再利用時は各OSSのライセンスと周辺コードを確認してください。
Appendix A:simdjson―前段・SIMDカーネル・後段
JSON parserは、文字列の内側か外側か、escape文字か、といった状態に依存するので、全体を一度にSIMD化するのは難しい処理です。simdjsonは、処理を分類と文法解釈に分けます。
入力準備: padded buffer(末尾の安全な読み出し領域)
↓
前段 / stage 1
blockをload
quote・backslash・空白・構造文字を分類
文字列内外・escape状態をmask/carryで更新
UTF-8を検査
構造位置をindex配列へ書く
↓
後段 / stage 2
indexを順に読み、JSONの文法と値を解釈
DOMならtape/値を作る。On-Demandなら要求された値を読む
stage 1は、複数byteの分類やUTF-8検査、bitmask生成をまとめて行えるためSIMD向きです。block境界をまたぐquoteやescapeの状態はcarryとして次blockへ渡します。SIMD maskはそのまま構文木にならず、構造位置のindexへ変換されます。
stage 2はindexを逐次消費し、ネスト、数値、文字列など依存関係の強い文法を解釈します。DOMではtapeや値の格納も必要です。On-Demand APIは全体を先に構築せず、要求された値を読むことで後段の作業を抑えます。したがって速度の設計点はstage 1だけでなく、「並列分類できる前段」と「逐次性が残る後段」の境界にもあります。simdjson parse_many設計資料
Appendix B:SwissTable―hash・候補抽出・キー確認
ハッシュ表検索は、hashを計算し、衝突先を探し、キーの等価性を確認します。SwissTableは検索全体をSIMD化するのでなく、逐次検索の中に制御byte群を一括で見る段階を入れています。
前段(1キー)
hash(key)を計算
H1でprobe開始位置、H2で候補判定byteを作る
現在のprobe groupを選ぶ
↓
SIMD段階(group単位)
制御byte群をload
H2一致位置を比較してcandidate bitmaskを作る
empty/deleted位置のmaskも作る
↓
後段(候補slot)
maskの立ったslotだけkey equality
一致すれば返す
なければemptyで終了、または次groupをprobe
SIMDが効くのは制御byteの連続比較と状態判定です。hash計算やprobe group選択は通常スカラーです。後段の候補ごとのoperator==もスカラーで、文字列なら長さや内容に応じた比較になります。ただしH2で候補を先に絞るため、多くのslotは重い比較へ進みません。
制御byteの符号bitやempty/deleted値も比較とmask生成を意識して設計されています。大事なのはキー比較自体をSIMD化することではなく、キー比較を必要とする候補を少なくすることです。Abseil Swiss Tables設計資料
Appendix C:Google Highway―幅に依存しない積和
言語:C++(Google Highway API)
template <typename T>
HWY_INLINE T DotProductSIMD(const T* HWY_RESTRICT a,
const T* HWY_RESTRICT b, size_t n) {
using D = hn::ScalableTag<T>;
const D d;
using V = hn::Vec<D>;
const size_t N = hn::Lanes(d);
V sum = hn::Zero(d);
size_t i = 0;
for (; i + N <= n; i += N)
sum = hn::MulAdd(hn::Load(d, a + i), hn::Load(d, b + i), sum);
if (i < n)
sum = hn::MulAdd(hn::LoadN(d, a + i, n - i),
hn::LoadN(d, b + i, n - i), sum);
return hn::ReduceSum(d, sum);
}
Appendix D:pulp―Rust traitでSIMD処理を渡す
言語:Rust
use pulp::{Arch, Simd, WithSimd};
struct TimesThree<'a>(&'a mut [f64]);
impl<'a> WithSimd for TimesThree<'a> {
type Output = ();
#[inline(always)]
fn with_simd<S: Simd>(self, simd: S) {
let (head, tail) = S::as_mut_simd_f64s(self.0);
let three = simd.splat_f64s(3.0);
for x in head { *x = simd.mul_f64s(three, *x); }
for x in tail { *x *= 3.0; }
}
}
fn scale(v: &mut [f64]) { Arch::new().dispatch(TimesThree(v)); }
Appendix E:SwissTable―制御byteから候補maskを得る
言語:C++(SSE2 intrinsicを使った短縮例)
BitMaskType Match(h2_t hash) const {
auto wanted = _mm_set1_epi8(static_cast<char>(hash));
auto equal = _mm_cmpeq_epi8(wanted, ctrl); // 16 byteを並列比較
return BitMaskType(_mm_movemask_epi8(equal));
}
Appendix F:Embree―lane maskを保つ交差判定
言語:C++(Embreeのベクトル型を使った短縮例)
vbool<M> valid = valid0;
const vfloat<M> den = dot(tri_Ng, ray_dir);
const vfloat<M> U = dot(cross(tri_v0 - ray_org, ray_dir), tri_e2);
const vfloat<M> V = dot(cross(tri_v0 - ray_org, ray_dir), tri_e1);
valid &= (den != vfloat<M>(0.0f)) & (U >= 0.0f) &
(V >= 0.0f) & (U + V <= abs(den));
if (none(valid)) return false;
実コードでは符号処理、tnear/tfar判定、hit情報、filterも扱います。上記はlaneごとの有効判定を示す短縮例です。