【実務・中級編】なぜそのコードは最適化されないのか?GCCの『Optimization Miss』レポートを活用したボトルネック特定法 – 実行環境・ランタイム・コンパイラ生産性向上バイブル

はじめに:なぜその「高速化コード」はコンパイラに無視されるのか?

テックリードとしてチームのコードレビューをしていると、しばしば次のような光景に出くわします。

> 「このループ、SIMD(ベクトル化)させれば10倍速くなるはずだから、ループを展開して一時変数も分離したんだ」

しかし、実際にコンパイルしてベンチマークを取ってみると、期待した速度が出ていないどころか、ベースラインのコードと実行速度が全く変わっていない。あるいは、バイナリを逆アセンブルしてみても、期待した `vaddps` や `ymm` レジスタを使った並列演算命令が一切生成されていない。

ここで多くのエンジニアは「C言語の限界だ」「手動でアセンбле(インラインアセンブリ)を書くしかない」と絶望します。しかし、それは大きな誤解です。

問題の所在はCPUでもC言語でもなく、「コンパイラの思考プロセス」を開発者が理解していないことにあります。

GCCやClangといったモダンなコンパイラは、極めて高度な静的解析と最適化エンジンを内蔵しています。彼らは「安全に高速化できる」と確信できたコードしか最適化しません。少しでもエイリアシング(ポインタの指す先が重複する可能性)の懸念があったり、ループの終了条件が複雑だったりすると、コンパイラは「日和って」最適化を安全側に倒します。

これが、いわゆる Optimization Miss(最適化の失敗・放棄) です。

本稿では、GCCの秘められた機能である `-fopt-info` ファミリーを駆使し、コンパイラの脳内を完全に透視してボトルネックを粉砕するプロの実践手法を徹底解説します。

—

1. コンパイラの「脳内覗き見」:`-fopt-info` の真価

コンパイラは、コードを最適化パスに通す際、内部の的中率や断念した理由をテキストとして出力する機能を持っています。これを有効にするのが `-fopt-info` オプションです。

単に「最適化されたか・されないか」だけでなく、「なぜそのループをベクトル化しなかったのか」というコンパイラの断りの理由(Reason)をミリ秒単位で暴き出します。

開発スピードを劇的に高めるビルドパイプライン設定(Makefile / CMake)

ローカルでの実験や、CI/CDパイプラインにこの解析を組み込むためのベストプラクティス構成例を提示します。

【CMakeLists.txt のベストプラクティス】

cmake_minimum_required(VERSION 3.20)
project(OptimizationMaster C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_FLAGS “${CMAKE_C_FLAGS} -O3 -Wall -Wextra”)

デバッグビルドや通常ビルドとは別に、最適化解析専用のビルドタイプを定義する
if(CMAKE_BUILD_TYPE STREQUAL “OptReport”)
# -O3に加え、ループ(-loop)とベクトル(-vec)の最適化情報を詳細(-optimized, -missed)に出力
# さらに、標準エラー出力ではなくファイル(optimization_report.txt)へリダイレクトして静的解析しやすくする
set(CMAKE_C_FLAGS “${CMAKE_C_FLAGS} -fopt-info-loop-all -fopt-info-vec-all”)
message(STATUS “>>> Optimization Reporting Mode Enabled <<<") endif() add_executable(target_app main.c) この設定により、コンパイル時に以下のような詳細なレポートが生成されます。開発者はこのレポートを読むことで、暗黒の最適化ブラックボックスに光を当てることができます。 ---

2. 実践:なぜそのループはベクトル化を拒絶されたのか?

次のような、一見すると極めてシンプルでベクトル化できそうな配列加算のコードを考えてみます。

include

void add_arrays(float restrict dest, const float src1, const float src2, size_t n) {
for (size_t i = 0; i < n; ++i) { dest[i] = src1[i] + src2[i]; } } ここで `restrict` キーワードをあえて外した、あるいはポインタの指すメモリ領域が重複(エイリアス)している可能性があるとコンパイラが判断したコードをコンパイルしたとします。

コンパイラの嘆き(`-fopt-info-vec-missed` の出力例)

main.c:5:5: missed: couldn’t vectorize loop
main.c:6:21: missed: not vectorized: unsafe dependent memory references in loop
main.c:2:6: note: use -fopt-info-vec-optimized for more information

「unsafe dependent memory references(安全ではない依存性のあるメモリー参照)」

これこそがコンパイラの叫びです。
コンパイラはこう考えています:
> 「もし `dest` と `src1` が指しているメモリー領域の一部が重なっていたら(例: `dest = src1 + 1`)、ループを回しながら書き込むことで、次の周回で読み込む `src1` の値がすでに書き換わってしまい、結果が壊れるかもしれない。だから、SIMD命令を使って一括処理するリスクを取るわけにはいかない」

解決策:`restrict` キーワードによる制約の明文化

ポインタが互いに重複しない(エイリアスしない)ことが保証されている場合、C99以降で導入された `restrict` 修飾子を付与します。これにより、コンパイラは安心してSIMD化(ベクトル化)のコードを生成します。

// restrictにより、「このポインタ同士は絶対にメモリ領域が重ならない」とコンパイラに誓約する
void add_arrays_optimized(float restrict dest, const float restrict src1, const float restrict src2, size_t n) {
for (size_t i = 0; i < n; ++i) { dest[i] = src1[i] + src2[i]; } } 再度コンパイルして `-fopt-info-vec-optimized` のレポートを確認すると、以下のように出力が変わります。 main.c:5:5: optimized: loop vectorized using 32-byte vectors (AVX2) 見事に AVX2命令(256bit幅レジスタ)を使ったベクトル化に成功しました。 ---

3. チーム開発における最適化レポートの共有化と自動化ルール

個人のローカル環境で `-fopt-info` を眺めているだけでは、チーム全体の生産性向上には繋がりません。誰かがうっかり「最適化を阻害するコード構造」をマージしてしまったとき、CI(継続的インテグレーション)がそれを検知してブロックする仕組みが必要です。

ここでは、GitHub ActionsなどのCI環境で、最適化の失敗(ベクトル化の断念)を検知してビルドを警告・失敗させるためのスクリプトと設定のベストプラクティスを共有します。

チーム標準:最適化回帰検知スクリプト(`check_opt.sh`)

コンパイラの出力する最適化レポートをパースし、意図しない「vectorization missed」が発生した場合にエラー終了するシェルスクリプトをプロジェクトの `scripts/` 配下に配置します。

!/usr/bin/env bash
==============================================================================
チーム標準: コンパイラの最適化ミス(ベクトル化放棄)を検知するCI用スクリプト
==============================================================================
set -euo pipefail

ビルドログの出力先
LOG_FILE=”build_optimization.log”

echo “[] Building project with optimization diagnostics…”
GCCを対象に、強制的に最適化情報とベクトル化情報をファイルに出力させる
cmake -B build -DCMAKE_BUILD_TYPE=OptReport
cmake –build build — 2>&1 | tee “$LOG_FILE”

echo “[] Analyzing optimization report for missed vectorizations…”

「missed: couldn’t vectorize loop」が含まれている行を抽出
MISSED_COUNT=$(grep -c “missed: couldn’t vectorize loop” “$LOG_FILE” || true)

if [ “$MISSED_COUNT” -gt 0 ]; then
echo “——————————————————————”
echo “[ERROR] Optimization Miss Detected! (${MISSED_COUNT} loops failed to vectorize)”
echo “——————————————————————”
# 該当する行をハイライト表示
grep -C 3 “missed: couldn’t vectorize loop” “$LOG_FILE”
echo “——————————————————————”
echo “HINT: Check pointer aliasing (use restrict) or loop trip counts.”
exit 1
else
echo “[SUCCESS] All target loops were successfully vectorized/optimized!”
exit 0
fi

このスクリプトを Pull Request のトリガーとして CI パイプラインに組み込むことで、「知らぬ間に最適化が外れていた」というパフォーマンスの退行(リグレッション)を完全に防ぐことができます。

—

4. プロが実践する:その他の最適化阻害要因と見抜き方

`restrict` 以外にも、コンパイラが最適化をあきらめる代表的なコード構造があります。`-fopt-info` の出力メッセージと紐付けて、その対策を頭に叩き込んでおきましょう。

① ループ回数(Trip Count)がコンパイル時に不明、またはアライメントが不明

  • コンパイラのメッセージ: `not vectorized: number of iterations is not known` / `unaligned memory reference`
  • 原因: 配列のサイズが動的かつ不規則である場合、プロローグ・エピローグ処理(アライメント調整のための端数処理)のコードが肥大化するため、コンパイラがベクトル化を諦めることがあります。
  • 対策: ループのサイズが特定の倍数(例: 32バイトアライメントなら 8の倍数など)であることをコンパイラにヒントとして与えるか、`__builtin_assume_aligned` などのビルトイン関数を活用します。

② ループ内部での関数呼び出し・分岐(Control Flow)

  • コンパイラのメッセージ: `not vectorized: control flow in loop`
  • 原因: ループの中に `if` 文や、インライン展開されない外部関数呼び出しが存在する場合、SIMDのデータ並列処理の原則(同じ演算を同時に行う)から外れるため、ベクトル化が困難になります。
  • 対策: 条件分岐をマスク演算(三項演算子やビット演算)に書き換えるか、処理をループの外に持ち出します(Loop-invariant code motion)。また、小さな関数であれば `static inline` を付与してコンパイラにインライン展開を強制します。

—

5. おわりに:コンパイラと「対話」できるエンジニアへ

優れた低レイヤエンジニアやテックリードは、魔法のように速いコードを書くわけではありません。彼らは「コンパイラの思考プロセス」を熟知し、コンパイラが最も効率的なコードを生成しやすい「環境証拠」をコードの構造によって提供しているに過ぎません。

今回紹介した `-fopt-info` を活用したボトルネック特定法は、単なるデバッグ手法の枠を超え、C言語という言語の仕様とハードウェアの物理的制約(キャッシュ、レジスタ、メモリアライメント)を脳内で直結させるための強力なトレーニングです。

「なぜこのコードは最適化されないのか?」
その問いに悩んだときは、コンパイラに語らせてみてください。コンパイラは必ず、その答えをレポートの行間に示してくれます。

チーム全体のコード品質と実行パフォーマンスを次の次元へ引き上げるために、まずは今日のビルドスクリプトに `-fopt-info-vec` を組み込んでみることから始めてみましょう。

タイトルとURLをコピーしました