コンパイラ最適化・未定義動作の影響・Debug/Release・ゼロコスト抽象化¶
コンパイラ最適化 — 何をしているのか¶
コンパイラは「観測可能な動作(出力・volatileアクセス等)が同じなら、コードを何をしてもよい」(as-ifルール)という原則で変形します。主な最適化:
| 最適化 | 内容 | 例 |
|---|---|---|
| 定数畳み込み・伝播 | コンパイル時に計算 | 60*60 → 3600、その値を使う先も連鎖的に |
| インライン展開 | 呼び出しを本体で置換 | → 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開発者が誤解しやすい点¶
- 「デバッガで変数が見えない=壊れている」— Releaseでは正常(最適化で消えただけ)。調査はDebug/Developmentビルドで
- 「エディタで動けばOK」— UnityエディタはMono/JITでDebug相当。IL2CPPリリースビルドは最適化されたC++の世界で、このページの差異がそのまま現れる
- ベンチマークの罠: 結果を使わない計測コードはデッドコード除去で消える(「0msになった!」)。結果をvolatileや出力で「観測」させる
ゲーム開発での使用例¶
- 出荷ビルドだけのバグ調査: UB検出(サニタイザ)→最適化差の切り分け、という手順を知っている人が強い
- フレーム最適化: プロファイル→ホットパスのデータ設計(前ページ)→それでも足りなければintrinsics
- assert・ログを構成別に制御し、Shippingではゼロコストにする設計
理解度チェック¶
- as-ifルールとは何ですか。「観測可能な動作」に含まれないものの例は?
- 「-O2で壊れた」ときにまず疑うべきものは?
- 「ゼロコスト抽象化」の正確な意味と、その前提条件は?
演習¶
godbolt.orgで、(a) 結果を使わないループが-O2で消えること、(b) std::unique_ptr 経由のアクセスが-O2で生ポインタと同じコードになること、を確認してください。
前: キャッシュ・分岐予測・SIMD | カテゴリ目次 | 次: 演習