「Rustのコンパイルは遅い」はどの待ち時間の話なのか検証した
◆ 目次
#Rust
「Rustはコンパイルが遅い」と言う人と「言われるほど遅くない」と言う人の話は、だいたい噛み合わない。どちらかが嘘をついているとは思わない。コンパイルという一語に性質の違う待ち時間がいくつも入っていて、それぞれ別の待ち時間を見て話しているのではないかと考えている。それを確かめるために、Rustのリポジトリ3つとC++のエディタ1つで、待ち時間を種類ごとに測った。
先に立場を書いておく。自分は、人が見落とすものを型やコンパイラが止めてくれる仕組みを好んで使う人間で、これはそういう人間のポジショントークとして読んでほしい。とはいえRustを擁護するために測ったわけではないので、遅かったところは遅いと書く。
「遅い」の中身
Rustの公式の調査の結果を読むと、一番多かった不満は小さな変更の後の再ビルドを長く待たされることだったそうだ。
Waiting too long for an incremental rebuild after making a small source code change was by far the most common complaint
Rustの公式の調査の結果 https://blog.rust-lang.org/2025/09/10/rust-compiler-performance-survey-2025-results/
一方で、クリーンビルドを大きな障害と答えた人は2割ほどだったらしい。
Around 20% of participants responded that clean builds are a significant blocker for them.
Rustの公式の調査の結果 https://blog.rust-lang.org/2025/09/10/rust-compiler-performance-survey-2025-results/
同じRustでも、どの段階を待っているかで時間は大きく違うはずだと考えて、待ち時間を次の3つに分けた。
- 依存クレートまで含めたクリーンビルド。CIや、cloneした直後に待つ
- 1行直した後の型検査だけ。
cargo checkがこれで、コード生成をしない - 1行直した後の再ビルドと実行。
cargo buildやcargo testで、型検査の後にコード生成とリンクが入る
コードを書くのが人でもAIでも、直した後にこれらのコマンドを回して結果を待つことは変わらない。
cargo checkがコード生成を飛ばすことは、cargoのドキュメントに書いてある。
This will essentially compile the packages without performing the final step of code generation, which is faster than running
cargo build.
cargo checkのドキュメント https://doc.rust-lang.org/cargo/commands/cargo-check.html
測ったもの
- Rustからは規模と形の違うripgrep、helix、mdBookの3つを選んだ。ripgrepは小さめのCLI、helixはクレートの多いエディタ、mdBookはasyncとproc macroを使う中規模のワークスペースである。proc macroはコンパイルのときにコードを生成するマクロのこと
- C++からはhelixと同じモーダルなエディタのKakouneを比べる相手として選んだ
- 1行の変更は3種類入れた
- 実行ファイルを作るクレートの関数の中身
- 多くのクレートから依存されるクレートの関数の中身。ripgrepではgrep-searcher、helixではhelix-core、mdBookではmdbook-coreを変えた
- その同じクレートの公開関数のシグネチャ。引数の型を
Tからimpl Into<T>に変えた。呼び出す側を書き換えずにコンパイルが通るようにするためである
- 変更のたびに
cargo check、cargo build、cargo test --no-runの時間を測った。最初の1回は捨てて、その後の5回の中央値を取った - リンカはデフォルト (GNU ld) 、lld、moldの3つを切り替え、リンカが動いていた時間も別に記録した
- 環境はdockerの中で、CPU 2個、メモリ4GB、aarch64、rustc 1.99.1である。aarch64なので、デフォルトのリンカはGNU ldになる
コミットの固定、変更を入れた場所、再現の手順はリポジトリに置いた。
rust-compile-wait-breakdown https://github.com/kbwo/rust-compile-wait-breakdown
待ち時間が長いかどうかの目安には、公式の調査が問題にしている10秒を借りた。
the fact that 55% of respondents have to wait more than ten seconds for a rebuild is far from ideal.
Rustの公式の調査の結果 https://blog.rust-lang.org/2025/09/10/rust-compiler-performance-survey-2025-results/
結果
1行の変更の後の待ち時間は次のとおりで、数字は5回の中央値、単位は秒、リンカはデフォルト (GNU ld) である。
| 変更 | コマンド | ripgrep | mdBook | helix |
|---|---|---|---|---|
| 実行ファイルのクレートの関数の中身 | check | 0.10 | 0.12 | 0.41 |
| 実行ファイルのクレートの関数の中身 | build | 0.83 | 2.02 | 7.92 |
| 実行ファイルのクレートの関数の中身 | test --no-run | 1.31 | 2.01 | 9.77 |
| 多くから依存されるクレートの関数の中身 | check | 0.21 | 0.40 | 1.08 |
| 多くから依存されるクレートの関数の中身 | build | 0.94 | 2.45 | 9.37 |
| 多くから依存されるクレートの関数の中身 | test --no-run | 1.27 | 4.27 | 10.91 |
| 多くから依存されるクレートのシグネチャ | check | 0.19 | 0.43 | 1.15 |
| 多くから依存されるクレートのシグネチャ | build | 1.03 | 3.44 | 11.21 |
| 多くから依存されるクレートのシグネチャ | test --no-run | 1.27 | 5.62 | 14.40 |
クリーンビルドは次のとおりで、こちらも5回の中央値である。
| リポジトリ | debug | release |
|---|---|---|
| ripgrep | 6.93 | 18.11 |
| mdBook | 25.00 | 50.72 |
| helix | 56.09 | 126.17 |
全条件の中央値、最小、最大と、リンカが動いていた時間は集計のファイルにある。
results/summary.md https://github.com/kbwo/rust-compile-wait-breakdown/blob/main/results/summary.md
cargo checkは3つのリポジトリのどの変更でも中央値が1.2秒以内だった。一番遅かったhelixのシグネチャの変更でも、中央値1.15秒、5回の最大でも1.22秒である。
buildとtestはどの組み合わせでもcheckよりはっきり長かった。helixでは、helix-coreの中身を変えた後のtestが10.91秒、シグネチャを変えた後のbuildが11.21秒、testが14.40秒で、10秒を超えた。checkより長くかかった分のうちリンカが動いていた時間の割合は、3つのリポジトリ、3種類の変更、buildとtestを掛け合わせた18の組み合わせで58%から96%だった。
クリーンビルドはripgrepのdebugの6.93秒を除いてすべて10秒を超え、helixのreleaseは2分を超えた。
シグネチャを変えたとき
多くのクレートから依存されるクレートの公開関数のシグネチャを変えれば、依存する側のクレートの型検査がやり直しになって、checkでも10秒を超える場面があるだろうと考えて測ったが、そうはならなかった。シグネチャを変えた後のcheckはripgrep 0.19秒、mdBook 0.43秒、helix 1.15秒で、中身だけを変えたときの0.21秒、0.40秒、1.08秒とほとんど変わらない。
cargoの出力を見ると、helixではhelix-coreの関数の中身だけを変えたときにも、helix-coreを含む7つのクレートがすべて検査し直されていた。
Checking helix-core v25.7.1 (/tmp/w/helix/helix-core)
Checking helix-vcs v25.7.1 (/tmp/w/helix/helix-vcs)
Checking helix-lsp v25.7.1 (/tmp/w/helix/helix-lsp)
Checking helix-dap v25.7.1 (/tmp/w/helix/helix-dap)
Checking helix-view v25.7.1 (/tmp/w/helix/helix-view)
Checking helix-tui v25.7.1 (/tmp/w/helix/helix-tui)
Checking helix-term v25.7.1 (/tmp/w/helix/helix-term)results/raw/helix.jsonl https://github.com/kbwo/rust-compile-wait-breakdown/blob/main/results/raw/helix.jsonl
依存する側のクレートはシグネチャを変えたかどうかに関係なく検査し直されていて、それでも1秒ほどで終わる。
シグネチャの変更で伸びたのはbuildのほうだった。helixのbuildは中身の変更で9.37秒、シグネチャの変更で11.21秒である。リンカが動いていた時間は6.05秒と5.83秒でほぼ同じなので、伸びた2秒ほどはリンク以外の部分の時間である。impl Into<T>にすると関数がジェネリックになり、呼び出す側のクレートでもコード生成が要るようになったからだと思うが、ここは確かめていない。
リンカ
buildとtestの待ち時間の大半がリンカの動いている時間なら、リンカを変えれば縮むはずで、lldはおおむねそのとおりだった。中央値がデフォルト (GNU ld) より小さく、かつ5回の最大がデフォルト (GNU ld) の5回の最小より小さいときを速くなったと数えると、3つのリポジトリ、3種類の変更、buildとtestの18の組み合わせのうち17で速くなった。
設定を変えずにmoldを使うと、同じ数え方で速くなったのは18のうち8だけだった。moldのスレッド数を2に絞ると、同じ数え方で18の組み合わせのすべてが速くなり、lldを2スレッドにしたものとほぼ同じになった。
| 変更とコマンド | デフォルト (GNU ld) | lld | mold | lld 2スレッド | mold 2スレッド |
|---|---|---|---|---|---|
| 実行ファイルのクレートの中身 test | 9.77 | 5.55 | 8.58 | 3.95 | 4.28 |
| helix-coreの中身 test | 10.91 | 6.72 | 10.89 | 5.35 | 5.71 |
| helix-coreのシグネチャ build | 11.21 | 6.73 | 8.47 | 6.11 | 6.14 |
| helix-coreのシグネチャ test | 14.40 | 9.41 | 13.23 | 7.93 | 8.77 |
このコンテナではCPUを2個に制限していても、nprocがホストの14を返していた。
nproc (host CPUs visible): 14
cgroup cpu.max (quota period): 200000 100000results/env.txt https://github.com/kbwo/rust-compile-wait-breakdown/blob/main/results/env.txt
moldはこの14を見て、実際には使えない数のスレッドを立てていたのだと思う。CPUを絞ったコンテナやCIでmoldに替えても速くならなかったら、まずスレッド数を疑う。
公式の調査でも、再ビルドが遅い理由の一つにリンクが挙がっている。
The linking phase is too slow.
Rustの公式の調査の結果 https://blog.rust-lang.org/2025/09/10/rust-compiler-performance-survey-2025-results/
x86_64のLinuxでは、2025年9月18日のRust 1.90.0から、デフォルトのリンカがlldになっている。
The
x86_64-unknown-linux-gnutarget will now use the LLD linker for linking Rust crates by default.
Rust 1.90.0の告知 https://blog.rust-lang.org/2025/09/18/Rust-1.90.0/
x86_64のLinuxで測っていれば、何も設定しないときの数字はlldの列に近くなっていたはずだが、そこは測っていない。
C++のKakouneと並べる
Kakouneでは、.ccファイルの関数の中身を1行変えた後の再ビルドが2.69秒で、再コンパイルしたのは1ファイルだった。多くのファイルがincludeするヘッダのinline関数を1行変えると21.81秒かかり、再コンパイルは56ファイルになった。クリーンビルドを1回だけ測ったら21.98秒だったので、ヘッダの1行でほぼクリーンビルドの分を待ったことになる。
変更の形で待ち時間が変わること自体は、Rustの3つにもあった。ただ伸び方の大きさは違う。Rustの3つで変更の種類だけを変えたとき、buildとtestの中央値の開きは最大でもmdBookのtestの2.01秒から5.62秒への約2.8倍だった。Kakouneのヘッダの変更は8.1倍である。比べられるのはここまでである。KakouneとRustの3つは規模も設計も違い、Kakouneではリンクの時間を分けて測っていない。この数字から、C++とRustのどちらが速いかは言えない。
遅いと言ってよい範囲
測った範囲で線を引くと、次のようになる。
cargo checkで型検査だけを待つ場面は、3つのどれでも、どの変更でも中央値で1.2秒以内、5回の最大でも1.22秒だった。この規模で、ここを遅いと言うのは当たらない- 1行直して動かすまでの場面は、helixの規模だとデフォルト (GNU ld) のリンカで10秒を超える。ここは遅いと言ってよい。ただしcheckより長くかかった分の6割から9割強はリンカが動いていた時間で、lldに替えるとhelixでも10秒を切った
- クリーンビルドはripgrepのdebugを除いて10秒を超えた。CIや初回のcloneでは遅いと言ってよい。手元では1日に何度も待つものではないが、CIで毎回ゼロからビルドしているなら、その都度この時間がかかる
- シグネチャの変更は型検査の待ち時間をほとんど伸ばさず、helixではbuildの時間をリンク以外の部分で2秒ほど伸ばした
遅いと言う人がクリーンビルドやhelixくらいの規模の再ビルドを見ていて、遅くないと言う人がripgrepくらいの規模の再ビルドを見ているなら、両方とも正しい。ripgrepのbuildとtestは、どの変更でも中央値で1.31秒以内だった。
測った環境はCPU 2個のコンテナで、数字の絶対値は手元のマシンやCIとは違う。比べられるのは、同じ環境の中での待ち時間どうしの差である。クレートが100を超えるような大きなワークスペースでもcheckが速いままかは、この3つからは分からない。
そのうち測る