ビルドを極める
この章でわかること:
- LTO・codegen-unitsの実測トレードオフ(ビルド17→39秒、バイナリ5.8→3.9MB)
- PGO(プロファイルに基づく最適化)の完全な手順と、正直な実測結果
- コンパイル時間を削る体系的な方法 — 開発イテレーションも性能のうち
- バイナリサイズの最小化(実測: 422KB→279KB)と、その代償
- なぜ体系に必須か — コードを1行も変えずに引き出せる性能と、 日々のビルド待ち時間は、どちらも「ビルドの知識」だけで決まります。 6章で紹介した設定群を、実測で裏づけて完成させます
この章の実測はすべて筆者のMac(Apple M4)で行いました。 ビルド時間はマシンとキャッシュ状態に強く依存するため、 絶対値ではなく比率を見てください。
リリースプロファイルの実測
Section titled “リリースプロファイルの実測”6章で「速度優先ならlto = "thin"、
codegen-units = 1」と書きました。その代償を数字にします。
本書のexamples(wgpu依存ツリー込み)のクリーンビルドで比較しました。
| 設定 | クリーンビルド | バイナリサイズ |
|---|---|---|
| lto=off, codegen-units=16 (cargo既定) | 17秒 | 5.8MB |
| lto=thin, codegen-units=1 (6章の推奨) | 29秒 | 4.6MB |
| lto=fat, codegen-units=1 | 39秒 | 3.9MB |
LTOを深めるほど、ビルドは遅く、バイナリは小さくなります (クレート横断でコードが融合・重複排除されるためです)。 実行速度への効果はコードの性質に依存します——クレートの境界を 高頻度で越える呼び出しが多いほど効き、この本の行列積のような 「ホットループが1クレートに閉じた」コードでは差が出にくくなります。 判断は8章の計測で行ってください。
実務の定石は、プロファイルを分けることです。
[profile.release] # 日常のリリースビルド: 既定のまま速く回す
[profile.dist] # 配布用: 時間をかけて最良を狙うinherits = "release"lto = "thin"codegen-units = 1PGO — 手順と現実
Section titled “PGO — 手順と現実”PGO(profile-guided optimization、6章)は 「実際の実行の統計」をコンパイラに与える手法です。手順は4段階です。
# 0. 前回のプロファイルを消しておく(混入防止)rm -rf /tmp/pgo-data
# 1. 計測を仕込んだバイナリを作る# (--target を明示すると、ビルドスクリプト自身の実行が# プロファイルに混ざるのを防げます)RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" cargo build --release
# 2. 代表的なワークロードで実行する(プロファイルが書き出される)./target/release/myapp --typical-workload
# 3. プロファイルを1つに統合する。llvm-profdataは# `rustup component add llvm-tools` で入る(PATH外にあるので# ~/.rustup/toolchains/*/lib/rustlib/*/bin/ から探す)llvm-profdata merge -o /tmp/pgo-data/merged.profdata /tmp/pgo-data
# 4. プロファイルを使って再ビルドするRUSTFLAGS="-Cprofile-use=/tmp/pgo-data/merged.profdata" cargo build --releaseコンパイラは分岐の実測頻度をもとに、ホットパスのコード配置 (16章のフロントエンド改善)、 インライン判断、間接呼び出しの直接化などを行います。
正直な実測結果を報告します。筆者が「偏った間接呼び出しを 5000万回行う」マイクロベンチマークでこの手順を試したところ、 ベースライン約70ミリ秒に対しPGO版は約75ミリ秒——改善なしでした。 小さなプログラムでは、プロファイルなしでもコンパイラの静的な 判断がすでに最適に近く、PGOの出番がなかったのです。
PGOが効くと報告されているのは、コンパイラ自身、ブラウザ、 大規模サーバのような「コードが巨大で、分岐が多様で、 命令キャッシュが逼迫する」プログラムです(rustc自身もPGOで ビルドされています)。数%〜十数%の改善が典型で、 魔法ではなく、大きなコードの整理整頓と考えるのが正確です。 手順を自動化するcargo-pgoも あります。さらに先には、リンク済みバイナリを実行プロファイルで 並べ替えるBOLTという道具も存在します(cargo-pgoが対応)。
コンパイル時間という性能
Section titled “コンパイル時間という性能”Rustの遅いコンパイルは開発の摩擦そのものです。原因と対策を 体系的に並べます。
まず計測から(8章の原則はここでも同じです)。
cargo build --timings # クレートごとの所要時間のHTMLレポート時間を消費する主要な要因と対策:
- 依存クレートの多さ —
--timingsで上位を特定し、 不要な依存とfeatureを削ります(default-features = false)。 依存は実行性能ではなくビルド性能の敵です - 単相化の爆発 — 7章のとおり、
ジェネリクスは使用型ごとにコードを複製します。巨大なジェネリック
関数は、型に依存しない本体を非ジェネリックな内部関数に
切り出すと激減します。コンパイル時間とバイナリサイズが
問題の境界では、
dyn(実行時コストと引き換え)も正当な選択です - リンク時間 — 大きなプロジェクトではリンカの置き換えが
効きます。Linuxでは
moldが定番で、macOSも近年の標準リンカが 高速化しています。インクリメンタルビルドの終盤はほぼリンク時間 なので、体感への効果が大きい項目です - 開発ループの選択 — 型検査だけなら
cargo check(コード生成を 丸ごと省く)。エディタのrust-analyzerもこれに相当します。 CIではsccache等のコンパイルキャッシュが効きます
バイナリサイズ
Section titled “バイナリサイズ”最後はサイズです。サイズ優先のプロファイルを作り、実測しました (対象は小さなCLIベンチマーク)。
[profile.min-size]inherits = "release"opt-level = "z" # サイズ優先の最適化lto = "fat"panic = "abort" # 巻き戻し機構を捨てる(6章)strip = true # シンボル情報を除去結果: 422KB → 279KB(34%減)。ただし実行時間は約70ミリ秒から
約90ミリ秒に悪化しました。opt-level = "z"はループ展開や
インライン化を抑えるため、サイズと速度は明確なトレードオフです
(一方で16章のとおり、命令キャッシュが
逼迫する大きなプログラムでは、小さくすることが速さに転じる場合も
あります——だから計測です)。
何が太らせているかはcargo-bloatで
関数単位に分解できます。よくある犯人は、単相化の複製、
Debug/フォーマット機構の連鎖、そして(Rustは標準ライブラリを
静的リンクするため)使っていないようで使われているstdのコードです。
サイズが本当に厳しい組み込みやWebAssemblyの世界では、
これらに加えてno_std(標準ライブラリなし構成)という
選択肢が現れます——本書では名前の紹介にとどめます。
- LTOとcodegen-unitsは「ビルド時間 ⇄ バイナリサイズ・(コード次第で)速度」の ダイヤルです。実測ではビルド17→39秒、サイズ5.8→3.9MBの幅がありました。 日常用と配布用でプロファイルを分けるのが定石です
- PGOは4段階の手順で導入できますが、小さなプログラムには効きません (実測: 効果なし)。大規模で分岐の多様なコードの整理整頓です
- コンパイル時間は
--timingsで計測し、依存削減・単相化の抑制・ リンカ・cargo checkで削ります - サイズは
opt-level="z"+panic="abort"+stripで34%減。 ただし速度と引き換えでした(実測: 70→90ミリ秒)
次章はPart Vの締めくくりとして、データ構造の「実装の中身」—— HashMapのSwissTable、BTreeの実性能、SmallVec、ビット操作——を 実測で開きます。「アルゴリズムとデータ構造が最優先」(8章)の 判断を、実装レベルの知識で支えます。