3 ポイント 投稿者 GN⁺ 2026-01-03 | 2件のコメント | WhatsAppで共有
  • 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_cachedinstall を連続実行
    • そのため、依存関係を持つ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件のコメント

 
iolothebard 2026-01-06

言うのは簡単だ。コードを見せてくれ!

 
GN⁺ 2026-01-03
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のメタデータに直接追加すれば、依存関係のインストールツリーを完全に並列化できるのではないかという提案だ

    • 私も同じことを考えたが、gemspec の情報とRubyGems.orgのメタデータが一致しない可能性がある
      以前RubyGems.orgで働いていたとき、メタデータはバージョンごとに抽出されていたと記憶している
      過去バージョンの gemspec を再処理する必要があり、これは 危険なメタデータ変更 になり得る
      そのため過去バージョンへの適用は難しいだろうが、今後については unpack なしでインストール順序を分かるよう改善できそうだ
  • AaronがBundlerを Rustで書き直すことより、実質的なアルゴリズム改善 に集中しているのが気に入っている

    • 高速化も良いが、私はRubyのインストール自体を管理してくれる機能のほうが必要だ
      複数のバージョン管理ツールとRubyのバージョンが入り乱れた 混沌とした環境 には本当にうんざりする
    • AaronがShopify所属だからか、gem.coopプロジェクトへの言及がないのが複雑な気分だ
      問題は単なる速度ではなく、コントロール権とエコシステムの方向性 だと思う
      Rubyはこの10年、速度に注力してきたが、ドキュメントの質やコミュニティ運営のほうがむしろ重要だった
      言語が衰退する理由を真剣に考え、多様なアイデアを押し進めるべき時期だ
  • 最近の関連投稿として How uv got so fast(2025年12月、457コメント)がある

  • RubyGemsをより高速にするには、各 gem のファイル一覧を レジストリ/データベース化 するのが鍵だ
    こうすれば require のたびにファイルシステムをスキャンする必要がなくなる
    gem を直接編集するとメタデータを再ハッシュする必要があるが、どうせ手動編集は推奨されていない

    • 昔これに似たコードを書いたことがあるが、ディスクキャッシュがなくても その場でハッシュを生成 するだけでかなりの高速化があった
      今では古いものだが、今でも愛着のある小さなプロジェクトだ
      コード: fastup
    • 「bundle install」の最適化は方向性が間違っている
      本当の問題は、$LOAD_PATH がすべての gem を追加して 組み合わせ爆発 を起こす構造にある
      複数のキャッシュプロジェクトが存在すること自体、これが実問題だという証拠だ
      以前、アプリ起動に数分かかっていたのを、load path を操作して 分単位で短縮 したこともある
    • ランタイム側でこれを処理しようとしたが、Rubyには効率的なデータ構造が不足していて実装が難しかった
    • 実はこれはすでに bootsnap がやっていることだ
      以前、bootsnapをbundlerに統合しようと提案したが却下された
  • RubyGemsの構造説明が興味深かった
    gem は tar ファイルで、その中のYAML GemSpecが依存関係を宣言する
    RubyGems.orgはこの情報をAPIで提供するので、eval なしでも依存関係を確認できる
    ただしYAMLは パース効率の低いフォーマット なので、JSONやprotobufのような代替のほうが良いかもしれない
    それでも gemserver がすでに依存情報を返しているなら、大きな問題ではなさそうだ

    • YAMLはいまひとつだが、一般的な gemspec のサイズなら 性能への影響は軽微 だと思う
    • 人が編集せずレビューだけする用途の lockfile なら、YAMLの複雑な機能を削った 単純なパーサ を作れる
      例: バージョン、依存関係、ハッシュ程度だけを含む構造
    • 実際、こうしたメタデータはRubyGemsやPyPIが あらかじめデータベースにパースして保存 している
      uvが速い理由もここにある — パッケージをダウンロードせずに依存関係を計算できるからだ
  • 以前、gem のインストール方法を改善した プロトタイプ動画 を作ったことがある
    how_gems_should_be.mov

  • Rubyの fibers(またはAsyncライブラリ) はしばしば過大評価されている
    スレッドと同様に、コネクションプールのような上位レベルの調整問題は依然として残る
    それでもIOバウンドなインストール作業を非同期化すれば、意味のある性能向上 は見込める

    • 純粋なRubyでさらに絞り出すなら、
      1. パースの速いインデックス形式を使う(関連gist
      2. 初期ダウンロードはスレッドで処理する
      3. 展開と post-install は fork に分離する
        こんな形で進めると思う
  • グローバルキャッシュ をすべての bundler インスタンスで共有する」というアイデアを検討中だ
    長期的には大きな利点がありそうだが、隠れた複雑さがないか見極めている段階だ
    関連Issue: rubygems #7249

    • 完全に単純というわけではないが、他のエコシステムでの 先行事例 を見れば十分実現可能だ
      Rubyがこの問題を最初に解くわけではないのだから、今こそその恩恵を受けるべきだ
  • 最適化の基本原則はシンプルだ — 何もしないのが最速

    • 「賢いコードは速い」という思い込みを捨てるべきだ
      不要な作業そのものをしないこと が本当の最適化だ