コンテンツにスキップ

コンパイラ最適化・未定義動作の影響・Debug/Release・ゼロコスト抽象化

コンパイラ最適化 — 何をしているのか

コンパイラは「観測可能な動作(出力・volatileアクセス等)が同じなら、コードを何をしてもよい」(as-ifルール)という原則で変形します。主な最適化:

最適化 内容
定数畳み込み・伝播 コンパイル時に計算 60*603600、その値を使う先も連鎖的に
インライン展開 呼び出しを本体で置換 inlineの実際
デッドコード除去 結果が使われないコードの削除 「計測用ループが丸ごと消えて0ms」の正体
ループ変形 不変式の巻き上げ、展開、ベクトル化 for内のv.size()呼び出しを外へ
レジスタ割付 変数をメモリに置かない レジスタ
命令並べ替え 依存のない命令の順序変更 パイプライン効率化(→ マルチスレッドでメモリモデル問題の遠因)
最適化レベル: -O0(なし・デバッグ用) / -O1 / -O2(標準) / -O3(積極) / -Os(サイズ優先)
MSVC: /Od(なし) / /O2(速度)。LTO(-flto, /GL)でリンク時に翻訳単位を越えて最適化

未定義動作が最適化に与える影響(このページの核心)

コンパイラは「UBは起きない」という前提で変形します。UBのページの続きを、最適化の視点から:

// 例1: 符号付きオーバーフローはUB → 「起きない」前提でループを変形
for (int i = 0; i <= n; ++i) { ... }
// もし n == INT_MAX なら i<=n は常に真(無限ループ)だが、
// コンパイラは「オーバーフローは起きない=ループは有限」とみなして
// 回数を事前計算する等の変形をする。実行時の挙動は「壊れ方すら不定」になる

// 例2: nullチェックの削除(→ UBページの例)
// 例3: 未初期化変数 → 「どんな値と仮定してもよい」→ 分岐ごと消える

重要な帰結: 「-O0では動くが-O2で壊れる」の大半はコンパイラのバグではなく、自分のコードのUBが最適化で顕在化したもの。逆に言うと、UBフリーなコードは最適化レベルを上げても意味が変わりません——これが「UBを書かない」ことの実利です。

デバッグビルドとリリースビルドの違い

Debug (-O0, /Od) Release (-O2, /O2)
最適化 なし(コードは書いた通り) 上記すべて
変数 全部メモリにあり、デバッガで見える レジスタ化・消滅で「見えない」「値が飛ぶ」
ステップ実行 行と命令が対応 インライン化・並べ替えで行が飛ぶ
assert / チェック 有効(NDEBUG未定義) assertは無効化が通例
STL チェック付き(MSVCのデバッグイテレータ等)で桁違いに遅いことがある 高速
速度 ゲームによっては遊べない遅さ 本番速度
  • ゲーム開発では中間のDevelopment/Profile構成(最適化あり+一部チェック・計測機能)を持つのが普通(→ ビルド構成)
  • 「Debugでだけ再現するバグ」「Releaseでだけ壊れるバグ」の両方が正常にあり得る(初期化パターン・タイミング・レイアウトが変わるため)。Releaseビルドのテストを後回しにしない

ゼロコスト抽象化

C++の設計哲学:「使わない機能の代金は払わない。使う抽象は手書きの低レベルコードと同等になるべき」。

抽象 実行時コスト
クラス(非virtual)・名前空間・const・テンプレート ゼロ(コンパイル時に消える)
unique_ptr / vector<T>[] / range-for / ラムダのインライン適用 手書き相当(最適化前提)
仮想関数 / RTTI / 例外(投げた時) / std::function / shared_ptr 有料(選んで使う)
  • 「ゼロコスト」は最適化ビルドでの話。-O0ではunique_ptrの間接層もそのまま残る(Debugビルドが遅い一因)
  • C#との対照: 抽象の多く(クラス=ヒープ+GC、interface呼び出し、デリゲート)が常に有料で、選べない。C++はコスト表を見て選ぶ言語——本Wikiの各ページで「コスト」を毎回書いてきたのはこのため

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

  1. 「デバッガで変数が見えない=壊れている」— Releaseでは正常(最適化で消えただけ)。調査はDebug/Developmentビルドで
  2. 「エディタで動けばOK」— UnityエディタはMono/JITでDebug相当。IL2CPPリリースビルドは最適化されたC++の世界で、このページの差異がそのまま現れる
  3. ベンチマークの罠: 結果を使わない計測コードはデッドコード除去で消える(「0msになった!」)。結果をvolatileや出力で「観測」させる

ゲーム開発での使用例

  • 出荷ビルドだけのバグ調査: UB検出(サニタイザ)→最適化差の切り分け、という手順を知っている人が強い
  • フレーム最適化: プロファイル→ホットパスのデータ設計(前ページ)→それでも足りなければintrinsics
  • assert・ログを構成別に制御し、Shippingではゼロコストにする設計

理解度チェック

  1. as-ifルールとは何ですか。「観測可能な動作」に含まれないものの例は?
  2. 「-O2で壊れた」ときにまず疑うべきものは?
  3. 「ゼロコスト抽象化」の正確な意味と、その前提条件は?

演習

godbolt.orgで、(a) 結果を使わないループが-O2で消えること、(b) std::unique_ptr 経由のアクセスが-O2で生ポインタと同じコードになること、を確認してください。


前: キャッシュ・分岐予測・SIMD | カテゴリ目次 | 次: 演習