3 puntos por GN⁺ 2026-01-03 | 2 comentarios | Compartir por WhatsApp
  • Analiza los límites de rendimiento de Bundler y compara por qué el gestor de paquetes de Python uv es rápido
  • La velocidad de uv no se debe al lenguaje Rust, sino a su diseño estructural, como descargas en paralelo, caché global y manejo de dependencias basado en metadatos
  • Bundler tiene acoplados los procesos de descarga e instalación, lo que limita el procesamiento en paralelo; si se separan, hay un gran margen de mejora
  • Se puede reducir la duplicación entre RubyGems y Bundler mediante integración de caché global, instalación con hardlinks e integración del resolvedor PubGrub
  • Incluso sin reescribir el lenguaje, la mayor parte de las mejoras de rendimiento puede lograrse dentro del código Ruby, acercándose a velocidades del nivel de uv

Comparación de rendimiento entre Bundler y uv

  • A partir de la pregunta planteada en RailsWorld, “¿por qué Bundler no es tan rápido como uv?”, se investigan los cuellos de botella de rendimiento de Bundler
  • El autor afirma con convicción que Bundler puede alcanzar velocidades del nivel de uv, y deja claro que la diferencia de rendimiento no es un problema del lenguaje, sino del diseño
  • Citando el artículo de Andrew Nesbitt “How uv got so fast”, analiza si las optimizaciones clave de uv pueden aplicarse a Bundler

¿Hace falta reescribirlo en Rust?

  • Es cierto que uv está escrito en Rust, pero la causa esencial de su velocidad no es Rust en sí
  • Si al eliminar los cuellos de botella de Bundler se llegara al punto en que “la única mejora restante sería reescribirlo en Rust”, eso se consideraría un éxito
  • Reescribirlo en Rust ofrece libertad para probar diseños experimentales sin las restricciones de compatibilidad existentes, pero no es un requisito indispensable

Cuellos de botella estructurales de Bundler

  • Bundler combina la descarga e instalación de gems en un solo método, lo que impide las descargas en paralelo
    • En el código de ejemplo, el método install ejecuta fetch_gem_if_not_cached y luego install de forma consecutiva
    • Por eso, gems con relaciones de dependencia (a -> b -> c) solo pueden instalarse de manera secuencial
  • En experimentos, cuando hay dependencias tarda más de 9 segundos, pero gems independientes (d, e, f) se completan en menos de 4 segundos con descargas en paralelo
  • Si se separan la descarga y la instalación, se puede procesar en paralelo sin romper las reglas de dependencia
    • Se propone separar en cuatro etapas (descarga → descompresión → compilación → instalación)
    • En gems Ruby puros, relajar el orden de instalación de dependencias podría aportar mejoras adicionales de velocidad

Optimización de caché e instalación

  • El enfoque de uv de caché global e instalación con hardlinks también puede aplicarse a Bundler
    • Actualmente, Bundler y RubyGems usan cachés separadas según la versión de Ruby
    • Hace falta integrarlas en una caché compartida basada en $XDG_CACHE_HOME
    • La instalación con hardlinks podría aplicarse después de unificar la caché
  • Bundler ya usa el resolvedor de dependencias PubGrub, pero RubyGems todavía usa molinillo
    • La integración de ambos resolvedores es clave para eliminar deuda técnica

Qué optimizaciones asociadas a Rust sí pueden aplicarse

  • La deserialización zero-copy podría aplicarse parcialmente en la etapa de parsing YAML de RubyGems
  • El GVL (Global VM Lock) de Ruby no representa una gran limitación para el paralelismo en tareas centradas en IO
    • El procesamiento de IO y ZLIB libera el GVL, por lo que pueden ejecutarse en paralelo
    • Sin embargo, al escribir archivos pequeños, la sobrecarga de gestionar el GVL sí reduce el rendimiento
    • Ya se está trabajando en mejorar esto dentro de Ruby
  • Optimización de comparación de versiones: uv acelera estas comparaciones codificando versiones como enteros u64
    • En Ruby también podría mejorarse el rendimiento del resolvedor convirtiendo Gem::Version a una representación basada en enteros
    • Ya hubo intentos de refactorización relacionados, pero se dejaron en pausa por problemas de compatibilidad hacia atrás

Conclusión y próximos pasos

  • La velocidad de uv se debe más a un diseño que elimina trabajo innecesario que al lenguaje, y Bundler también puede mejorar en esa misma dirección
  • RubyGems y Bundler ya cuentan con una estructura moderna de gestión de paquetes, por lo que alcanzar velocidades del nivel de uv es realista
  • El mayor desafío es mantener compatibilidad con código legado
  • Incluso sin reescribirlo en Rust, el 99% de la mejora de rendimiento puede lograrse dentro del código Ruby; el 1% restante sería marginal
  • En una publicación posterior se abordarán el profiling real de Bundler y RubyGems, y las causas concretas de sus cuellos de botella

2 comentarios

 
iolothebard 2026-01-06

¡Hablar es barato. Muéstrame el código!

 
GN⁺ 2026-01-03
Comentarios en Hacker News
  • No conozco a fondo la estructura de Bundler, pero creo que la mayor mejora sería adoptar el diseño de caché de uv
    Una parte clave de por qué uv es rápido está en su estructura de caché, y eso se puede replicar en otros lenguajes o ecosistemas
    Aun así, la parte de ignorar el límite superior de requires-python no es por rendimiento, sino para una mejor resolución de dependencias
    Por ejemplo, si un proyecto requiere Python 3.8 o superior, pero alguna dependencia impone la restricción <4, entonces no se podría instalar en Python 4
    uv resuelve para todas las versiones compatibles, así que ignorar el límite superior casi no ahorra tiempo
    La discusión relacionada puede verse en el foro Python Discuss

  • Después de PEP 658, la Simple Repository API de Python entrega los metadatos directamente, y RubyGems.org ya ofrece información parecida
    Pero solo al descomprimir un gem se puede saber si tiene extensiones nativas
    Por eso se propone que esta información se agregue directamente a los metadatos de RubyGems.org, para poder paralelizar por completo el árbol de instalación de dependencias

    • Yo pensé lo mismo, pero existe la posibilidad de que la información del gemspec y los metadatos de RubyGems.org difieran
      Recuerdo que cuando trabajaba en RubyGems.org, los metadatos se extraían por versión
      Habría que volver a procesar los gemspec de versiones antiguas, y eso podría ser un cambio riesgoso de metadatos
      Así que quizá sea difícil aplicarlo a versiones anteriores, pero hacia adelante sí podría mejorarse para conocer el orden de instalación sin necesidad de unpack
  • Me gusta que Aaron se enfoque en mejoras algorítmicas reales en lugar de reescribir Bundler en Rust

    • Mejorar la velocidad está bien, pero yo necesito más una función que también administre la instalación de Ruby
      Ese entorno confuso donde se mezclan varias herramientas de gestión de versiones y distintas versiones de Ruby es realmente desesperante
    • Como Aaron trabaja en Shopify, me deja una sensación ambigua que no mencione el proyecto gem.coop
      Creo que el problema no es solo la velocidad, sino también el control y la dirección del ecosistema
      Ruby se ha enfocado en la velocidad durante los últimos 10 años, pero la calidad de la documentación y la gestión de la comunidad eran todavía más importantes
      Ya es momento de pensar en serio por qué el lenguaje está perdiendo fuerza y empujar ideas diversas
  • Como lectura relacionada reciente está How uv got so fast (diciembre de 2025, 457 comentarios)

  • Para hacer RubyGems más rápido, la clave es registrar o guardar en una base de datos la lista de archivos de cada gem
    Así no haría falta escanear el sistema de archivos cada vez que se ejecuta require
    Si se modifica un gem directamente, habría que volver a hacer hash de los metadatos, pero de todos modos no se recomienda editarlo manualmente

    • Hace tiempo escribí código parecido; no tenía caché en disco, pero incluso generar hashes al vuelo daba una mejora importante de velocidad
      Ahora ya estará anticuado, pero sigue siendo un miniproyecto al que le tengo cariño
      Código: fastup
    • Optimizar bundle install es atacar el problema por el lado equivocado
      El problema real es que $LOAD_PATH agrega todos los gem y provoca una explosión combinatoria
      El hecho de que existan varios proyectos de caché demuestra que este sí es un problema real
      Antes una app tardaba minutos en arrancar, y una vez logré reducirla en minutos manipulando el load path
    • Intenté resolver esto en runtime, pero en Ruby faltan estructuras de datos eficientes, así que fue difícil de implementar
    • En realidad esto ya es lo que hace bootsnap
      Antes propuse integrar bootsnap en bundler, pero lo rechazaron
  • Me pareció interesante la explicación de la estructura de RubyGems
    Un gem es un archivo tar, y dentro lleva un YAML GemSpec que declara las dependencias
    RubyGems.org expone esa información por API, así que se pueden revisar dependencias sin usar eval
    Aun así, YAML es un formato poco eficiente de parsear, y quizá una alternativa como JSON o protobuf sería mejor
    Pero si gemserver ya devuelve la información de dependencias, probablemente no sea un gran problema

    • YAML no es gran cosa, pero para el tamaño típico de un gemspec, el impacto en rendimiento parece mínimo
    • Si el lockfile es solo para revisión y no para edición manual, se podría hacer un parser simple quitando las funciones complejas de YAML
      Por ejemplo, una estructura que incluya solo versión, dependencias y hash
    • En realidad RubyGems y PyPI ya parsean y guardan estos metadatos en una base de datos por adelantado
      Esa también es una razón de por qué uv es rápido: puede calcular dependencias sin descargar el paquete
  • Hace tiempo hice un video prototipo sobre cómo debería funcionar la instalación de gems
    how_gems_should_be.mov

  • Las fibers de Ruby (o la librería Async) suelen estar sobrevaloradas
    Igual que con los threads, siguen existiendo problemas de coordinación de nivel superior, como los pools de conexiones
    Aun así, si las tareas de instalación ligadas a IO se manejan de forma asíncrona, se puede lograr una mejora de rendimiento significativa

    • Si hubiera que exprimir un poco más desde Ruby puro,
      1. usar un formato de índice rápido de parsear (gist relacionado)
      2. hacer la descarga inicial con threads
      3. separar la descompresión y el post-install con fork
        Creo que iría por un enfoque así
  • Se está evaluando la idea de que una caché global sea compartida por todas las instancias de bundler
    A largo plazo parece que daría una gran ventaja, pero se está revisando si hay complejidades ocultas
    Issue relacionado: rubygems #7249

    • No es totalmente simple, pero viendo los casos previos en otros ecosistemas, parece perfectamente viable
      Ruby no es el primero en resolver este problema, así que ya va siendo hora de aprovechar esos beneficios
  • El principio básico de la optimización es simple: no hacer nada es lo más rápido

    • Hay que abandonar la ilusión de que “el código inteligente es rápido”
      La verdadera optimización es evitar hacer trabajo innecesario