3 points par GN⁺ 2026-01-03 | 2 commentaires | Partager sur WhatsApp
  • 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 install exécute successivement fetch_gem_if_not_cached puis install
    • En conséquence, des gems ayant des dépendances (a -> b -> c) ne peuvent être installées que de façon séquentielle
  • 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_HOME est 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 u64 pour accélérer les comparaisons
    • En Ruby aussi, convertir Gem::Version vers 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

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

 
iolothebard 2026-01-06

Les paroles ne valent rien. Montrez-moi le code !

 
GN⁺ 2026-01-03
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-python n’est pas lié aux performances, mais à une meilleure résolution des dépendances
    Par 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 4
    uv 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

    • J’y ai pensé aussi, mais il est possible que les informations du gemspec diffèrent des métadonnées de RubyGems.org
      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

    • Les gains de vitesse sont bien, mais j’ai encore plus besoin d’une fonctionnalité qui gère directement l’installation de Ruby
      Ces environnements confus où se mélangent plusieurs outils de gestion de versions et différentes versions de Ruby sont vraiment pénibles
    • Comme Aaron est chez Shopify, j’éprouve des sentiments mitigés devant l’absence de mention du projet gem.coop
      À 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 require
    Si 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

    • J’ai déjà écrit du code assez proche de cette idée : il n’y avait pas de cache disque, mais même générer les hachages à la volée apportait un gros gain de performance
      C’est sans doute dépassé aujourd’hui, mais ça reste un mini-projet auquel je tiens
      Code : fastup
    • Optimiser bundle install est une mauvaise direction
      Le vrai problème, c’est que $LOAD_PATH ajoute tous les gems et provoque une explosion combinatoire
      L’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’ai essayé de traiter cela au runtime, mais Ruby manque de structures de données efficaces, ce qui a rendu l’implémentation difficile
    • En réalité, c’est déjà ce que fait bootsnap
      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

    • YAML n’est pas idéal, mais sur la taille habituelle d’un gemspec, l’impact sur les performances sera sans doute minime
    • S’il s’agit d’un lockfile destiné à être relu mais pas modifié à la main, on pourrait créer un parseur simple supprimant les fonctionnalités complexes de YAML
      Par exemple : une structure qui ne contient que les versions, dépendances et hachages
    • En réalité, ce type de métadonnées est déjà pré-analysé et stocké en base de données par RubyGems ou PyPI
      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

    • Pour pousser davantage en Ruby pur,
      1. utiliser un format d’index rapide à parser (gist connexe)
      2. gérer les téléchargements initiaux avec des threads
      3. séparer la décompression et le post-install avec des fork
        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

    • Ce n’est pas totalement simple, mais en s’inspirant des précédents existants dans d’autres écosystèmes, cela paraît tout à fait faisable
      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

    • Il faut abandonner l’illusion que « du code intelligent est rapide »
      La vraie optimisation, c’est de ne pas faire de travail inutile