キャッシュ・分岐予測・SIMD¶
現代CPUの性能は「計算の速さ」ではなく「データと命令をいかに待たずに供給し続けるか」で決まります。ゲームの最適化に直結する3つのハードウェア機構を扱います。数値は典型的なオーダーで、実際のCPUごとに異なります(実装依存)。
キャッシュ¶
階層とコスト感覚¶
レジスタ ~0 サイクル
L1キャッシュ ~4 サイクル (32〜64KB / コア)
L2 ~12 サイクル (256KB〜1MB / コア)
L3 ~40 サイクル (数MB〜数十MB / 共有)
メインメモリ ~200+ サイクル ← L1の50倍以上遅い!
- メモリはキャッシュライン(典型64バイト)単位で読まれる。1バイト読むと隣の63バイトも載る——隣接データは実質タダ
- プリフェッチ: 連続アクセスのパターンをハードが検出し、先読みする——配列の順走査が最強である理由
ゲームコードへの帰結¶
// 遅い: ポインタの配列(実体はヒープに散在)→ 1体ごとにキャッシュミス(~200サイクル)
std::vector<Enemy*> enemies;
// 速い: 実体の連続配列 → ライン単位でまとめて載り、プリフェッチも効く
std::vector<Enemy> enemies;
// さらに: Updateで使うメンバだけを詰める(ホットデータ分離)→ 1ラインに載る個体数が増える
これがData Locality、AoS/SoA、ECSの物理的根拠です。「リストよりvector」「小さい構造体」「使うものを近くに」——第4部・第7部の最適化パターンはすべてこのページの応用。
- 命令キャッシュも同じ: コードが大きい(過剰なインライン化・テンプレートブロート)と命令供給が詰まる
- false sharing: マルチスレッドで同じラインの別データを書き合うと、ライン所有権の奪い合いで激減速(第7部)
分岐予測¶
CPUは命令をパイプライン(数十段の流れ作業)で先読み実行しており、分岐(if)の行き先を予測して投機実行します。外れると積み上げた作業を捨てる(~15〜20サイクルの損失)。
- 予測器は「前回と同じ」「パターン」を学習する——規則的な分岐はほぼ無料、ランダムな分岐は高い
// 有名な例: ソート済み配列の方が同じ処理でも数倍速い
for (int x : data) if (x >= 128) sum += x;
// dataがソート済み → 予測ほぼ全中(前半false連続、後半true連続)
// ランダム → 50%で外れ、ペナルティの嵐
ゲームコードへの帰結¶
- ホットループ内の「型によるswitch」「仮想関数呼び出し」(間接分岐)は予測ミスの温床 → 型でソートして同種をまとめて処理(ECSのアーキタイプ処理はこれを構造化したもの)
- 分岐を消す技: min/maxやビット演算による無分岐化、
[[likely]]/[[unlikely]](C++20のヒント)——ただし計測してから。予測が当たる分岐は既にほぼ無料
SIMD (Single Instruction, Multiple Data)¶
1命令で複数データを同時に計算する仕組み(x64: SSE=128bit(float×4)、AVX=256bit(float×8)。ARM: NEON)。
// スカラー: 1回に1個
for (int i = 0; i < n; ++i) out[i] = a[i] + b[i];
// SIMD(概念): 1回に8個 → 理論8倍
// コンパイラの自動ベクトル化: 上の単純ループは -O2/-O3 で勝手にSIMD化されることが多い
使い方の階段(下ほど手間)¶
- 自動ベクトル化に任せる: 単純なループ+連続データ+依存なし、が条件。SoAレイアウトはこの条件を作るためでもある
- ライブラリ(DirectXMath, glm等の数学ライブラリはSIMD実装済み)
-
intrinsics(
_mm256_add_ps等を手書き)——エンジン・ミドルウェアの領域 -
条件: データが連続で整列(alignas → メモリレイアウト)し、分岐が少ないこと——つまりキャッシュ・分岐・SIMDの最適化は同じデータ設計に収束する(データ指向設計)
C#との対応¶
- ハードは同じなのでC#にも全部当てはまる(List
のポインタ追跡が遅い理由もキャッシュ)。UnityのBurst+Job System+NativeArrayは、まさに「連続データ+自動ベクトル化+並列」をC#で実現する仕組み——DOTSはこのページのハードウェア事情の製品化 - C#の
System.Numerics.Vector<T>がSIMDの入り口
Unity開発者が誤解しやすい点¶
- 「最適化=アルゴリズムのオーダー改善」だけではない——同じO(n)でもデータ配置で10倍変わるのが現代CPU
- 「ifは軽い」— 当たる分岐は軽い、外れる分岐は重い。分岐の数ではなく予測可能性
- Burstが速い理由を「魔法」にしない——連続メモリ・エイリアス制約・ベクトル化という、このページの原理の適用
ゲーム開発での使用例¶
- パーティクル・弾幕・群衆・スキニング・物理——「同じ計算を大量データに」の全て
- フレームスパイク調査: キャッシュミス率・分岐ミス率をプロファイラ(VTune, perf, PIX)で見る
- データレイアウト設計(構造体サイズ、ホット/コールド分離、SoA化)
使う場面 / 使わない場面¶
この知識で設計を変えるのは計測でメモリ律速・分岐律速と判明した大量処理だけ。数百体規模のゲームロジックをSoA化しても複雑さに見合いません(→ 早すぎる最適化)。ただし「大量に出るものを実体の連続配列で持つ」程度の基本は、最初からやって損がない習慣です。
理解度チェック¶
- キャッシュラインの仕組みから「配列の順走査が速い」理由を説明できますか。
- 分岐予測ミスのコストと、「ソート済みだと速い」現象の関係は?
- SIMD自動ベクトル化が効く3条件は?
演習¶
samples/cache_bench.cpp(検証済み: 同じ合計計算を「連続配列」と「シャッフルしたインデックス経由」で行い時間比較)を実行し、アクセスパターンだけで実行時間が変わることを確認してください。要素数を変えてL1/L2/L3の境目も観察してみてください。
前: 関数呼び出しとCPU | カテゴリ目次 | 次: コンパイラ最適化とUB