バイナリ肥大化の「因果」を断つ:cargo-bloatによるRustバイナリ最適化の深淵
Rustを選択するエンジニアの多くは、その安全性と引き換えに「バイナリサイズ」という怪物と対峙することになる。単に`lto = true`や`codegen-units = 1`を設定して満足しているなら、それはまだ表層に過ぎない。
真に最適化されたバイナリを構築するには、「なぜその関数がリンクされ、実行ファイルという巨大な墓場に埋葬されているのか」という依存関係の深淵を解明する必要がある。本稿では、`cargo-bloat`を単なる分析ツールではなく、CI/CDパイプラインに組み込まれた「ゲートキーパー」へと昇華させる手法を伝授する。
—
1. cargo-bloat:静的解析の先にある「依存関係の真実」
`cargo-bloat`は、単にサイズを表示するツールではない。LLVMのシンボルテーブルを走査し、デッドコード除去(Dead Code Elimination)が及ばなかった範囲を可視化する「侵入検知器」だ。
特に危険なのは、「肥大化の連鎖」である。一つの汎用的なクレートを導入したことで、それ自体は軽量でも、推移的依存関係(Transitive Dependencies)の中で重厚な暗号化ライブラリや不要なデバッグシンボル生成ロジックがリンクされるケースが後を絶たない。
現場で叩くべき「真実を炙り出す」コマンド
まずは、単純な実行ではなく、JSONフォーマットで出力し、後続の自動化プロセスにデータを流し込む準備をする。
依存関係のサイズを関数単位でJSON出力する
–cratesを指定してライブラリ単位で、–symbolsで関数単位で追跡する
cargo bloat –release –crates –json > bloat_report.json
—
2. CI/CDパイプラインへの「肥大化ゲート」実装
パフォーマンスを追求する現場では、「サイズはエンジニアの良心に委ねる」という甘えは許されない。バイナリサイズをCIのテスト項目として定義し、閾値を超えた瞬間にビルドを失敗させる「サイズ・リグレッションテスト」を導入する。
GitHub Actionsにおける自動化スクリプト例
`jq`と組み合わせて、特定の閾値を超えた場合に警告を出すパイプラインを構築する。
jobs:
size-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cargo-bloat
run: cargo install cargo-bloat
- name: Analyze Binary Size
run: |
# バイナリサイズを計測し、合計が10MBを超えたらビルドエラーにする
cargo bloat –release –json > report.json
TOTAL_SIZE=$(jq ‘.[“files”] | map(.size) | add’ report.json)
if [ “$TOTAL_SIZE” -gt 10485760 ]; then
echo “::error::バイナリサイズが制限値(10MB)を超えています: ${TOTAL_SIZE} bytes”
exit 1
fi
—
3. 「デフォルトフィーチャー」という名の罠
多くのRustクレートは、`default-features = true`によって、必要以上の機能を持ち込んでいる。例えば、HTTPクライアントに「TLSバックエンド」を複数抱えたり、不要なシリアライザを同梱したりするケースだ。
`cargo-bloat`で分析した結果、使っていないはずの機能が巨大なセクションを占有していた場合、`Cargo.toml`で徹底的に切り捨てる必要がある。
悪例: 必要な機能だけを明示せず、デフォルトに甘える
[dependencies]
reqwest = “0.11”
理想: 必要な機能のみをピンポイントで指定し、バイナリを削ぎ落とす
[dependencies]
reqwest = { version = “0.11”, default-features = false, features = [“rustls-tls”] }
この際、`cargo tree -e features`で依存関係を可視化し、`cargo bloat`の結果と突き合わせるのが、熟練アーキテクトの定跡である。
—
4. 低レイヤ最適化:バイナリから「脂肪」を削ぎ落とす仕上げ
`cargo-bloat`で戦犯を特定し、不要な機能を削除した後は、以下の設定を`Cargo.toml`に適用し、コンパイラに極限の努力を強いる。
[profile.release]
opt-level = ‘z’ # サイズ最適化を最優先 (sよりも更に攻撃的)
lto = “fat” # 全てのクレートにまたがるリンク時最適化
codegen-units = 1 # コンパイル速度を犠牲にしてでも最適化効率を最大化
panic = ‘abort’ # パニック時のスタックアンワインドコードを削除
strip = true # シンボル情報を完全除去
なぜ `opt-level = ‘z’` なのか?
多くのエンジニアは `’s’` を選択するが、`’z’` は関数のインライン展開よりも「コードサイズ削減」を優先させる。マイクロサービスやLambdaのような実行環境では、実行速度のわずかなロスよりも、コールドスタート時間を短縮する「バイナリの軽量さ」が圧倒的な競争優位性を生む。
—
結び:エンジニアの美学としてのバイナリ管理
`cargo-bloat`を使いこなすということは、あなたの書いたコードがメモリ空間でどう展開されるか、その「解像度」を高める行為に他ならない。
ツールが提示する数値は単なるデータではない。それは、あなたが依存を選択する際に行った「決断の履歴」である。不要なライブラリを削ぎ落とし、必要最小限の機能だけをコンパイルしたバイナリには、余計なものが何もないという「機能美」が宿る。
今日から、CIの中にこの「ゲート」を設置せよ。バイナリサイズを制御する者は、実行環境という戦場において、常に優位な位置を確保できるはずだ。