ハクソク

世界を動かす技術を、日本語で。

Bundlerはuvと同じくらい速くなれるか?

概要

Bundlerのパフォーマンス向上について、uvの手法を参考にしつつ、現状の課題と改善策を考察。
Rust化の必要性よりも、設計やアーキテクチャの見直しが重要である点を強調。
依存関係解決並列ダウンロードグローバルキャッシュなど、適用可能な最適化技術を整理。
BundlerRubyGemsの現状のボトルネックを明確化し、具体的な改善案を提示。
Pythonの事例との比較を通じて、Rubyコミュニティへの示唆を抽出。

Bundlerはuv並みに速くなれるか

  • RailsWorldでの「Bundlerはuv並みに速くできないのか?」という問いをきっかけに、Bundlerの性能問題を調査・発表
  • uvの高速化手法を分析し、BundlerやRubyGemsにも適用可能か検討
  • 結論として「Bundlerもuv並みの速度は実現可能」と判断(ただし誤差範囲あり)

Rustへの書き換え論

  • uvの高速化理由として「Rustで書かれているから」がよく挙げられるが、本質は設計と最適化手法にある
  • Rust化は最終手段であり、現状のボトルネック解消が優先
  • 新言語での書き直しは発想の自由度をもたらす利点も

Pythonパッケージ管理との比較

  • Pythonでは長年「依存関係取得のためにパッケージコードの実行が必要」だったが、RubyGemsはYAMLのGemSpecで依存情報を明示
  • RubyGems.orgはAPIで依存関係情報を提供、eval不要
  • Pythonコミュニティの標準化努力(PEP 658等)を評価

uvが省略したもの・参考点

  • uvは「requires-pythonの上限チェックを無視」し、解決のバックトラック回数を削減
  • Bundlerでも「required Ruby version」のチェック最適化が可能性

Rust不要のパフォーマンス最適化

  • uvの手法で特に有用なものを抽出

    • 並列ダウンロード

      • Bundlerはダウンロードとインストールが密結合、並列化が困難
      • 依存関係解決後、キューイングシステムで「依存解決済みのみ並列インストール」→深い依存ツリーでは直列処理に
      • ダウンロードとインストールの分離で並列化可能、特に純粋Ruby Gemなら更なる高速化
      • 提案:インストール工程を「ダウンロード→展開→コンパイル→配置」に分割
    • グローバルキャッシュとハードリンク

      • uvは「グローバルキャッシュ+ハードリンク」で仮想環境ごとの重複コピーを回避
      • Bundler/RubyGemsも「$XDG_CACHE_HOME」などにグローバルキャッシュを実現すべき
      • 現状はRubyバージョンごとにキャッシュが分断、重複ダウンロード・展開が発生
      • インストール時のハードリンク利用で更なる効率化が期待
    • 依存関係のないGemの並列インストール

      • 依存関係がない場合は完全並列インストールが可能
      • 依存ツリーが浅い場合、現状でも一定の並列化効果が得られる
    • ネイティブ拡張Gemの特別扱い

      • ネイティブ拡張Gemは依存Gemの事前インストールが必須
      • 展開後にネイティブ拡張の有無を判定し、依存解決済み後にインストール再開
    • gelの事例

      • Bundler代替のgelは「ダウンロードとインストールの分離」を実現し、同様のケースで大幅な高速化

まとめと今後の課題

  • Bundlerは設計の見直しアーキテクチャ改善で、uv並みの高速化が十分可能
  • 並列ダウンロード・グローバルキャッシュ・処理分離など、Rust不要の最適化余地
  • Pythonコミュニティの標準化努力やuvの大胆な設計判断から学べる点多数
  • 既存の改善案やオープンチケットも活用し、段階的な性能向上を目指すべき

Hackerたちの意見

面白い投稿だね。でも、最初の部分が特に気になった。Ruby Gemsはtarファイルで、その中のファイルの一つがGemSpecのYAML表現なんだ。このYAMLファイルはGemの依存関係を全部宣言してるから、RubyGemsは何もevalしなくても、特定のGemをインストールする前に必要な依存関係が分かるんだよね。さらに、RubyGems.orgは依存関係情報を問い合わせるためのAPIを提供していて、これが実際の依存関係情報を得るための普通の方法なんだ(やっぱりevalは不要)。大規模なPythonの依存関係セットとRubyの依存関係セットのパース速度を比較するのは面白そうだね。YAMLはパースするのに効率的じゃないことで有名だし。`pip`よりはマシだったかもしれないけど、依存関係情報をもっと効率的なフォーマット(JSONとかprotobufとか)でパースする余地がないとは思えないな。ただ、最後の方で「ほとんどの」依存関係をインストールするのにgemspecをパースする必要がないっていう点は、結構重要かもね(情報がすでにgemserverから返されるなら)。
YAMLはひどいものだけど、普通のgemspecのサイズやコンテキストを考えると、psychが低い単位のMB/sのスループットで動いている時に、YAMLが大きな影響を与えるとは思えないな。
ほとんど関係ないけど、これらのメタデータファイルはそれぞれのパッケージマネージャーに取り込まれるんだ。RubyGemsに公開すると、そのファイルはデータベースに読み込まれてAPIで利用できるようになる。Pythonファイルを公開する時も、pyproject.tomlがPyPIデータベースに解析されて利用可能になるのと同じだよ。これがUVが古いPythonパッケージマネージャーより速い大きな理由で、PyPIレジストリの変更を活用できたから。今、これらのパッケージマネージャーは、パッケージ全体をダウンロードして解凍してから解析する必要なく、依存関係の計算を実行できるんだ。
最近の関連情報: How uv got so fast - https://news.ycombinator.com/item?id=46393992 - 2025年12月(457コメント)
AaronがBundlerに対して実用的なアルゴリズムやデザインの改善に焦点を当てているのは評価できるね。「Rustで書き直す」っていうのに早まって飛び込むよりも。
スピードが上がるのはいいけど、それ以上にRubyのインストールも管理してほしい。Rubyやバージョン管理ツールの混乱には本当にイライラしてる。
うーん。アーロンはクールだけど、Shopifyで働いてるしね。DHHもアーロンもgem.coopプロジェクトの誰かについて言及してないし、なんか複雑な気持ちになるな。根本的な問題は単にスピードだけじゃなくて、開発者やユーザーのコントロールにも関わってると思う。だから「早すぎる」ってラベルが変だと思うんだよね。全てをスピードのラベルにまとめようとしてるみたいで。問題はもっと広いと思う。実際、スピードにはあまり興味がないし、gemをインストールするのに0.35秒かかろうが0.45秒かかろうが、1.5秒かかろうが、正直どうでもいい。例えば、高品質なドキュメント、これはRubyが大失敗した分野だよね。全てのプロジェクトじゃないけど、多くのプロジェクトがそう。で、Rubyはなんで衰退してるの?誰もこの問題に本当に目を向けたがらないし。だから、問題は「単に」スピードの問題として見るべきじゃないと思う。つまり… Matzは過去10年間スピードに焦点を当ててきたけど、Rubyは衰退した。そろそろ他のことにもっと焦点を当てる時期かもね。人々が速いRubyを望んでるわけじゃないけど、言語が衰退してるなら、真剣に考えて、計画を立てて、いくつかのアイデアを出して、それを強く推進する必要がある。古い開発者を排除して、何も起こってないかのように振る舞うのはダメだよね。
「全てのBundlerインスタンスのためのグローバルキャッシュ」問題をじっと見つめてるんだけど、これが隠れた複雑さの地雷原なのか、それとも実際には比較的簡単なものなのかを見極めようとしてる。実装が長く続けば続くほど効果があるターゲットとして面白いね。というのも、これからのバージョンから共有されるだけだから。[1] https://github.com/ruby/rubygems/issues/7249
確かに超簡単ではないけど、最近の先行事例から学べることはたくさんあるよ。Rubyは制約を考えると、初めてこれを解決するにはあまり良い場所ではなかったかもしれない(pipに似てる)。でも、Rubyエコシステムが他のエコシステムの成果を活かさない理由はないよね。
それは確かに可能だよ。ずいぶん前にプロトタイプを書いたことがあるし。https://ra66i.org/tmp/how_gems_should_be.mov
Rubyのfibers(というかAsyncライブラリ)は、上級者のスレッド調整の問題(コネクションプールとか)を理解していないジュニアRailsエンジニアにフェティシズム的に扱われがちだと思う。でも、これはfibersの良い使い道になるかもしれないね。毎日使っているコードベースには約230のgemがあって、それらのIOバウンドなインストールをノンブロッキングコールに分けられれば、スレッドを立ち上げてそれらの間でコンテキストスイッチするよりも、かなりのパフォーマンス差が出ると思うよ。
本当に純粋なRubyで残りを絞り出すためにやることは、(約10年離れていたから新しい部分があるかもしれないけど、私が知っている限りでは意味のあるものはない)安価に解析できるインデックスフォーマットを使うことかな。昔書いたギストにこのことが書いてあるよ:https://gist.github.com/raggi/4957402。初期のアーカイブダウンロードにはスレッドを使うべきだね(これは単なるIOだから、インデックスみたいなキャッシュを再利用したい)。解凍やインストール後のステップにはいくつかのフォークを使うといいよ(これらは予測できない同時実行の挙動があるから)。
Bundlerが他のパッケージマネージャーに比べてそんなに遅いとは思わなかったな。
Rails 8.1とRuby 3も驚くほど速いし、今「おまかせ」フレームワークに戻るのは本当に新鮮な空気だね。特に今はAIツールを使って、依存関係を使わずに多くのことをゼロから実装できるから。
特に遅いとは言わないけど、サクサク動くわけじゃないよね。
gemsがrubygemsを速くするためにできる最大のことは、各gemのファイルのレジストリ/データベースを持つことだと思う。そうすれば、rubygemsは毎回`require`するたびにファイルシステムをスキャンしなくて済むようになる。もし直接gemを編集したら、壊れちゃうことになるけどね。ファイルを追加しても、メタデータが再ハッシュされるまで見つからない。gemのインストールやアンインストールなどのコマンドも、そのメタデータを維持するために修正が必要になる。でも、実際には、シェルコマンドでそんな風にgemライブラリをいじるべきじゃないし(もし手動でいじってるなら、メタデータを再生成するのはそんなに負担じゃないよ)。
何年か前にほぼこれと同じようなコードを書いたことがあるよ(確か、ディスクにキャッシュはせず、毎回新しくハッシュを構築するから、まだ大幅な速度向上が得られる)。今はおそらく時代遅れで壊れてるけど、私のお気に入りのミニプロジェクトの一つだよ。(そして、ダークモードではグラフがほとんど読めないことに気づいた)https://github.com/pmahoney/fastup
tenderloveは絶対に外さないね。
最適化:何もしない方が、何かするより早い。
賢いことをしてコードをバrrにする?いや、ダメだ。バrrにするためにバカなことはやめよう。