3 Punkte von GN⁺ 2026-01-03 | 2 Kommentare | Auf WhatsApp teilen
  • Analyse der Leistungsgrenzen von Bundler und Vergleich mit den Gründen, warum der Python-Paketmanager uv so schnell ist
  • Die Geschwindigkeit von uv beruht nicht auf Rust selbst, sondern auf strukturellen Designentscheidungen wie parallelen Downloads, globalem Cache und metadatenbasierter Abhängigkeitsverarbeitung
  • Bei Bundler sind Download und Installation gekoppelt, was die Parallelisierung einschränkt; eine Trennung verspricht deutliche Verbesserungen
  • Durch Integration eines globalen Caches, Installation per Hardlink und Einbindung des PubGrub-Solvers ließen sich Doppelungen zwischen RubyGems und Bundler reduzieren
  • Auch ohne Neuschreibung in einer anderen Sprache lassen sich die meisten Performance-Verbesserungen direkt in Ruby erreichen, sodass eine Geschwindigkeit nahe uv möglich ist

Leistungsvergleich von Bundler und uv

  • Ausgehend von der auf der RailsWorld gestellten Frage „Warum ist Bundler nicht so schnell wie uv?“ werden die Performance-Engpässe von Bundler untersucht
  • Der Autor ist überzeugt, dass Bundler Geschwindigkeiten auf uv-Niveau erreichen kann, und stellt klar, dass der Unterschied nicht an der Sprache, sondern am Design liegt
  • Unter Bezug auf Andrew Nesbitts Artikel „How uv got so fast“ wird analysiert, ob sich die zentralen Optimierungen von uv auf Bundler übertragen lassen

Neuschreibung in Rust?

  • Zwar ist uv in Rust geschrieben, doch die eigentliche Ursache für die Geschwindigkeit ist nicht Rust selbst
  • Wenn das Beseitigen der Engpässe in Bundler dazu führt, dass „eine Neuschreibung in Rust die einzig verbleibende Verbesserung“ wäre, dann wäre das bereits ein Erfolg
  • Eine Neuschreibung in Rust bietet die Freiheit, experimentelle Designs ohne bestehende Kompatibilitätszwänge auszuprobieren, ist aber keine zwingende Voraussetzung

Strukturelle Engpässe in Bundler

  • Bundler koppelt den Download und die Installation von Gems in einer einzigen Methode, wodurch parallele Downloads unmöglich werden
    • Im Beispielcode führt die Methode install nacheinander fetch_gem_if_not_cached und install aus
    • Dadurch können Gems mit Abhängigkeiten (a -> b -> c) nur sequentiell installiert werden
  • Experimente zeigen: Mit Abhängigkeiten dauert es über 9 Sekunden, während unabhängige Gems (d, e, f) per parallelem Download in weniger als 4 Sekunden abgeschlossen sind
  • Durch eine Trennung von Download und Installation ließe sich parallel arbeiten, ohne die Abhängigkeitsregeln zu verletzen
    • Vorgeschlagen wird eine Aufteilung in vier Schritte (Download → Entpacken → Kompilieren → Installation)
    • Bei reinen Ruby-Gems könnte die Installationsreihenfolge der Abhängigkeiten gelockert werden, was zusätzlichen Geschwindigkeitsschub bringen würde

Cache- und Installationsoptimierung

  • Das Modell von uv mit globalem Cache und Installation per Hardlink ließe sich auch auf Bundler anwenden
    • Bundler und RubyGems verwenden derzeit getrennte Caches je Ruby-Version
    • Erforderlich wäre eine Zusammenführung in einen gemeinsamen Cache auf Basis von $XDG_CACHE_HOME
    • Die Installation per Hardlink wäre nach einer solchen Cache-Zusammenführung möglich
  • Bundler nutzt bereits den Abhängigkeits-Solver PubGrub, während RubyGems weiterhin molinillo verwendet
    • Eine Vereinheitlichung der Solver beider Systeme ist zentral für den Abbau technischer Schulden

Übertragbarkeit Rust-bezogener Optimierungen

  • Zero-copy-Deserialisierung könnte teilweise auch beim YAML-Parsing in RubyGems angewendet werden
  • Der GVL (Global VM Lock) von Ruby schränkt Parallelisierung bei IO-lastigen Aufgaben nicht stark ein
    • IO- und ZLIB-Verarbeitung geben den GVL frei, sodass parallele Ausführung möglich ist
    • Beim Schreiben kleiner Dateien kann allerdings der Overhead der GVL-Verwaltung die Leistung mindern
    • An Verbesserungen innerhalb von Ruby wird bereits gearbeitet
  • Optimierung von Versionsvergleichen: uv kodiert Versionen als u64-Integer, um Vergleiche zu beschleunigen
    • Auch in Ruby ließe sich Gem::Version in eine integerbasierte Darstellung überführen, um die Solver-Performance zu verbessern
    • Entsprechende Refactoring-Versuche gab es bereits, wurden aber wegen Abwärtskompatibilitätsproblemen aufgeschoben

Fazit und weitere Pläne

  • Die Geschwindigkeit von uv beruht eher auf einem Design, das unnötige Arbeit vermeidet, als auf der Sprache selbst; Bundler kann in dieselbe Richtung verbessert werden
  • RubyGems und Bundler verfügen bereits über eine moderne Paketverwaltungsstruktur, sodass Geschwindigkeiten auf uv-Niveau realistisch sind
  • Die größte Herausforderung bleibt Legacy-Code und die Wahrung der Kompatibilität
  • Auch ohne Neuschreibung in Rust sind 99 % der Performance-Gewinne innerhalb des Ruby-Codes erreichbar; das verbleibende 1 % fällt kaum ins Gewicht
  • In einem Folgebeitrag sollen das tatsächliche Profiling von Bundler und RubyGems sowie die konkreten Ursachen der Engpässe behandelt werden

2 Kommentare

 
iolothebard 2026-01-06

Reden ist billig. Zeig mir den Code!

 
GN⁺ 2026-01-03
Hacker-News-Kommentare
  • Ich kenne die interne Struktur von Bundler nicht im Detail, aber ich denke, die größte Verbesserung wäre die Übernahme von uvs Cache-Design.
    Ein wesentlicher Grund, warum uv so schnell ist, liegt in seiner Cache-Struktur, und das lässt sich auch in anderen Sprachen oder Ökosystemen nachbilden.
    Dass uv die Obergrenze von requires-python ignoriert, dient allerdings nicht der Performance, sondern einer besseren Abhängigkeitsauflösung.
    Wenn ein Projekt zum Beispiel Python 3.8 oder höher verlangt, aber eine Abhängigkeit die Einschränkung <4 setzt, kann es unter Python 4 nicht installiert werden.
    uv löst für alle unterstützten Versionen auf, daher spart das Ignorieren der Obergrenze praktisch keine Zeit.
    Die zugehörige Diskussion findet sich im Python-Discuss-Forum.

  • Seit PEP 658 stellt die Simple Repository API von Python Metadaten direkt bereit; RubyGems.org bietet bereits ähnliche Informationen an.
    Allerdings erkennt man erst nach dem Entpacken eines Gems, ob es native Extensions enthält.
    Deshalb der Vorschlag, diese Information direkt in die Metadaten von RubyGems.org aufzunehmen, damit sich der Baum der Abhängigkeitsinstallationen vollständig parallelisieren lässt.

    • Das habe ich auch gedacht, aber es ist möglich, dass sich die Informationen in der gemspec und in den Metadaten von RubyGems.org unterscheiden.
      Ich erinnere mich aus meiner Zeit bei RubyGems.org, dass die Metadaten versionsweise extrahiert wurden.
      Man müsste also gemspecs älterer Versionen erneut verarbeiten, und das könnte eine riskante Änderung der Metadaten sein.
      Deshalb wäre das für ältere Versionen wohl schwer umzusetzen, aber künftig könnte man die Installationsreihenfolge vielleicht auch ohne Entpacken bestimmen.
  • Mir gefällt, dass Aaron sich nicht auf eine Neuschreibung von Bundler in Rust, sondern auf echte algorithmische Verbesserungen konzentriert.

    • Mehr Geschwindigkeit ist gut, aber ich brauche eher eine Funktion, die auch die Ruby-Installation selbst verwaltet.
      Diese chaotische Umgebung mit mehreren Versionsverwaltungs-Tools und unterschiedlichen Ruby-Versionen ist wirklich frustrierend.
    • Da Aaron bei Shopify ist, fühlt es sich seltsam an, dass das Projekt gem.coop nicht erwähnt wird.
      Ich denke, das Problem ist nicht nur die Geschwindigkeit, sondern auch Kontrolle und die Ausrichtung des Ökosystems.
      Ruby hat sich in den letzten zehn Jahren auf Geschwindigkeit konzentriert, aber die Qualität der Dokumentation und die Pflege der Community waren eigentlich wichtiger.
      Jetzt ist der Zeitpunkt, ernsthaft darüber nachzudenken, warum die Sprache an Bedeutung verliert, und verschiedene Ideen entschlossen voranzutreiben.
  • Ein aktueller verwandter Beitrag ist How uv got so fast (Dezember 2025, 457 Kommentare).

  • Wenn man RubyGems schneller machen will, ist der entscheidende Punkt, die Dateiliste jedes Gems als Registry-/Datenbankeintrag zu führen.
    Dann muss beim require nicht jedes Mal das Dateisystem durchsucht werden.
    Wenn man ein Gem direkt verändert, müssten die Metadaten erneut gehasht werden, aber manuelle Änderungen sind ohnehin nicht empfohlen.

    • Ich habe früher einmal ähnlichen Code geschrieben; selbst ohne Disk-Cache brachte schon das Erzeugen der Hashes on the fly einen deutlichen Geschwindigkeitsgewinn.
      Heute ist das vielleicht veraltet, aber es bleibt ein kleines Herzensprojekt.
      Code: fastup
    • Die Optimierung von „bundle install“ ist der falsche Ansatz.
      Das eigentliche Problem ist die Struktur, bei der $LOAD_PATH alle Gems hinzufügt und dadurch eine kombinatorische Explosion auslöst.
      Dass es mehrere Cache-Projekte gibt, ist ein Beleg dafür, dass genau das das reale Problem ist.
      Früher brauchte eine App bei mir mehrere Minuten zum Starten, und durch Manipulation des Load Path konnte ich das um Minuten verkürzen.
    • Ich wollte das zur Laufzeit lösen, aber Ruby fehlen dafür effiziente Datenstrukturen, deshalb war die Umsetzung schwierig.
    • Tatsächlich ist das genau das, was bootsnap bereits macht.
      Ich hatte früher vorgeschlagen, bootsnap in bundler zu integrieren, aber das wurde abgelehnt.
  • Die Erklärung der Struktur von RubyGems war interessant.
    Ein Gem ist eine Tar-Datei, darin deklariert ein YAML-GemSpec die Abhängigkeiten.
    RubyGems.org stellt diese Information per API bereit, sodass sich Abhängigkeiten auch ohne eval prüfen lassen.
    YAML ist allerdings ein Format mit geringer Parsing-Effizienz, daher wären Alternativen wie JSON oder protobuf vielleicht besser.
    Wenn der gemserver diese Abhängigkeitsinformationen ohnehin schon zurückgibt, ist das vermutlich kein großes Problem.

    • YAML ist zwar nicht ideal, aber bei der typischen Größe einer gemspec dürfte der Performance-Effekt minimal sein.
    • Wenn eine Lockfile nur zur Prüfung, nicht zur manuellen Bearbeitung gedacht ist, könnte man einen einfachen Parser bauen, der auf komplexe YAML-Funktionen verzichtet.
      Zum Beispiel eine Struktur, die nur Version, Abhängigkeiten und Hashes enthält.
    • Tatsächlich speichern Dienste wie RubyGems oder PyPI solche Metadaten vorab geparst in einer Datenbank.
      Das ist auch ein Grund, warum uv schnell ist — die Abhängigkeiten lassen sich berechnen, ohne Pakete herunterzuladen.
  • Ich habe früher einmal ein Prototyp-Video erstellt, wie die Gem-Installation aussehen sollte.
    how_gems_should_be.mov

  • Die Fibers von Ruby (oder die Async-Bibliothek) werden oft überschätzt.
    Wie bei Threads bleiben übergeordnete Koordinationsprobleme wie Connection Pools weiterhin bestehen.
    Trotzdem kann asynchrone Verarbeitung bei IO-gebundenen Installationsaufgaben einen spürbaren Performance-Gewinn bringen.

    • Wenn ich in reinem Ruby noch mehr herausholen wollte, würde ich wohl so vorgehen:
      1. ein Indexformat mit schnellem Parsing verwenden (zugehöriger Gist)
      2. den initialen Download per Threads erledigen
      3. Entpacken und Post-Install per fork auslagern
        So in etwa.
  • Die Idee eines globalen Caches, den alle Bundler-Instanzen gemeinsam nutzen, wird derzeit geprüft.
    Langfristig dürfte das viel bringen, aber es wird noch abgewogen, ob versteckte Komplexität dahintersteckt.
    Zugehöriges Issue: rubygems #7249

    • Ganz trivial ist es nicht, aber mit Blick auf bereits existierende Beispiele aus anderen Ökosystemen sollte es gut machbar sein.
      Ruby löst dieses Problem nicht als erstes, also ist es an der Zeit, nun ebenfalls davon zu profitieren.
  • Das Grundprinzip der Optimierung ist einfach — am schnellsten ist, nichts zu tun.

    • Man muss die Illusion aufgeben, dass „cleverer Code schnell ist“.
      Die wirkliche Optimierung besteht darin, unnötige Arbeit gar nicht erst zu machen.