Rustのビルド待ち時間を削る実践レシピ。高速リンカとsccache、cargo-nextestで開発サイクルを加速させる
目次
開閉
Webアプリケーションのパフォーマンス改善や、社内向けの高速なCLIツール作成などで、実務にRustを取り入れる現場が増えてきました。メモリ安全性と圧倒的なランタイム速度は頼もしい限りですが、日常的にコードを書く中でどうしても向き合わなければならないのが「ビルド待ち時間」の存在です。
コードを数行修正して cargo test や cargo run を叩いたとき、ターミナルで十数秒から数十秒待たされる。このわずかな待ち時間が、開発のリズムや集中力を少しずつ削ぎ落としていきます。
Rustのコンパイルが重くなる要因は、厳格な型推論や借用チェック、マクロ展開だけではありません。実は実務で手を動かしているとき、時間の多くを奪っているのは 「最終成果物を作るリンク処理」 と 「依存クレートの再コンパイル」、そして 「逐次的なテスト実行」 です。
今回は、既存のソースコードには一切手を加えず、手元の環境設定とツールの導入だけでインクリメンタルビルドとテストを大幅に軽快にする「3つのアプローチ」をまとめました。
1. ボトルネックの正体:なぜコード修正後のビルドは遅いのか
Rustで開発していて最も頻度が高いのは、「関数の中身を1〜2行書き換えて動作を確かめる」というインクリメンタルな作業です。
このとき、cargo check(型チェックと構文解析)だけであれば数秒で終わることが多いのですが、実際にバイナリを生成したりテストを実行したりしようとすると、途端に時間が伸びます。
その大きな要因が リンク(Linking) です。
Rustは静的リンクを基本とし、依存している膨大なライブラリやランタイムを単一のバイナリに結合します。標準のリンカ(Linuxの GNU ld や macOSの旧リンカ、Windowsの link.exe)は堅牢ですが、大規模なオブジェクトファイルを扱う際の並列化が弱く、コードを1行変えただけでも数十秒のリンク待ちが発生してしまうのです。
この「リンク待ち」と「テスト実行の待ち時間」を解消するために、以下の3段構えで開発環境を整えていきます。
- 高速リンカへの差し替え: リンク処理を並列化して数秒以内に終わらせる
- sccacheの導入: 依存クレートのビルド結果をローカル全体でキャッシュする
- cargo-nextestの導入: テストをプロセス単位で最適に並列実行する
2. 第1の手:高速リンカへの差し替え(OS別の最適解)
最も劇的な効果を実感しやすいのが、リンカの差し替えです。ただし、お使いのOSによって最適な選択肢が大きく異なります。ここを混同すると「ツールを入れたのに動かない」「不要な巨大パッケージを入れてしまった」という混乱に繋がりやすいため、まずはOS別の推奨アプローチを整理しておきます。
| OS環境 | 推奨アプローチ | 理由 |
|---|---|---|
| Linux (Ubuntu / Arch等) | mold に差し替え | 圧倒的に高速(GNU ldの数倍〜十数倍)。現在最も効果が高い |
| Windows (MSVC) | lld-link に差し替え | LLVMの高速リンカ。MSVCの link.exe に比べて大幅に短縮 |
| macOS (Apple Silicon) | OS標準(Xcode 15+)のまま | Xcode 15以降で新リンカが導入され高速化済み。追加ツール不要 |
リンカのインストール
LinuxおよびWindows環境で必要なツールをインストールします(※macOS環境の方は追加インストール不要です)。
# Ubuntu / Debian (Linux)
sudo apt update && sudo apt install -y mold clang
# Arch Linux
sudo pacman -S mold clang
# Windows (PowerShell / Scoop経由でLLVMを導入)
scoop install llvm
# ※winget をお使いの場合は: winget install LLVM.LLVM~/.cargo/config.toml でのグローバル設定
プロジェクト個別の Cargo.toml を汚さず、マシン全体で高速リンカを有効にするため、ホームディレクトリ配下の ~/.cargo/config.toml(Windowsの場合は %USERPROFILE%\.cargo\config.toml)に設定を記述します。
# Linux (x86_64) 向け: mold を使用
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
# Linux (ARM64 / aarch64) 向け: mold を使用
[target.aarch64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
# Windows (x86_64 MSVC) 向け: lld-link を使用
[target.x86_64-pc-windows-msvc]
linker = "lld-link"
# ※macOS (Apple Silicon) は Xcode 15以降の標準リンカで十分高速なため、設定不要です手元での差分ビルド検証
効果を客観的に確認するため、中規模なWeb APIプロジェクト(REST APIサーバー、依存クレート約120個、総コード行数約1.5万行)を対象に、コントローラー層の処理を1行変更した際のインクリメンタルビルド時間を計測しました。
| 項目 | 検証環境のスペック |
|---|---|
| OS | Ubuntu 24.04 LTS (x86_64) / Linux 6.8 |
| CPU | AMD Ryzen 7 7840HS (8コア / 16スレッド) |
| メモリ | 32 GB DDR5 |
| Rust | rustc 1.81.0 (stable) |
| リンカ | GNU ld 2.42 vs mold 2.32.0 |
# 標準リンカ(GNU ld)でのビルド
$ touch src/main.rs && time cargo build
Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.42s
real 0m9.452s
# mold 適用後でのビルド
$ touch src/main.rs && time cargo build
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.83s
real 0m1.861s
9秒以上かかっていたインクリメンタルビルドが 約1.8秒 にまで短縮されました。1回あたりの差は7秒程度ですが、1日に何十回もコンパイルを繰り返す現場では、集中力の維持において決定的な違いを生みます。
3. 第2の手:sccache によるコンパイルキャッシュの共有
ブランチを切り替えたり、cargo clean を叩いたりした際、tokio や serde、axum といった巨大な依存クレート群をゼロから再コンパイルさせられてうんざりした経験はないでしょうか。
Mozillaが開発している sccache(Shared Compilation Cache)は、C/C++の ccache に似た仕組みで、コンパイルされた中間アーティファクトをハッシュ値とともに保存・再利用してくれます。
sccache の導入手順
Rustのツールチェイン管理には cargo-binstall を使うと、コンパイル待ちなしで事前ビルド済みバイナリを即座に導入できます。
# cargo-binstall 経由でバイナリを高速インストール
cargo binstall sccache
# または通常の cargo install
cargo install sccache --locked
インストール後、こちらも ~/.cargo/config.toml に rustc-wrapper として指定します。
[build]
rustc-wrapper = "sccache"キャッシュの動作と統計確認
設定後に cargo build を実行すると、バックグラウンドで sccache デーモンが立ち上がり、コンパイル結果を自動的にキャッシュストレージ(デフォルトはローカルディスク)に蓄積します。
キャッシュの効き具合は sccache --show-stats でいつでも確認できます。
$ sccache --show-stats
Compile requests 342
Compile requests executed 28
Cache hits 314
Cache misses 0
Cache timeouts 0
Cache errors 0
Cache hit rate 91.81 %
Supported/executed compiles 100 %
Non-cacheable compiles 0
Cache size 1.2 GiB
Max cache size 10.0 GiB
キャッシュヒット率が90%を超えていれば、別ブランチへの移動や一時的な作業ディレクトリの作成時でも、重い依存クレートの再コンパイルがほぼ瞬時にスキップされます。
4. 第3の手:cargo-nextest でテスト実行をプロセス並列化する
コード修正後のリンクとキャッシュが整ったら、最後のボトルネックである「テストの実行速度」を改善します。
Rust標準の cargo test は、1つのテストバイナリ内でマルチスレッドを使ってテスト関数を実行します。しかし、この方式には実務上の課題がいくつかあります。
- テスト間でグローバル状態(環境変数や一時ファイル、DB接続など)を共有していると、スレッド競合でテストが不安定になりやすい
- 途中でハングしたテスト関数があると、全体の進行が止まり原因特定が難しい
- 出力ログが混ざり合い、どのテストが失敗したのか追うのにスクロールが必要になる
Meta社が開発・オープンソース化した cargo-nextest は、これらの課題を根本から解決する次世代テストランナーです。
nextest の導入と実行
こちらもバイナリインストールが可能です。
cargo binstall cargo-nextest
使い方はきわめてシンプルで、普段の cargo test を cargo nextest run に置き換えるだけです。
$ cargo nextest run
Compiling my-api-service v0.1.0 (/path/to/project)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.42s
Starting 28 tests across 3 binaries
PASS [ 0.008s] my-api-service::models user_deserialization_test
PASS [ 0.012s] my-api-service::handlers health_check_returns_200
PASS [ 0.015s] my-api-service::auth token_validation_success
PASS [ 0.021s] my-api-service::db connection_pool_acquire
PASS [ 0.034s] my-api-service::api create_user_endpoint
...
------------
Summary [ 0.142s] 28 tests run: 28 passed, 0 skippedなぜ nextest は速く、壊れにくいのか
cargo-nextest の最大の特徴は、「各テストを独立したプロセスとして分離し、マシンコアを使い切って並列実行する」 点にあります。
- スレッドセーフティの心配が不要: 各テストが別プロセスで動くため、環境変数の書き換え(
std::env::set_var)や静的変数の操作が他のテストに干渉しません。 - きめ細かいタイムアウト検知: テストごとに「スローテスト判定(例: 5秒以上)」や「タイムアウトによる強制停止」を設定できるため、デッドロックでCIが30分止まるような事故を未然に防げます。
- 失敗箇所の明瞭なサマリー: 失敗したテストの標準出力・エラートレースだけが末尾に整理されて表示されるため、大量のログからエラーを探す認知負荷がゼロになります。
5. コピペで使える ~/.cargo/config.toml まとめ
ここまで紹介した高速リンカと sccache の設定を1箇所にまとめた設定例です。
ホームディレクトリ(~/.cargo/config.toml または %USERPROFILE%\.cargo\config.toml)に配置するだけで、手元のすべてのRustプロジェクトで自動的に高速化の恩恵を受けられます。
# -------------------------------------------------------------
# ~/.cargo/config.toml - ローカル開発用グローバル高速化設定
# -------------------------------------------------------------
[build]
# コンパイルキャッシュの有効化
rustc-wrapper = "sccache"
# -------------------------------------------------------------
# OS別リンカ設定
# -------------------------------------------------------------
# Linux (x86_64): mold リンカ
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
# Linux (ARM64 / aarch64): mold リンカ
[target.aarch64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
# Windows (MSVC): lld-link リンカ
[target.x86_64-pc-windows-msvc]
linker = "lld-link"
# ※macOS (Apple Silicon / Xcode 15+):
# 標準リンカが最初から高速化されているため、ターゲット設定は不要(記述なしでOK)です
待ち時間を削ることで、思考のリズムを取り戻す
Rustのコンパイラが提供してくれる安全性や型システムの恩恵は計り知れません。しかし、思考と試行のサイクルがあまりに重いと、「とりあえず書いて動かしてみる」というアジャイルな実験が億劫になりがちです。
- 高速リンカ で最後の数秒〜十数秒の詰まりを解消する
- sccache でクレート再構築の無駄をローカル全体から排除する
- cargo-nextest でテストを堅牢かつ軽快に回す
この3つの手当てを行うだけで、コードを書き換えてからフィードバックを得るまでの時間は半分以下に短縮されます。
並行して複数ブランチでの修正や動作確認を行う機会が多い方は、ディレクトリを行き来する際の再ビルドを防ぐ git worktree と組み合わせると、さらに開発のストレスを減らすことができます。
開発環境は一度整えてしまえば、以後のすべてのプロジェクトで静かに効果を発揮し続けます。Rustのビルド時間に少しでも引っかかりを感じていた方は、ぜひ手元の環境で試してみてください。