- Bundlerの性能限界を分析し、Pythonのパッケージマネージャー uv が高速な理由を比較
- uvの速度はRust言語によるものではなく、並列ダウンロード、グローバルキャッシュ、メタデータベースの依存関係処理 などの構造的設計によるもの
- Bundlerは ダウンロードとインストールの処理が結合されているため並列化に制約 があるが、これを分離すれば大きな改善が見込める
- グローバルキャッシュ統合、ハードリンクインストール、PubGrubソルバー統合 などにより、RubyGemsとBundlerの重複を減らせる
- 言語を書き換えなくても、ほとんどの性能向上はRubyコード内で達成可能 であり、uvレベルの速度に近づける
Bundlerとuvの性能比較
- RailsWorldで提起された「なぜBundlerはuvほど速くないのか?」という問いをきっかけに、Bundlerの性能ボトルネック を調査
- 著者はBundlerがuvレベルの速度を達成できると確信しており、性能差は言語ではなく設計の問題 だと明言
- Andrew Nesbittの “How uv got so fast” を引用し、uvの主要な最適化手法がBundlerに適用可能かどうかを分析
Rustへの書き直しは必要か
- uvがRustで書かれているのは事実だが、速度の本質的な原因はRustそのものではない
- Bundlerのボトルネックを取り除いた結果、「Rustへの書き直しだけが残された改善策」になるなら、それは成功だと評価
- Rustへの書き直しは、既存の互換性制約なしに実験的な設計を試せる自由 を与えるが、必須条件ではない
Bundlerの構造的ボトルネック
- Bundlerは gemのダウンロードとインストールを1つのメソッドに結合 しており、並列ダウンロードができない
- 例示コードでは
install メソッドが fetch_gem_if_not_cached と install を連続実行
- そのため、依存関係を持つgem(
a -> b -> c)は逐次的にしかインストールできない
- 実験結果では、依存関係がある場合は9秒以上かかる一方、独立したgem(
d, e, f)は 並列ダウンロードで4秒以内に完了
- ダウンロードとインストールを分離 すれば、依存関係のルールを維持しつつ並列処理が可能
- 4段階(ダウンロード → 展開 → コンパイル → インストール)への分離を提案
- 純粋なRuby gemでは依存関係のインストール順序を緩和でき、さらなる高速化が可能
キャッシュとインストールの最適化
- uvの グローバルキャッシュとハードリンクインストール 方式はBundlerにも適用可能
- BundlerとRubyGemsは現在、Rubyのバージョンごとに別々のキャッシュを使用
$XDG_CACHE_HOME ベースの 共有キャッシュ への統合が必要
- ハードリンクインストールは、キャッシュ統合後に適用できる
- Bundlerはすでに PubGrub依存関係ソルバー を使っているが、RubyGemsは依然としてmolinilloを使用
- 2つのシステムの ソルバー統合 が技術的負債解消の鍵
Rust由来の最適化要素は適用できるか
- ゼロコピー逆シリアライズ は、RubyGemsのYAMLパース段階で一部適用の可能性がある
- RubyのGVL(Global VM Lock) は、IO中心の処理では並列化に大きな制約を与えない
- IOとZLIB処理はGVLを解放するため、並列実行が可能
- ただし、小さなファイル書き込みではGVL管理のオーバーヘッドが性能低下要因になる
- Ruby内部ではこれを改善する作業が進んでいる
- バージョン比較の最適化: uvはバージョンを
u64 整数にエンコードして比較速度を高めている
- Rubyでも
Gem::Version を整数ベースに変換して ソルバー性能を向上 できる可能性がある
- 関連するリファクタリングの試みはすでにあったが、後方互換性の問題で保留 されている
結論と今後の計画
- uvの速さは、言語よりも 不要な作業を取り除いた設計 によるものであり、Bundlerも同じ方向で改善できる
- RubyGemsとBundlerはすでに 現代的なパッケージ管理構造を備えており、uvレベルの速度達成は現実的
- 最大の課題は レガシーコードと互換性維持
- Rustへ書き直さなくても、99%の性能向上はRubyコード内で可能 であり、残り1%はごくわずか
- 続編では、BundlerとRubyGemsの実際の プロファイリングと具体的なボトルネック要因 を扱う予定
2件のコメント
言うのは簡単だ。コードを見せてくれ!
Hacker Newsの意見
Bundlerの内部構造にそこまで詳しいわけではないが、最大の改善点は uvのキャッシュ設計 を取り入れることだと思う
uvが高速な主因のひとつはキャッシュ構造にあり、これは他の言語やエコシステムでも再現可能だ
ただし
requires-pythonの上限を無視する点は、性能のためではなく、より良い 依存関係解決 のためだたとえばプロジェクトが Python 3.8 以上を要求していても、ある依存が
<4制約を付けると Python 4 ではインストール不能になるuvはサポート対象の全バージョンに対して解決するので、上限を無視しても時間短縮効果はほとんどない
関連議論は Python Discussフォーラム で見られる
PEP 658以降、PythonのSimple Repository APIがメタデータを直接提供しているように、RubyGems.orgもすでに似た情報を提供している
ただし gem を展開しないと native extension の有無 は分からない
なので、この情報をRubyGems.orgのメタデータに直接追加すれば、依存関係のインストールツリーを完全に並列化できるのではないかという提案だ
以前RubyGems.orgで働いていたとき、メタデータはバージョンごとに抽出されていたと記憶している
過去バージョンの gemspec を再処理する必要があり、これは 危険なメタデータ変更 になり得る
そのため過去バージョンへの適用は難しいだろうが、今後については unpack なしでインストール順序を分かるよう改善できそうだ
AaronがBundlerを Rustで書き直すことより、実質的なアルゴリズム改善 に集中しているのが気に入っている
複数のバージョン管理ツールとRubyのバージョンが入り乱れた 混沌とした環境 には本当にうんざりする
問題は単なる速度ではなく、コントロール権とエコシステムの方向性 だと思う
Rubyはこの10年、速度に注力してきたが、ドキュメントの質やコミュニティ運営のほうがむしろ重要だった
言語が衰退する理由を真剣に考え、多様なアイデアを押し進めるべき時期だ
最近の関連投稿として How uv got so fast(2025年12月、457コメント)がある
RubyGemsをより高速にするには、各 gem のファイル一覧を レジストリ/データベース化 するのが鍵だ
こうすれば
requireのたびにファイルシステムをスキャンする必要がなくなるgem を直接編集するとメタデータを再ハッシュする必要があるが、どうせ手動編集は推奨されていない
今では古いものだが、今でも愛着のある小さなプロジェクトだ
コード: fastup
本当の問題は、
$LOAD_PATHがすべての gem を追加して 組み合わせ爆発 を起こす構造にある複数のキャッシュプロジェクトが存在すること自体、これが実問題だという証拠だ
以前、アプリ起動に数分かかっていたのを、load path を操作して 分単位で短縮 したこともある
以前、bootsnapをbundlerに統合しようと提案したが却下された
RubyGemsの構造説明が興味深かった
gem は tar ファイルで、その中のYAML GemSpecが依存関係を宣言する
RubyGems.orgはこの情報をAPIで提供するので、eval なしでも依存関係を確認できる
ただしYAMLは パース効率の低いフォーマット なので、JSONやprotobufのような代替のほうが良いかもしれない
それでも gemserver がすでに依存情報を返しているなら、大きな問題ではなさそうだ
例: バージョン、依存関係、ハッシュ程度だけを含む構造
uvが速い理由もここにある — パッケージをダウンロードせずに依存関係を計算できるからだ
以前、gem のインストール方法を改善した プロトタイプ動画 を作ったことがある
how_gems_should_be.mov
Rubyの fibers(またはAsyncライブラリ) はしばしば過大評価されている
スレッドと同様に、コネクションプールのような上位レベルの調整問題は依然として残る
それでもIOバウンドなインストール作業を非同期化すれば、意味のある性能向上 は見込める
こんな形で進めると思う
「グローバルキャッシュ をすべての bundler インスタンスで共有する」というアイデアを検討中だ
長期的には大きな利点がありそうだが、隠れた複雑さがないか見極めている段階だ
関連Issue: rubygems #7249
Rubyがこの問題を最初に解くわけではないのだから、今こそその恩恵を受けるべきだ
最適化の基本原則はシンプルだ — 何もしないのが最速
不要な作業そのものをしないこと が本当の最適化だ