Rustは高い安全性と実行パフォーマンスを両立する言語として、システム開発からWebアプリケーションまで幅広く採用されています。
しかし、Rust学習者や実戦投入を始めた開発者が直面する大きな課題の一つに、コンパイル時間の長さが挙げられます。
特にプロジェクトの規模が拡大するにつれて、ビルドの待機時間は開発の生産性に直結する重要な要素となります。
この記事では、最新のRustツールチェーンにおけるCargoのビルド設定や、実務で活用できる最適化手法について詳しく紹介します。
Rustのビルド時間が開発効率に与える影響
Rustのコンパイラである rustc は、借用チェッカーによる厳密な安全性チェックや、LLVMを用いた高度な最適化をバックエンドで行います。
これらの処理は非常に強力ですが、引き換えにコンパイルにかかる計算コストが大きくなる傾向にあります。
ビルド時間が長くなると、コードを修正してから動作を確認するまでのフィードバックループが阻害されます。
開発者の集中力を維持するためには、インクリメンタルビルドの時間をいかに短縮するかが鍵となります。
また、CI/CD環境におけるビルド時間の増大は、デプロイサイクルの遅延やコンピューティングコストの増加を招きます。
したがって、Cargoの設定を最適化することは、単なる快適さの追求ではなく、プロジェクト全体のコスト削減につながる重要な施策です。
Cargoビルドの基本操作とプロファイルの理解
CargoはRustの標準ビルドシステムであり、デフォルトで複数のビルドプロファイルを提供しています。
まずはこれらのプロファイルの特性を正しく理解することが、最適化の第一歩となります。
devプロファイルとreleaseプロファイルの使い分け
標準的な cargo build コマンドを実行すると、デフォルトでは dev プロファイルが使用されます。
dev プロファイルは、デバッグ情報の付与とコンパイル速度を優先するため、実行速度は低速です。
一方で、本番環境向けの cargo build --release は、コードの最適化を最大限に行いますが、その分コンパイル時間は大幅に長くなります。
開発中は dev プロファイルを使用し、パフォーマンス測定が必要な場合のみ release を使用するのが基本です。
cargo build –timings によるボトルネックの可視化
ビルド時間を短縮するためには、まず「どのクレートに時間がかかっているか」を把握する必要があります。
Rust 1.60以降で安定化した --timings フラグを使用することで、ビルドプロセスの詳細なレポートを出力できます。
# ビルド時間の詳細レポートを生成する
cargo build --timings
このコマンドを実行すると、target/cargo-timings/ ディレクトリ内にHTMLファイルが生成されます。
このレポートを確認することで、依存関係の依存グラフや、並列ビルドを妨げているボトルネックとなっているクレートを特定できます。
ビルド時間の可視化は、根拠に基づいた最適化を行うために不可欠なプロセスです。
Cargo.tomlによるビルド設定の最適化
プロジェクトの Cargo.toml に設定を記述することで、プロファイルごとの動作を詳細にカスタマイズできます。
最適化レベル(opt-level)の微調整
opt-level は、コンパイラがどの程度コードを最適化するかを 0 から 3 の数値で指定する項目です。
開発用ビルドでも、特定の計算負荷が高い処理を扱う場合は、レベル 0 では遅すぎてテストが困難な場合があります。
その場合、以下のように dev プロファイルの最適化レベルを 1 に引き上げることで、バランスを取ることができます。
# Cargo.toml
[profile.dev]
opt-level = 1 # 開発ビルドでも少しだけ最適化をかける
これにより、コンパイル時間を大幅に増やさずに、実行速度を数倍に引き上げることが可能になります。
LTO(Link Time Optimization)の活用
LTOは、リンク時にクレートをまたいだ最適化を行う手法であり、実行バイナリのサイズ削減と高速化に寄与します。
しかし、LTOは非常にメモリを消費し、リンク時間を劇的に増加させます。
デフォルトでは無効になっていますが、本番環境向けに "thin" LTOを指定することで、コンパイル時間の増加を抑えつつパフォーマンスを向上させることができます。
[profile.release]
lto = "thin" # フルLTOよりも高速な最適化を適用
Codegen Units(コード生成ユニット)の制御
Rustコンパイラは、クレートを複数のユニットに分割して並列にコード生成を行います。
codegen-units の数を増やすと並列度が上がりビルドは速くなりますが、最適化の質は低下します。
逆に、この値を 1 に設定すると、単一のユニットで最適化が行われるため、最高のパフォーマンスが得られますが、ビルドは非常に遅くなります。
[profile.release]
codegen-units = 1 # 実行速度を最優先にする設定
パニック時の動作設定(panic = “abort”)
デフォルトでは、Rustのプログラムがパニックした際にスタックトレースを表示するための「アンワインド (unwind)」情報が含まれます。
これを "abort" に変更することで、パニック時に即座に終了するように設定できます。
この変更により、アンワインド用のコード生成が不要になり、バイナリサイズの削減とビルド時間の短縮が期待できます。
[profile.release]
panic = "abort" # アンワインド処理を省略
リンカーの変更による劇的な高速化
Rustのコンパイル工程において、最後に実行される「リンク」のステップは意外にも多くの時間を消費します。
デフォルトのリンカーの代わりに、より高速なモダンリンカーを使用することで、全体のビルド時間を半分以下に短縮できる場合があります。
高速リンカー「mold」と「lld」の導入
Linux環境では、非常に高速なリンカーである mold が利用可能です。
また、LLVMプロジェクトの一部である lld も、標準のリンカーより高速に動作します。
これらのリンカーをRustで使用するには、.cargo/config.toml ファイルに設定を追加します。
# .cargo/config.toml
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"] # moldを使用する設定
macOSの場合は、zld などの代替リンカーが検討の候補に上がります。
大規模なプロジェクトでは、リンク時間の短縮が最も体感的なビルド速度向上に繋がります。
コンパイラバックエンドとキャッシュの活用
Rust 1.70以降、開発時のビルド速度をさらに向上させるための新しい選択肢が登場しています。
Craneliftバックエンドによる開発時ビルドの短縮
通常、RustはバックエンドにLLVMを使用しますが、Cranelift という高速なコード生成エンジンを使用するオプションがあります。
Cranelift はLLVMほど高度な最適化は行いませんが、コード生成速度が非常に高速です。
開発環境においてのみ Cranelift を有効にすることで、コード変更後のビルド待機時間を極限まで短くできます。
これを利用するには、nightlyツールチェーンや特定のコンポーネントの導入が必要になりますが、検討する価値は十分にあります。
sccacheを用いたビルド成果物の共有
sccache は、Mozillaが開発しているコンパイルキャッシュツールです。
同一の依存関係を複数のプロジェクトで使用している場合や、cargo clean を頻繁に行う場合に効果を発揮します。
コンパイル済みのオブジェクトをキャッシュに保存し、再利用することで、二回目以降のビルドを高速化します。
# sccacheのインストール例
cargo install sccache
# 環境変数でRustのコンパイララッパーとして指定
export RUSTC_WRAPPER=sccache
クラウド環境では、AWS S3やGoogle Cloud Storageなどの外部ストレージをキャッシュ先として指定することも可能です。
ソースコードレベルでのビルド最適化手法
Cargoの設定だけでなく、Rustのコードの書き方を工夫することでもビルド速度を改善できます。
ジェネリクスの単相化を抑制する
Rustのジェネリクスは強力ですが、コンパイル時に具体的な型ごとにコードを生成する「単相化 (Monomorphization)」を行います。
多くの型で同じジェネリック関数を呼び出すと、その分だけ生成されるコード量が増え、ビルド時間が増大します。
トレイトオブジェクト (&dyn Trait) を使用した動的ディスパッチを適切に活用することで、コード生成量を抑制できます。
パフォーマンスが最優先でない内部関数などは、動的ディスパッチへの切り替えを検討してください。
クレートの分割と依存関係の整理
一つの巨大なクレートで開発を進めると、コードの一部を変更しただけで多くの再コンパイルが発生します。
プロジェクトを論理的な単位で複数の「ワークスペース (Workspace)」内のクレートに分割してください。
Cargoはクレート単位で並列ビルドを行うため、適切に分割されたプロジェクトはマルチコアCPUの恩恵を最大限に受けられます。
また、不要な機能を無効にするために、依存ライブラリの default-features = false を指定することも有効です。
# 必要な機能だけを有効にする
[dependencies]
serde = { version = "1.0", default-features = false, features = ["derive"] }
CI/CD環境におけるビルドの効率化
継続的インテグレーションにおいて、ビルド時間はエンジニアの待機時間に直結します。
GitHub Actionsでのキャッシュ戦略
GitHub Actionsを使用している場合、actions/cache を活用して target/ ディレクトリや ~/.cargo/registry を保存することが一般的です。
ただし、Rustの target/ ディレクトリは非常に巨大になりやすいため、単純なキャッシュではキャッシュサイズの上限に達することがあります。
そこで、cargo-chef というツールを使用すると、依存関係のみを事前にビルドしてレイヤー化することができます。
これにより、ソースコードに変更があっても依存ライブラリの再ビルドを回避し、CIの実行時間を大幅に短縮できます。
| 手法 | 期待される効果 | 導入の難易度 |
|---|---|---|
| 高速リンカー (mold/lld) | リンク時間の劇的な短縮 | 低 |
| opt-level の調整 | 開発時の実行速度向上 | 低 |
| sccache の導入 | クリーンビルドの高速化 | 中 |
| クレートのワークスペース分割 | 並列ビルド効率の最大化 | 高 |
まとめ
Rustのビルド効率化は、開発体験を向上させるために避けては通れないテーマです。
本記事で紹介したように、まずは --timings を用いて現状を把握することから始めてください。
その後、Cargo.toml の設定見直し、高速リンカーへの切り替え、キャッシュツールの導入と段階的に対策を講じることが推奨されます。
適切なビルド最適化を行うことで、Rustの強力な恩恵を享受しつつ、ストレスのない開発環境を構築することが可能です。
日々の開発の中で、少しずつこれらのテクニックを取り入れ、より洗練されたRust開発ライフを実現しましょう。
