Kann Bundler so schnell werden wie uv?
(tenderlovemaking.com)- 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
installnacheinanderfetch_gem_if_not_cachedundinstallaus - Dadurch können Gems mit Abhängigkeiten (
a -> b -> c) nur sequentiell installiert werden
- Im Beispielcode führt die Methode
- 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::Versionin eine integerbasierte Darstellung überführen, um die Solver-Performance zu verbessern - Entsprechende Refactoring-Versuche gab es bereits, wurden aber wegen Abwärtskompatibilitätsproblemen aufgeschoben
- Auch in Ruby ließe sich
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
Reden ist billig. Zeig mir den Code!
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-pythonignoriert, 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
<4setzt, 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.
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.
Diese chaotische Umgebung mit mehreren Versionsverwaltungs-Tools und unterschiedlichen Ruby-Versionen ist wirklich frustrierend.
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
requirenicht 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.
Heute ist das vielleicht veraltet, aber es bleibt ein kleines Herzensprojekt.
Code: fastup
Das eigentliche Problem ist die Struktur, bei der
$LOAD_PATHalle 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 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.
Zum Beispiel eine Struktur, die nur Version, Abhängigkeiten und Hashes enthält.
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.
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
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.
Die wirkliche Optimierung besteht darin, unnötige Arbeit gar nicht erst zu machen.