1. Rustの安全性とビルド時間
2026年5月、JavaScriptランタイムのBunがZigからRustへの書き換えを決めました。
直接の引き金は、Claude Codeを悩ませていたメモリリークです。
Rustのborrow checkerがコンパイル時にメモリ問題を検出する、という判断でした1。
Rustはいま、メモリ安全・高性能・モダンな型システムを兼ね備えた言語として急速に普及しています。
C/C++の置き換え候補として語られることも多い。
ところが、コード量が3万行を超えたあたりから、開発ループに異変が起きます。
コードの修正は1分で終わるのに、ビルドに10分かかる。
worktreeをいくつか開けば、target directoryだけで100GBを超えます。
これは偶然ではなく、Rustが何をコンパイル時に解決しようとしているかの必然的な結果です2。
初期CコンパイラやSBCLを並べて、その構造が見てみます。
1.1. コンパイラとは何を解決しているのか
コンパイラが行う仕事は大きく分けると、字句解析・構文解析・型検査・最適化・コード生成・リンクです。
言語ごとに「どのフェーズが存在するか」「どこを人間に任せるか」が根本的に違います。
もう一つの軸は、「修正のたびに何を再処理するか」です。
ファイル単位か、関数単位か、crate単位か。
この単位の違いが、規模が大きくなったときの開発ループの重さを決定します。
2. Rustのコンパイル(整合性を閉じてから進む)
Rustのコンパイルは、フェーズごとに見ても重いです。
しかも、それぞれのフェーズが互いに依存しています。
まずCargoが依存関係を解決します。
crate、feature、build script、proc macro、test targetを含めて何をどうビルドするかを決めます。
この時点で、単なる1ファイルのコンパイルではなくなっています。
次にrustcが構文解析し、macro expansionを行います。
RustのmacroはCのプリプロセッサより構文に深く関わります。
proc macroはRustコードで書かれたコンパイル時プログラムで、コンパイル中に別のRustプログラムを動かす構造になっています。
その後、HIRと呼ばれる高水準中間表現へ変換し、型推論・trait解決・ライフタイム推論を行います。
ここでRustの型システムの核心が動きます。
さらにMIRと呼ばれる中水準中間表現へ変換し、borrow checkが入ります。
所有権・借用・ライフタイムの整合性をここで証明します。
Cなら未検査、Common Lispなら多くは実行時の問題になるところを、Rustはコンパイル時に止めます3。
2.1. 単相化——genericの代金
borrow checkの後、genericのmonomorphizationが行われます。
単相化と呼ばれるこの仕組みは、genericコードを実際に使われた型ごとに具体的な機械語コードへ展開します。Vec<u64>とVec<String>では別のVec用コードが生成されます4。
fn double<T: std::ops::Mul<Output = T> + Copy>(x: T) -> T {
x * x
}
// u32 用のコードが生成される
double(3u32);
// f64 用のコードが別に生成される
double(3.0f64);Code language: Rust (rust)
実行時にどの型か判断するコストを払わず、型ごとに専用コードを持つ。
それがRustの実行速度の源泉です。
ただしその代金は、コンパイル時のコード生成量と最適化コストとして現れます。
最後にLLVM IRへ変換し、LLVMが最適化して機械語を生成し、リンクします。
Rustの抽象化はLLVMに渡る時点でかなり展開されているので、LLVMは大量の具体化されたコードを最適化することになります。
フェーズをまとめると、こうなります。
- Cargo依存解決
- macro展開(declarative macro と proc macro)
- HIR変換
- 型推論・trait解決・ライフタイム推論
- MIR変換
- borrow check
- 単相化(型ごとにコードを展開)
- LLVM最適化・コード生成
- リンク
- test target生成(cargo testの場合)
3. 軽いコンパイルとの違い
3.1. 初期Cコンパイラ(薄い変換器)
Dennis RitchieがCを設計したのは、Unixを書くためでした。
1973年にUnixカーネルをCで書き直せる程度の言語として完成しています5。
Cは「薄い高級アセンブラ」として育った言語であり、コンパイラの仕事は意図的に少なく設計されています。
初期Cコンパイラのフェーズを順に見ると、ソースを読んで構文解析し、型をある程度確認し、アドレス計算や演算や分岐を機械語に落として、リンクします。
それだけです。
型処理は比較的軽いです。
Cの型は、Rustのtrait境界やborrow checkerのように「プログラム全体の所有関係を証明する」ものではありません。
マクロはプリプロセッサによる字句的な置換で、構文に深く関わらない。
genericもなく、所有権もありません。
コンパイルの単位は.cファイルです。
修正したファイルだけ再コンパイルして、既存の.oファイルとリンクすれば動きます。
最終リンクは全オブジェクトを見るので厳密にはO(n)に近いですが、意味的な再検査は修正ファイルに閉じます。
開発ループ全体では体感としてO(1)に近い。
「コンパイラが賢くない代わりに速い」「言語仕様が機械語に近い」という構造は、Dennis Ritchieが意図した設計であり、Unixを書くという目的には十分でした6。
3.2. SBCLのコンパイル(関数単位)
SBCLは、Common Lispの処理系で、高性能なネイティブコンパイラを持ちます。
内部ではIR1という制御・データフローグラフやIR2を通り、型推論・最適化・VOPへの変換を経てネイティブコードを生成します。VOPは仮想機械命令に近い表現です7。
フェーズの数だけ見れば、初期Cコンパイラより高度です。
それでも開発ループが軽く感じるのは、コンパイルの単位が「関数」だからです。
SLIMEというCommon Lisp用のエディタ環境でC-c C-cを押すと、その関数定義だけが再コンパイルされ、動いているLispイメージに差し替わります。
Rustのようにcrate全体・依存crate・test harness・linkまで毎回まとめて考えるわけではありません。
(defun foo (x)
(* x 2))Code language: Lisp (lisp)
このfooを直してC-c C-cすれば、今動いているイメージのfooだけが入れ替わります。
プロジェクト全体を再構築するのではなく、動いている世界の一部を置換する。
これがSBCLの開発ループの軽さの正体です。
また、SBCLの型宣言は、Rustとは役割が異なります。
SBCLでは型宣言を最適化ヒントとして、あるいは満たすべき条件として扱い、証明できないものは実行時にチェックされます。
Rustのように「コンパイル時に通らなければ拒否する」型システムとは、コストを払う場所が違います。
コンパイラ自体は高度でも、再コンパイルの単位が小さく、実行中の環境に直接差し替えられる。それがSBCLの開発ループが軽い理由です8。
4. 修正範囲より再処理が多い
3言語の差は、変更の影響伝播という観点で整理できます。
1箇所を修正したとき、その変更がコンパイラの依存グラフをどこまで伝播するかが、開発ループの重さを決めます。
| 処理系 | 伝播範囲 |
|---|---|
| Rust | O(m)〜O(n) |
| 初期C | O(1) |
| SBCL | O(1) |
nはプロジェクト全体のコード量、mは修正によって再処理が波及するcrateの大きさです。
- Cでは依存グラフがファイル単位で閉じています。
修正した.cファイルを再コンパイルして既存の.oとリンクすれば済むため、変更はほぼ伝播しません。 - SBCLでは修正した関数だけがイメージに差し替わります。
関数間の依存は実行時に解決されるため、再コンパイルの伝播範囲は関数1つです。 - Rustでは、修正箇所がpublic APIやtrait定義、generic関数、proc macroに触れると、依存する全crateへ変更が伝播します。
これがrecompilation cascadeと呼ばれる現象です。
修正量は1行でも、trait boundやライフタイム境界の変更は意味的依存グラフを広く刈り取り、crate全体の再検証・再生成を引き起こします。
ライブラリ開発ではpublic APIとtraitが中心になりやすいため、カスケードが起きやすい構造になっています。
4.1. ディスク肥大も同じ構造から来る
target directoryが数十GBになる理由も、この構造に根ざしています。
単相化によって型ごとに展開された具体コードが中間生成物として積み上がります。
差分コンパイルのためのincremental cacheも保存され、test targetはlibrary・binary・benchmarkとほぼ別物としてビルドされます。
worktreeを複数開けば、それぞれに巨大なtargetができます。
消す前提の生成物が積み上がるこの構造は、Cargoのビルドキャッシュ設計として知られており、2026年時点でも改善が議論されています9。
4.2. どこでコストを払うか
3言語の違いは「コンパイラが何を解決するか」ではなく、「どこでコストを払うか」の違いとして見るとわかりやすくなります。
Cは、コンパイル時コストを最小にして、実行時の危険と人間の注意にコストを回します。
メモリ破壊系の問題は実行時に起きます。
それを防ぐのはプログラマの責任です。
SBCLは、コンパイラ自体は高度でもREPLと実行時環境にコストを分散し、関数単位の差し替えで開発速度を保ちます。
型の証明をコンパイル時に完結させず、実行時チェックと宣言ベースの最適化に寄せる設計です。
Rustは、実行時コストと実行時エラーを減らすために、コンパイル時へ大きく前払いします。
メモリ安全性・ゼロコスト抽象化・genericの実行速度を、すべてコンパイル時の仕事で賄います。
Rustの重さは設計上の欠陥ではありません。
「実行時に安全でゼロコストな抽象化を保証する」という約束の代金です。
ただしその代金は、規模が大きくなるほど急激に膨らみます。修正量は定数でも、再検証・再生成・再リンクの対象がプロジェクト全体に広がっていくからです10。
Bunの件は、その構造を大規模に体現しています。
Zigのメモリ問題を解決しようとRustを選んだ直後に、今度はRustのコンパイル時間とcrate構造の問題が現れました。
Rustは「実行時の問題をコンパイル時に前払いする」言語ですが、その前払いが開発ループそのものを重くします。
小さいコードでは快適で、大きくなると壊れていく——それがRustの正直な姿です。
- 2026年5月14日、PR #30412がBunのmainブランチにマージされました。6,755コミット・1,009,257行のRustコードが追加されています。Jarred Sumnerはメモリ安全性を移行の主な動機として挙げています。 – Anthropic’s Bun Rust rewrite merged at speed of AI
- 2025年のRustコンパイラ性能調査では、「小さな変更後の差分リビルドが長すぎる」ことが回答者の最も多い不満として挙がっています。「リビルドにかかる時間をコードベースのサイズではなく、変更量に依存させることが目標だが、まだそこには至っていない」と公式ブログが認めています。 – Rust compiler performance survey 2025 results
- MIRはRFC 1211で導入されました。rustc公式ガイドでは「HIRから生成されるRustの簡略化された表現で、borrow checkerや最適化・コード生成に使われる」と定義されています。borrow checkerはMIRに対して動作し、use-after-freeやデータ競合をコンパイル時に検出します。 – The MIR (Mid-level IR) – Rust Compiler Development Guide
- rustc-dev-guideでは「genericでないコードと単相化されたコードを別のcodegen unitに割り当てる」設計が説明されています。この仕組みにより実行時のディスパッチコストを排除しますが、型の数に比例してコンパイル時の生成コード量が増加します。 – Monomorphization – Rust Compiler Development Guide
- Ritchie自身の回顧録によると「1973年初めに現代Cの基本が完成し、その夏にPDP-11向けUnixカーネルをCで書き直した」とあります。Cはもともとベル研究所でUnixユーティリティを書くために開発されました。 – The Development of the C Language – Dennis M. Ritchie
- 1973年にUnixカーネルをCで書き直したことで、Cはハードウェアへの移植性をもたらしました。以降のUnixツール群も順次Cに置き換えられています。Cが移植性を持つのは、アセンブリコードをハードウェアごとに書き直す必要がなくなったからです。 – Dennis Ritchie and Ken Thompson: The Bell Labs Pair Who Built Unix and C
- SBCLのソースコードでは、IR1は「コンパイラの第一中間表現」、IR2は「仮想機械向けの第二中間表現」として定義されています。IR1フェーズでは型推論と高水準最適化が行われ、IR2フェーズでVOPへの変換と機械語生成が行われます。 – SBCL Internals: IR2 Conversion
- SBCLのマニュアルでは、型宣言はコンパイラへの最適化ヒントとして機能し、証明できない型は実行時にassertionとしてチェックされると説明されています。Rustの静的型システムとは異なり、型の整合性を実行時に確認する余地を残しています。 – SBCL User Manual
- Cargoのtarget directoryの圧縮・縮小についてはGitHubのissue #16462で議論が続いています。また、Rustコンパイラ性能調査2025では「derive proc macroの展開結果がキャッシュされていない」など、差分コンパイルの改善余地が具体的に指摘されています。 – Compress target directory files – rust-lang/cargo #16462
- Rustコンパイラ性能調査2025では「リンクフェーズは常にゼロから実行されるため、他のフェーズと異なり差分化が難しい」と指摘されています。x86_64 Linux向けにLLDリンカへの切り替えが進められており、リンク時間の短縮が期待されています。 – Rust compiler performance survey 2025 results