- Bunのパッケージインストールは、既存のパッケージマネージャーと比べて非常に高速に動作する
- 高速インストールの鍵は、システムプログラミングの観点からのアプローチとシステムコールの最小化にある
- Zig言語ベースのネイティブ実装、バイナリキャッシュの利用、OSごとの最適化など、細かな戦略の適用によって性能を向上させている
- tarballの展開とファイルコピーの過程でも、ハードウェア特性を活かした高性能な手法を導入している
- 依存関係グラフやlockfileなど、データ構造の最適化によってCPUキャッシュ効率とメモリアクセス性を高めている
Bun Installが速い理由
- Bunの
bun installは平均して、npmより7倍、pnpmより4倍、yarnより17倍高速なパッケージインストール性能を提供する
- これは単なるベンチマークではなく、パッケージインストールの問題をJavaScriptではなくシステムプログラミングの観点から捉えた結果である
- システムコールの最小化、マニフェストのバイナリキャッシュ、tarball展開の最適化、OSネイティブなファイルコピーなど、複数の層で積極的に性能最適化を適用している
Node.jsとパッケージマネージャーアーキテクチャの限界
- 2009年のNode.js登場以降、イベントループとスレッドプールベースの非同期IOモデルがパッケージマネージャーにも広がった
- 当時はハードウェアの制約(遅いディスク、遅いネットワーク)があったため、非同期IOと高頻度のシステムコールという戦略は合理的だった
- しかし現代のシステムでは、NVMe SSD、高速ネットワーク、高性能CPUが一般的であり、本当のボトルネックはIOではなくシステムコールのオーバーヘッドである
システムコールとモードスイッチのコスト
- プログラムがファイル読み込みのような処理を要求すると、user modeからkernel modeへの切り替えが必要になり、この過程で高コストなCPUサイクル(1000〜1500 cycles)が消費される
- パッケージインストールは基本的に数万回から数十万回以上のシステムコールを必要とするため、実作業そのものではなく切り替えコストだけで数秒分のCPU時間を消費することがある
- たとえばReactとその依存関係をインストールする際、npmは約100万回、yarnは400万回、pnpmは50万回、bunは16万回のシステムコールを使う
既存のパッケージマネージャーとBunのアプローチの違い
- npm、pnpm、yarnはいずれもNode.jsベースであり、JavaScriptを複数の抽象レイヤー(libuv、イベントループ、スレッドプール、システムコールの仲介)を通して実行しなければならない
- このとき引数変換、ワーカープールのキュー、イベントループ上のタスク分岐、futex(ロック同期)システムコールなどが積み重なり、IOそのものよりもシステムコール管理のほうが遅くなる結果を招く
- Node.jsで作られたパッケージマネージャーは、この構造的な限界のため、実際にネイティブに近い性能を出すのが難しい
Bun: Zigで実装されたネイティブインストールエンジン
- BunはZig言語で直接システムコールを呼び出し、JavaScriptエンジンや抽象化レイヤーをすべて省いている
- たとえばファイル読み込みでは、Zigコードからそのまま
openat()システムコールを実行し、即座にデータを返す
- そのため数万個のファイルを読む過程も、別途スレッドプール・イベントループ・データ変換を挟まず超高速に動作する
- ベンチマークでは、Bunは1秒あたり146,057個のpackage.jsonを読め、Node.jsは6万個台で2倍以上遅い
依存関係管理とDNS最適化
- Bunは
bun install実行時、依存関係の解析と同時にDNS prefetchを非同期でトリガーする
- たとえばmacOSではAppleの非公式async DNS API(
getaddrinfo_async_start())を使い、スレッドをブロックせずにネットワーク処理を同時実行できるようにしている
- 既存のパッケージマネージャーはlibuvスレッドプールベースで、実際には内部でブロッキングコードが走るため、リソースの無駄が発生する
パッケージマニフェストのバイナリキャッシュ
- npmなどはマニフェストをJSONでキャッシュするが、Bunは一度パースした後、その結果をバイナリ(
.npmファイル)に変換して保存する
- 文字列重複とパースオーバーヘッドを最小化し、実メモリ上ではオフセット計算だけで即座に値へアクセスできる(新規オブジェクト生成、パース、ガベージコレクションが不要)
- ETagとIf-None-Matchヘッダーで変更点だけを確認し、不要なデータパースなしに最新性を検証できる
- ベンチマークでは、Bunのキャッシュインストールはnpmのfresh installよりも速い
Tarball(圧縮ファイル)処理性能
- 一般的なパッケージマネージャーはtarballをストリームで受け取り、バッファメモリが不足するたびに再割り当て・コピー・リサイズが連続して発生する
- Bunはtarball全体を受信してから展開し、gzip末尾4バイトから展開後サイズを事前に把握して、メモリを一度だけ割り当てる
- libdeflateなどを活用して高速に展開し、不要な重複コピーやサイズ変更をすべて排除している
依存関係グラフとデータ構造の最適化
- 既存のパッケージマネージャーはJavaScriptオブジェクトやポインタベースの依存ツリーを作るため、メモリがランダムに分散し、CPUキャッシュミスが頻発する(pointer chasing問題)
- Bunは**Structure of Arrays(SoA)**パターンを適用し、すべてのパッケージ、文字列、依存関係を大きな連続メモリ領域に保存している
- オフセットと長さベースのアクセスにより、CPUは一度に複数のパッケージをキャッシュライン単位で読める(キャッシュフレンドリーな構造)
- lockfileもJSON/YAMLではなくSoAパターンに合わせ、文字列重複を除去しつつ順次メモリアクセスしやすい形で保存する
- lockfileのバイナリ形式(
bun.lockb)も試験的に導入されたが、Gitでの共同作業性が下がるため、可読性の高いプレーン形式へ切り替えられた
OS別ファイルコピー最適化
macOS
- clonefileを使用: ディレクトリ全体をCopy-On-Write方式で1回のシステムコールで複製する
- ディスク容量の重複使用を最小限に抑えつつ、インストール速度を最大化する
- clonefileが失敗した場合は、フォールバックとしてper-directory cloning→copyfileへ段階的に切り替える
Linux
- ハードリンクを優先して試行: 新しいファイルを生成せず、既存ファイルへの新しい参照だけを作る(ディスク上のデータ移動なし)
- ハードリンク不可の場合、Btrfs/XFSでは
ioctl_ficloneでCopy-On-Writeを適用する
- その後は
copy_file_range、sendfile、最後に通常のcopyfile方式へとフォールバックする
総評
- Bunはシステムコールの最小化、バイナリ構造、OS最適化、データ構造改善によって、パッケージマネージャーの従来の性能限界を超えている
- その結果、超高速なインストールだけでなく、メモリ効率とCPU効率も改善している
- 既存のNode.jsベースのマネージャーに比べ、別のランタイムへ置き換えなくてもプロジェクトへ適用できる(互換性を維持)
- 実際の大規模コードベースでは、数分かかっていたインストール工程を数ミリ秒〜数秒以内に短縮する体験を提供する
- システム、ハードウェア、OSレベルに合わせた最適化の優れた事例として、研究・参考価値が高い
まだコメントはありません。