``
Rustプロジェクトの規模が大きくなるにつれ、cargo build の待ち時間がボトルネックになるケースは珍しくありません。Rust公式が2025年夏に実施した「Rust Compiler Performance Survey」では、ビルドパフォーマンスへの満足度が10段階中「6」にとどまり、「コンパイル時間の長さが生産性を下げている」という回答が多数を占めました。
Rustのコンパイルが遅くなる仕組みを正しく把握し、適切な対策を打てば、開発体験は大きく改善できます。
Rustのコンパイルが遅い3つの根本原因
LLVMバックエンドによる最適化コスト
Rustコンパイラ(rustc)はソースコードをまずMIR(Mid-level Intermediate Representation)に変換し、その後LLVM IRへ変換してからLLVMに最適化とネイティブコード生成を委ねます。LLVMは高品質な最適化を行う反面、この処理には時間がかかります。
特にリリースビルド(--release)ではLLVMの最適化パスが多段に実行されるため、デバッグビルドの数倍の時間を要することもあります。cargo llvm-linesコマンドを使うと、LLVMが生成するIRの行数をクレート単位で確認でき、どの部分がLLVMの負荷を増大させているか特定できます。
ジェネリクスとモノモーフィゼーション
Rustのジェネリクスは「モノモーフィゼーション」と呼ばれる方式で処理されます。ジェネリック関数が具体的な型引数で呼び出されるたびに、その型ごとの専用コードが生成される仕組みです。
たとえば fn process<T: Display>(value: T) が String、i32、Vec<u8> の3種類で呼ばれると、コンパイラは3つの独立した関数を生成します。大規模プロジェクトでは数百のインスタンスが生まれることもあり、コンパイル時間とバイナリサイズの両方に影響を及ぼします。
対策としては、ジェネリック関数の内部でも型に依存しない処理を非ジェネリックな内部関数に切り出す手法が有効です。
// 改善前: 全体がモノモーフィゼーションされる
fn write_data<W: Write>(writer: &mut W, data: &[u8]) -> io::Result<()> {
// 前処理(型に依存しない)
let header = create_header(data.len());
validate_data(data)?;
// 書き込み(型に依存する)
writer.write_all(&header)?;
writer.write_all(data)?;
Ok(())
}
// 改善後: 型に依存しない部分を分離
fn write_data<W: Write>(writer: &mut W, data: &[u8]) -> io::Result<()> {
let prepared = prepare_write(data)?; // 非ジェネリック
writer.write_all(&prepared.header)?;
writer.write_all(prepared.data)?;
Ok(())
}
fn prepare_write(data: &[u8]) -> io::Result<PreparedData> {
let header = create_header(data.len());
validate_data(data)?;
Ok(PreparedData { header, data })
}
リンク処理のボトルネック
コンパイルの最終段階であるリンク処理も、大きな時間を消費する工程です。Rustプロジェクトは多数のクレートに依存するため、それらのオブジェクトファイルを結合するリンク処理が肥大化します。
デフォルトのリンカ(GNU ldやApple ld)はシングルスレッドで動作するものが多く、現代のマルチコアCPUの恩恵を受けにくい構造です。後述する高速リンカへの切り替えで、この工程を劇的に短縮できます。
ビルド時間の計測と可視化
対策を講じる前に、まずボトルネックを正確に把握する必要があります。
cargo –timingsでクレート別の所要時間を把握する
# クリーンビルドの計測
cargo clean && cargo build --timings
# テストビルドの計測
cargo clean && cargo test --timings --no-run
--timingsオプションを付けると、ビルド完了後にHTMLレポートが生成されます。各クレートのコンパイル時間がガントチャート形式で表示され、並列度の低い箇所やビルドの直列ボトルネックを視覚的に発見できます。
self-profileでコンパイラの内部処理を分析する
さらに詳細な分析が必要な場合は、rustcのself-profile機能を使います。
# self-profileを有効にしてビルド
cargo rustc -- -Z self-profile
# 結果をChromiumのトレースビューアで確認
cargo install measureme
summarize summarize-*.mm_profdata
crox crox-*.mm_profdata
この機能で、型チェック・モノモーフィゼーション・LLVM最適化など、コンパイラ内部のどの処理フェーズがボトルネックになっているか詳細に把握できます。
高速リンカの導入
リンク時間の短縮は、最も即効性のある改善手法です。
Rust 1.90.0(2025年9月リリース)以降、x86_64-unknown-linux-gnuターゲットではlldがデフォルトリンカとして採用されました。これにより、多くのLinuxユーザーは特別な設定なしにリンク時間が約30%改善されています。さらに高速化を求める場合はmoldへの切り替えが有効です。
リンカの種類と特性
| リンカ | 開発元 | 対応OS | 並列処理 | ライセンス | 特徴 |
|---|---|---|---|---|---|
| GNU ld | GNU Project | Linux | 限定的 | GPL | 旧来のデフォルトリンカ |
| lld | LLVM Project | Linux / Windows / macOS | 対応 | Apache 2.0 | Rust 1.90以降Linux x86_64でデフォルト採用 |
| mold | rui314 | Linux / macOS | 高度な並列化 | MIT | 最高速クラスのリンカ。バージョン2.0でMITライセンスに変更 |
| wild | David Lattimore | Linux | 対応(開発中) | MIT / Apache 2.0 | Rust製の新しいリンカ。インクリメンタルリンクを目指す |
moldの導入手順
moldは現在のLinux環境で最も高速なリンカの一つです。
# Ubuntu/Debianの場合
sudo apt install mold
# またはGitHubリリースからインストール(バージョンは最新を確認)
# https://github.com/rui314/mold/releases
MOLD_VERSION=2.40.4
curl -L "https://github.com/rui314/mold/releases/download/v${MOLD_VERSION}/mold-${MOLD_VERSION}-x86_64-linux.tar.gz" | tar xz
sudo cp "mold-${MOLD_VERSION}-x86_64-linux/bin/mold" /usr/local/bin/
Rustプロジェクトから使用するには、.cargo/config.tomlに以下を追加します。
# .cargo/config.toml
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
moldの導入だけで差分ビルドのリンク時間が50〜80%短縮されるケースもあります。
macOS環境でのリンカ設定
macOS環境ではmoldの代わりにlldを使う方法が一般的です。
# .cargo/config.toml (macOS)
[target.aarch64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
lldを使うにはLLVMのインストールが必要です。
brew install llvm
sccacheによるコンパイルキャッシュ
sccacheはMozillaが開発したコンパイルキャッシュツールで、コンパイル結果をキャッシュして再利用することでフルビルドを高速化します。
セットアップ
# インストール
cargo install sccache
# 環境変数で有効化(.bashrcや.zshrcに追記)
export RUSTC_WRAPPER=sccache
グローバルに設定する場合は、Cargoの設定ファイルにも書けます。
# ~/.cargo/config.toml
[build]
rustc-wrapper = "sccache"
キャッシュの確認
sccache --show-stats
sccacheはローカルディスクだけでなく、S3やGCSなどのクラウドストレージをキャッシュバックエンドとして使えます。CI環境でチーム全体のビルドキャッシュを共有する運用が特に効果的です。
Cargo.tomlとビルドプロファイルの最適化
デバッグビルドの高速化設定
Cargo.tomlの[profile.dev]セクションで、開発時のコンパイル速度を改善できます。
[profile.dev]
opt-level = 0 # 最適化なし(デフォルト)
debug = "limited" # デバッグ情報を削減(行テーブルのみ)
incremental = true # インクリメンタルコンパイル有効(デフォルト)
codegen-units = 256 # 並列コード生成を最大化
[profile.dev.package."*"]
opt-level = 2 # 依存クレートだけ最適化(実行速度改善)
debug = "limited"の設定は、デバッグ情報の生成量を減らしてリンク時間を短縮します。変数名の表示が制限される代わりに、行番号ベースのデバッグは引き続き可能です。
リリースビルドの設定
[profile.release]
opt-level = 3 # 最大最適化
lto = "thin" # Thin LTO(速度とバイナリサイズのバランス)
codegen-units = 1 # 最大最適化のため単一に
strip = "symbols" # シンボル除去でバイナリ縮小
panic = "abort" # パニック時のunwindコードを省略
lto = "thin"はFull LTOより短時間でリンク時最適化を実行しつつ、実行バイナリの性能もほぼ同等に保てます。codegen-units = 1はコンパイル時間が長くなりますが、最も高品質な最適化が得られます。
ワークスペース構成の見直し
クレート分割戦略
大規模プロジェクトでは、ソースコードを複数のクレートに分割することでインクリメンタルビルドの効率が向上します。
my-project/
├── Cargo.toml # workspace設定
├── crates/
│ ├── core/ # ビジネスロジック
│ ├── api/ # HTTPハンドラ
│ ├── db/ # データベース層
│ └── cli/ # CLIエントリーポイント
ポイントは、変更頻度の高いコードと安定したコードを別クレートに分けることです。coreを変更してもdbの再コンパイルが不要なら、差分ビルドの時間が大幅に短縮されます。
不要な依存クレートの整理
依存クレートが増えるほどコンパイル時間は長くなります。使っていないクレートの除去や、featureフラグの絞り込みが有効です。
# 未使用の依存を検出
cargo install cargo-udeps
cargo +nightly udeps
また、重量級のクレートを軽量な代替に切り替える方法もあります。
| 元のクレート | 軽量な代替 | 効果 |
|---|---|---|
| serde + serde_derive | miniserde | コンパイル時間を大幅短縮 |
| reqwest | ureq | async不要な場合に有効 |
| chrono | time | 依存が少なくビルドが速い |
ただし、機能面のトレードオフがあるため、プロジェクトの要件に応じて判断してください。
Docker/CI環境でのビルド高速化
cargo-chefによるDockerレイヤーキャッシュの活用
Dockerイメージのビルドでは、ソースコードを変更するたびに依存クレートの再コンパイルが発生しがちです。cargo-chefを使うと、依存クレートのビルドをDockerレイヤーとしてキャッシュできます。
# ---- plannerステージ ----
FROM rust:1.93 AS planner
WORKDIR /app
RUN cargo install cargo-chef
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
# ---- builderステージ ----
FROM rust:1.93 AS builder
WORKDIR /app
RUN cargo install cargo-chef
COPY --from=planner /app/recipe.json recipe.json
# 依存クレートだけビルド(キャッシュが効く)
RUN cargo chef cook --release --recipe-path recipe.json
# ソースコードをコピーしてビルド
COPY . .
RUN cargo build --release --bin my-app
# ---- 実行ステージ ----
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/my-app /usr/local/bin/
CMD ["my-app"]
ソースコードを変更してもrecipe.json(依存関係の情報)が変わらなければ、cargo chef cookのレイヤーキャッシュが再利用されます。
CI環境でのキャッシュ戦略
GitHub Actionsなどでは、target/ディレクトリと~/.cargo/registryのキャッシュが定番です。
# GitHub Actions の例
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
sccacheとの併用でさらに効果を発揮します。sccacheのキャッシュをS3やGCSに置けば、異なるCIランナー間でもキャッシュが共有されます。
rust-analyzerの応答を改善する
IDE上でのrust-analyzerの動作が重い場合も、開発効率に大きく影響します。
設定で応答速度を改善する
VS Codeのsettings.jsonで以下の設定を調整できます。
{
"rust-analyzer.check.command": "clippy",
"rust-analyzer.cargo.buildScripts.overrideCommand": null,
"rust-analyzer.procMacro.attributes.enable": true,
"rust-analyzer.diagnostics.disabled": ["unresolved-proc-macro"],
"rust-analyzer.cargo.features": []
}
特にcargo.featuresで不要なfeatureを無効化すると、チェック対象のコード量が減り、応答速度が改善します。
cargo checkとcargo buildのキャッシュ分離問題
2025年の公式調査でも指摘されていますが、cargo checkとcargo buildはビルドキャッシュを共有しません。cargo checkを実行した後にcargo buildを行うと、チェック済みのクレートも再コンパイルされます。
開発中はcargo checkでエラーチェックを素早く回し、ビルドが必要なタイミングでのみcargo buildを実行するワークフローが現実的です。cargo-watchを使えば、ファイル保存時に自動でチェックを走らせることもできます。
cargo install cargo-watch
cargo watch -x check
Rustコンパイラ自体の改善動向
Craneliftバックエンド
LLVMに代わるコード生成バックエンドとして、Craneliftの開発が進んでいます。Craneliftはコンパイル速度を重視した設計で、デバッグビルドにおいてLLVMより大幅に速いコード生成が期待されています。
# Craneliftバックエンドの試用(nightly必要)
rustup component add rustc-codegen-cranelift-preview --toolchain nightly
CARGO_PROFILE_DEV_CODEGEN_BACKEND=cranelift cargo +nightly build -Zcodegen-backend
生成されるコードの実行速度はLLVMに劣りますが、開発時のイテレーション速度を重視する場合に検討の価値があります。
並列フロントエンド
Rustコンパイラのフロントエンド(パース・型チェック・借用チェック)を並列化する取り組みも進行中です。nightlyでは-Z threads=Nオプションで試験的に有効化できます。
RUSTFLAGS="-Z threads=8" cargo +nightly build
Nicholas Nethercoteのブログによると、2025年にはLLVM 21への更新で命令実行数(icount)が平均1.70%削減され、一時変数スコープの最適化で最大3%の改善が得られるなど、コンパイラ本体でも継続的な高速化が進んでいます。
wildリンカ
moldを超える速度を目指すRust製リンカ「wild」も注目の存在です。David Lattimoreが開発しており、一部ベンチマークではmoldの2倍の速度を記録しています。将来的にインクリメンタルリンクへの対応も計画されています。
まだ開発段階のため本番利用には時期尚早ですが、Rustのビルド高速化エコシステムが着実に進化していることを示しています。
高速化テクニック一覧と期待効果
| 対策 | 対象 | 導入の手間 | 期待される短縮効果 |
|---|---|---|---|
| moldリンカ導入 | リンク時間 | 低 | 差分ビルド50〜80%短縮 |
| sccache導入 | フルビルド | 低 | 2回目以降のフルビルド大幅短縮 |
debug = "limited" | デバッグ情報 | 低 | リンク時間10〜30%短縮 |
| Craneliftバックエンド | コード生成 | 中 | デバッグビルド20〜40%短縮 |
| クレート分割 | インクリメンタルビルド | 高 | 差分ビルド時間を局所化 |
| cargo-chef | Dockerビルド | 中 | 依存未変更時のビルドをスキップ |
| 依存クレート整理 | 全体 | 中 | クレート数に応じて改善 |
| ジェネリクス最適化 | コンパイル全体 | 高 | LLVM IR量に応じて改善 |
まとめ
Rustのコンパイルが遅い原因は、LLVM最適化・モノモーフィゼーション・リンク処理の3つに集約されます。
最も手軽で効果が大きいのはmoldリンカの導入とsccacheの設定です。.cargo/config.tomlへの数行の追加だけで、体感できる速度向上が得られます。プロジェクト規模が大きい場合は、ワークスペースのクレート分割や依存クレートの見直しも検討してください。
Rustコンパイラ自体もCraneliftバックエンドや並列フロントエンドなどの改善が進行中で、バージョンを上げるだけで恩恵を受けられる場面も増えています。cargo --timingsで現状を計測し、効果の大きい箇所から順に対策を適用するアプローチが最も効率的です。