buildprof が Bun の Zig ビルドが Rust ビルドより 5 倍遅かった理由を明らかに
すばやいポイント
buildprof は Bun の Zig ビルドが遅かった理由が、巨大な Full-LTO リンクフェーズと単一モジュールの Zig コンパイルにより並列化が不可能だったのに対し、Rust ビルドは Thin-LTO と多数のクレートを用いて、合計時間を約 24 分から約 5 分に削減したことを証明している。
buildprof とは?
buildprof は Linux 向けのオープンソースのトレーシングツール(https://github.com/lalitMaganti/buildprof)であり、ビルドが起動するすべてのプロセス、ファイルの読み書き、およびオプションでコンパイラ内部のトレースを記録する。時間は左から右へ流れ、バーの幅は実行時間、子プロセスは親プロセスの下に表示される階層的なタイムラインをレンダリングする。任意のビルドコマンドの前に付けることで使用できる:
buildprof -- make -j16
buildprof -- cargo build
buildprof -- ninja -C out/target
buildprof -- just build
buildprof -- ./dev/custom-build-script.sh
記録されたデータは、Perfetto プラットフォーム上で構築された Web UI(https://buildprof.lalitm.com/)で探索できる。
Bun のコンパイル時間に注目した理由
Jarred Sumner(Bun のチーフアーキテクト)のツイートでは、Bun 1.4.0(Rust ベース)が Linux で Bun 1.3.14(Zig ベース)よりも 5 倍速くコンパイルされたと主張した。ツイートでは、Zig ビルドは Full LTO を使用した一方、Rust ビルドは ThinLTO を使用したと指摘した。LTO 戦略はビルド時間に劇的な影響を与える可能性があるため、著者はこの主張を検証し、背後にある原因を理解するために buildprof を開発した。
数値の再現
著者は Bun 1.3.14 と 1.4.0 をチェックアウトし、6 コア/12 スレッドの VM 上で CI ビルドをスクリプト化し、ツイートの数値に近いタイミングを得た:
| ビルド | 报告された CI 中央値 | 単一 VM 上での再現 |
|---|---|---|
| Zig エラ(Bun 1.3.14) | 30 分 06 秒 | 24 分 24 秒 |
| Rust エラ(Bun 1.4.0) | 5 分 37 秒 | 5 分 40 秒 |
ギャップは継続しており、より深い調査を促した。
ビルドをプロセスツリーとして可視化
ビルドは本質的に プロセスツリー である:トップレベルのコマンド(例:cargo)がコンパイラ、リンカー、スクリプトを起動し、それらがさらにプロセスを起動する可能性がある。ptrace を用いて各 fork/exec/exit イベントを記録し、seccomp フィルターでファイル I/O をインターセプトすることで、buildprof は完全なツリーを構築し、ノード間のファイル依存関係をマッピングする。このアプローチは ビルドシステムに依存しない ため、カスタムスクリプトも自動的に含む。
Zig ビルド:リンカーのボトルネック
Zig エラの CI ビルドのタイムラインでは、単一の ld.lld 呼び出しが約 16 分(合計時間の約 2/3)を消費していることがわかる。--compiler-traces を有効にすると、内部の LLD フェーズが明らかになる。OptModule フェーズだけで 10 分以上を要しており、Full LTO がリンカーにプログラム全体に対して重い最適化フェーズを実行させていることを確認した。
Rust ビルド:軽量リンク
Rust エラの CI ビルドのタイムラインでは、合計リンク時間が 2 分 24 秒 であった。そのリンカーのコマンドには -plugin-opt=thinlto が含まれており、リンカーは軽量なシンボル解決のみを実行し、大部分の最適化は事前に並列で実行される rustc 呼び出しで行われている。この鮮明な対比は、LTO 戦略が時間差の主な原因であることを示している。
実験:Zig を ThinLTO に切り替え
著者は Bun の Zig ビルドを ThinLTO に切り替えるパッチを適用し、新しい実行を記録した。リンク時間は 3 分 40 秒 改善したが、多くの WebKit ライブラリ(例:libJavaScriptCore.a)がまだ Full LTO でビルドされていたため、全体のビルド時間は約 13 分にとどまった。これらの事前にビルドされたアーカイブは、リンカーに大きなビットコードモジュールに対して完全な最適化を実行させた。
WebKit を ThinLTO で再ビルド
必要な WebKit リビジョンとその ICU 依存関係を ThinLTO で再ビルドし、ダウンロードされたアーカイブと交換することで、以下の結果を得た:
| 設定 | 全体ビルド | 最終リンカー |
|---|---|---|
| 元の Full LTO(Zig) | 24 分 24 秒 | 16 分 35 秒 |
| Zig ThinLTO + 元の WebKit | 20 分 20 秒 | 12 分 55 秒 |
| Zig ThinLTO + 再ビルド済み ThinLTO WebKit | 15 分 11 秒 | 7 分 22 秒 |
再ビルドされた WebKit はリンク時間を半分に削減したが、Rust ビルドよりは依然として遅かった。
並列化:クレート vs 単一モジュール
依存関係の矢印を確認すると、C++ 側が先に終了した後も、リンカーが bun-zig.o の完了を待っていることがわかる。重要な観察点は:Bun の Rust ビルドは 90 以上のクレートに分割されており、Cargo が多数の rustc プロセスを並列で起動できる。一方、Zig ビルドはコードベース全体を 1 つのモノリシックな Zig モジュール としてコンパイルしており、並列化が制限され、リンカーの負荷が増大している(1 つの巨大な ThinLTO ビットコードモジュール vs 複数の小さなモジュール)。
その他の発見
- 雑多な CI ステップ – トレースはネットワークプローブ、Docker 検査、Git クエリをキャプチャしており、それぞれ 1 秒未満であった。
- 冷たい依存関係の取得 – 新規の WebKit のダウンロード/抽出には約 20 秒かかっており、
node → tar → gzipイベントとして可視化された。 - C++ コンパイルの詳細 – Clang に
-ftime-traceを有効にすると、12 秒のコンパイルがフロントエンドとバックエンドで均等に分割され、ModuleInlinerWrapperPassが 4 秒以上を消費していることがわかった。
buildprof の仕組み
- 記録 –
ptraceを使ってフォーク/実行を追跡し、seccomp フィルターでファイルイベントを処理する。eBPF や ftrace に伴う権限と安定性の問題を回避する。 - オーバーヘッド – 主にファイルオープンの数に比例する。例:ripgrep(約 0.2 秒)、Redis(約 5 秒)。
--no-file-eventsフラグでファイルトレースのオーバーヘッドを削除可能。 - UI – Perfetto UI のプラグインシステムを活用して構築され、タイムラインレンダリング、プロセスツリーのレイアウト、プロデューサーとコンシューマー間のオンデマンド矢印を提供する。
関連ツールとその限界
| ツール | 強み | 限界 |
|---|---|---|
| ninjatracing | Ninja のエッジと並列化を表示 | ラッパー スクリプトとサブプロセスツリーを無視 |
| Cargo timings | Cargo フェーズの詳細な時間 | カスタムスクリプトと非 Cargo 作業を無視 |
clang -ftime-trace |
コンパイラ内部フェーズを表示 | 全ビルドプロセスの視点がない |
| strace / tracexec | 一般的なシステムコールトレース | ビルド指向ではない、タイムラインの読みづらさ |
| What the Fork | プロセスツリー表示 | プライベートベータ、オープンソースではない |
buildprof は、完全なプロセスツリーのキャプチャ、ファイル依存関係の矢印、オプションのコンパイラトレースを1つのインタラクティブなタイムラインに統合することで、このギャップを埋めている。
今後の方向性
- ファイルシステムトレースのオーバーヘッドを削減(特に多数のファイルを開くビルド向け)。
- macOS と Windows サポートの追加。
- より多くのビルドシステム(npm、Gradle、Bazel)のサポート。
- クリティカルパス の自動計算と表示。
- GitHub のイシューを通じたユーザーからのフィードバックの統合。
コミュニティの反応(選定された HN コメント)
"良い記事!著者が Bun の Zig ビルドを Rust よりずっと速くできると結論づけることを期待していたが、深掘りは素晴らしい" – @anaqin
"Electric Insight のようだ…ビルドを差分比較して、なぜ片方が遅いかを確認できる" – @t43562
"いいね!macOS 版はどんなものになるの?" – @jiehong
"WebKit のリンクで Full LTO がシリアライズ部分だ。あなたのビジュアライザーはリンク時間とコード生成時間を分離できるか?" – @xcc3641
これらのコメントは、このケーススタディを超えたツールの適用可能性への関心と、クロスプラットフォームサポートへの関心を示している。
結論
- Bun の Zig ビルドの遅延の主因は、約 16 分間単独で実行された Full-LTO リンクステップ であった。
- ThinLTO に切り替えることでリンク時間は短縮されたが、事前にビルドされた WebKit アーカイブがまだ Full-LTO であったため、大きなギャップが残った。
- WebKit を ThinLTO で再ビルドすることで、全体のビルド時間を 15 分 に削減したが、Rust ビルドは 90 以上のクレートにわたる大規模な並列化 により、依然として速かった。
- buildprof は、隠れたボトルネックを明らかにし、仮説を検証し、具体的な最適化を導く上で非常に価値があった。
遅いビルドに直面している場合、buildprof を試してみよう:buildprof -- <your-build-command> と入力し、https://buildprof.lalitm.com/ でタイムラインを探索しよう。
Sources
関連
- プロジェクト
- Dispatch
- Dispatch
- Dispatch
- プロジェクト