コンテンツにスキップ

キャッシュ・分岐予測・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 LocalityAoS/SoAECSの物理的根拠です。「リストより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化されることが多い

使い方の階段(下ほど手間)

  1. 自動ベクトル化に任せる: 単純なループ+連続データ+依存なし、が条件。SoAレイアウトはこの条件を作るためでもある
  2. ライブラリ(DirectXMath, glm等の数学ライブラリはSIMD実装済み)
  3. intrinsics(_mm256_add_ps 等を手書き)——エンジン・ミドルウェアの領域

  4. 条件: データが連続整列(alignas → メモリレイアウト)し、分岐が少ないこと——つまりキャッシュ・分岐・SIMDの最適化は同じデータ設計に収束する(データ指向設計)

C#との対応

  • ハードは同じなのでC#にも全部当てはまる(Listのポインタ追跡が遅い理由もキャッシュ)。UnityのBurst+Job System+NativeArrayは、まさに「連続データ+自動ベクトル化+並列」をC#で実現する仕組み——DOTSはこのページのハードウェア事情の製品化
  • C#の System.Numerics.Vector<T> がSIMDの入り口

Unity開発者が誤解しやすい点

  1. 「最適化=アルゴリズムのオーダー改善」だけではない——同じO(n)でもデータ配置で10倍変わるのが現代CPU
  2. 「ifは軽い」— 当たる分岐は軽い、外れる分岐は重い。分岐の数ではなく予測可能性
  3. Burstが速い理由を「魔法」にしない——連続メモリ・エイリアス制約・ベクトル化という、このページの原理の適用

ゲーム開発での使用例

  • パーティクル・弾幕・群衆・スキニング・物理——「同じ計算を大量データに」の全て
  • フレームスパイク調査: キャッシュミス率・分岐ミス率をプロファイラ(VTune, perf, PIX)で見る
  • データレイアウト設計(構造体サイズ、ホット/コールド分離、SoA化)

使う場面 / 使わない場面

この知識で設計を変えるのは計測でメモリ律速・分岐律速と判明した大量処理だけ。数百体規模のゲームロジックをSoA化しても複雑さに見合いません(→ 早すぎる最適化)。ただし「大量に出るものを実体の連続配列で持つ」程度の基本は、最初からやって損がない習慣です。

理解度チェック

  1. キャッシュラインの仕組みから「配列の順走査が速い」理由を説明できますか。
  2. 分岐予測ミスのコストと、「ソート済みだと速い」現象の関係は?
  3. SIMD自動ベクトル化が効く3条件は?

演習

samples/cache_bench.cpp(検証済み: 同じ合計計算を「連続配列」と「シャッフルしたインデックス経由」で行い時間比較)を実行し、アクセスパターンだけで実行時間が変わることを確認してください。要素数を変えてL1/L2/L3の境目も観察してみてください。


前: 関数呼び出しとCPU | カテゴリ目次 | 次: コンパイラ最適化とUB