Bundler peut-il être aussi rapide que uv ?
(tenderlovemaking.com)- Analyse des limites de performance de Bundler et comparaison avec les raisons de la rapidité de uv, le gestionnaire de paquets Python
- La vitesse de uv ne vient pas du langage Rust, mais de choix d’architecture comme les téléchargements parallèles, le cache global et le traitement des dépendances basé sur les métadonnées
- Bundler couple les phases de téléchargement et d’installation, ce qui limite le traitement parallèle, mais les séparer permettrait de nets progrès
- Intégration d’un cache global, installation par liens physiques, intégration du solveur PubGrub : autant de pistes pour réduire les doublons entre RubyGems et Bundler
- Même sans réécrire l’outil dans un autre langage, l’essentiel des gains de performance peut être obtenu en Ruby, avec un niveau de vitesse proche de celui de uv
Comparaison des performances entre Bundler et uv
- L’enquête sur les goulots d’étranglement de Bundler part d’une question posée à RailsWorld : « Pourquoi Bundler n’est-il pas aussi rapide que uv ? »
- L’auteur affirme être convaincu que Bundler peut atteindre un niveau de performance comparable à celui de uv, et souligne que l’écart vient de la conception, pas du langage
- En s’appuyant sur l’article d’Andrew Nesbitt, “How uv got so fast”, il examine dans quelle mesure les optimisations clés de uv pourraient être appliquées à Bundler
Faut-il le réécrire en Rust ?
- uv est bien écrit en Rust, mais la cause fondamentale de sa rapidité n’est pas Rust lui-même
- Si l’on parvient à supprimer les goulots d’étranglement de Bundler au point que « le réécrire en Rust » devienne la seule amélioration restante, alors ce sera déjà une réussite
- Une réécriture en Rust offrirait la liberté d’expérimenter une nouvelle conception sans les contraintes de compatibilité existantes, mais ce n’est pas une condition indispensable
Les goulots d’étranglement structurels de Bundler
- Bundler combine le téléchargement et l’installation des gems dans une seule méthode, ce qui empêche les téléchargements parallèles
- Dans l’exemple de code, la méthode
installexécute successivementfetch_gem_if_not_cachedpuisinstall - En conséquence, des gems ayant des dépendances (
a -> b -> c) ne peuvent être installées que de façon séquentielle
- Dans l’exemple de code, la méthode
- Les expérimentations montrent qu’en présence de dépendances, l’opération prend plus de 9 secondes, alors que des gems indépendantes (
d, e, f) sont téléchargées en parallèle et terminées en moins de 4 secondes - Séparer téléchargement et installation permettrait de respecter les règles de dépendances tout en autorisant le parallélisme
- Proposition de découpage en quatre étapes : téléchargement → décompression → compilation → installation
- Pour les gems Ruby purs, on pourrait aussi assouplir l’ordre d’installation des dépendances pour gagner encore en vitesse
Optimisation du cache et de l’installation
- Le modèle de cache global et d’installation par liens physiques de uv pourrait aussi être appliqué à Bundler
- Aujourd’hui, Bundler et RubyGems utilisent des caches séparés selon les versions de Ruby
- Une unification vers un cache partagé basé sur
$XDG_CACHE_HOMEest nécessaire - L’installation par liens physiques pourrait être introduite après cette unification du cache
- Bundler utilise déjà le solveur de dépendances PubGrub, mais RubyGems s’appuie encore sur molinillo
- L’unification des solveurs des deux systèmes est présentée comme la clé pour résorber cette dette technique
Applicabilité des optimisations liées à Rust
- La désérialisation zero-copy pourrait être partiellement applicable à l’étape de parsing YAML de RubyGems
- Le GVL (Global VM Lock) de Ruby n’est pas un obstacle majeur pour les tâches orientées IO
- Les opérations d’IO et de ZLIB libèrent le GVL, ce qui permet une exécution parallèle
- En revanche, l’écriture de petits fichiers subit le surcoût de gestion du GVL, ce qui pénalise les performances
- Des travaux sont en cours dans Ruby pour améliorer ce point
- Optimisation de la comparaison de versions : uv encode les versions sous forme d’entiers
u64pour accélérer les comparaisons- En Ruby aussi, convertir
Gem::Versionvers une représentation entière pourrait améliorer les performances du solveur - Une tentative de refactorisation a déjà existé, mais a été mise en pause pour des raisons de compatibilité descendante
- En Ruby aussi, convertir
Conclusion et suite
- La rapidité de uv vient moins du langage que d’une conception qui élimine le travail inutile, et Bundler peut progresser dans la même direction
- RubyGems et Bundler disposent déjà d’une architecture moderne de gestion des paquets, ce qui rend réaliste l’objectif d’atteindre des performances proches de uv
- Le principal défi reste la compatibilité avec le code legacy
- Même sans réécriture en Rust, 99 % des gains de performance sont possibles directement en Ruby, le 1 % restant étant marginal
- Un prochain article abordera le profilage réel de Bundler et RubyGems ainsi que les causes concrètes des goulots d’étranglement
2 commentaires
Les paroles ne valent rien. Montrez-moi le code !
Avis sur Hacker News
Je ne connais pas assez bien l’architecture de Bundler, mais je pense que la plus grande amélioration serait d’adopter la conception du cache de uv
L’une des principales raisons pour lesquelles uv est rapide réside dans la structure de son cache, et cela peut être reproduit dans d’autres langages ou écosystèmes
En revanche, le fait d’ignorer la borne supérieure de
requires-pythonn’est pas lié aux performances, mais à une meilleure résolution des dépendancesPar exemple, si un projet exige Python 3.8 ou plus, mais qu’une dépendance impose la contrainte
<4, l’installation devient impossible avec Python 4uv résout pour toutes les versions prises en charge, donc ignorer la borne supérieure ne fait pratiquement pas gagner de temps
Une discussion connexe est disponible sur le forum Python Discuss
Depuis la PEP 658, l’API Simple Repository de Python fournit directement les métadonnées ; RubyGems.org propose déjà des informations similaires
Mais on ne peut savoir s’il y a une extension native qu’après avoir décompressé le gem
Je me demande donc s’il ne faudrait pas ajouter directement cette information aux métadonnées de RubyGems.org afin de paralléliser complètement l’arbre d’installation des dépendances
Quand je travaillais chez RubyGems.org, je me souviens que les métadonnées étaient extraites version par version
Il faudrait retraiter les gemspec des anciennes versions, ce qui pourrait constituer un changement de métadonnées risqué
Ce serait donc difficile à appliquer aux anciennes versions, mais à l’avenir on pourrait sans doute améliorer cela pour connaître l’ordre d’installation sans décompression
J’apprécie qu’Aaron se concentre sur des améliorations algorithmiques concrètes plutôt que sur une réécriture de Bundler en Rust
Ces environnements confus où se mélangent plusieurs outils de gestion de versions et différentes versions de Ruby sont vraiment pénibles
À mon avis, le problème n’est pas seulement la vitesse, mais aussi le contrôle et l’orientation de l’écosystème
Ruby s’est concentré sur la vitesse ces dix dernières années, mais la qualité de la documentation et la gestion de la communauté étaient sans doute plus importantes
Il est temps de réfléchir sérieusement aux raisons du déclin du langage et de pousser des idées variées
Parmi les billets récents sur le sujet, il y a How uv got so fast (décembre 2025, 457 commentaires)
Pour accélérer RubyGems, la clé serait de mettre en registre / base de données la liste des fichiers de chaque gem
Cela éviterait d’avoir à scanner le système de fichiers à chaque
requireSi on modifie directement un gem, il faut re-hacher les métadonnées, mais de toute façon les modifications manuelles ne sont pas recommandées
C’est sans doute dépassé aujourd’hui, mais ça reste un mini-projet auquel je tiens
Code : fastup
bundle installest une mauvaise directionLe vrai problème, c’est que
$LOAD_PATHajoute tous les gems et provoque une explosion combinatoireL’existence de plusieurs projets de cache prouve bien que c’est un vrai problème
À une époque, le démarrage d’une application prenait plusieurs minutes, et j’ai déjà réussi à gagner des minutes entières en manipulant le load path
J’avais autrefois proposé d’intégrer bootsnap à bundler, mais cela a été refusé
L’explication de l’architecture de RubyGems était intéressante
Un gem est un fichier tar, et le YAML GemSpec qu’il contient déclare les dépendances
RubyGems.org fournit ces informations via l’API, donc on peut vérifier les dépendances sans eval
Cela dit, YAML est un format peu efficace à parser, donc des alternatives comme JSON ou protobuf pourraient être meilleures
Malgré cela, si gemserver renvoie déjà les informations de dépendances, cela ne semble pas être un gros problème
Par exemple : une structure qui ne contient que les versions, dépendances et hachages
C’est aussi pour cela que uv est rapide — il peut calculer les dépendances sans télécharger les paquets
J’avais autrefois réalisé une vidéo prototype pour améliorer la manière dont les gems sont installés
how_gems_should_be.mov
Les fibers de Ruby (ou la bibliothèque Async) sont souvent surestimés
Comme avec les threads, des problèmes de coordination de plus haut niveau comme les pools de connexions subsistent
Malgré tout, si on traite les tâches d’installation orientées IO de manière asynchrone, on peut obtenir des gains de performance significatifs
c’est probablement ainsi que j’aborderais la chose
L’idée d’un cache global partagé par toutes les instances de bundler est à l’étude
À long terme, cela semble pouvoir apporter de gros bénéfices, mais on essaie encore d’évaluer s’il existe une complexité cachée
Issue connexe : rubygems #7249
Ruby n’est pas le premier à résoudre ce problème, donc il est temps d’en récolter les bénéfices
Le principe de base de l’optimisation est simple : ne rien faire est ce qu’il y a de plus rapide
La vraie optimisation, c’est de ne pas faire de travail inutile