- Las implementaciones alternativas como runtimes de lenguajes o JIT pueden ofrecer mejor rendimiento, pero su adopción puede verse limitada porque deben seguir constantemente los cambios de la implementación canónica y las expectativas de los usuarios
- PyPy, LuaJIT y TruffleRuby mostraron un gran rendimiento de ejecución, pero la brecha de compatibilidad y la carga de seguir nuevas funciones se convirtieron en barreras para su despliegue real
- YJIT eligió integrarse dentro de CRuby en lugar de ser otra implementación separada de Ruby, por lo que fue diseñado desde el inicio para ser 100% compatible con las funciones de CRuby, y se ha desplegado en Shopify, Discourse y GitHub, entre otros
- Una opción como Crystal, que se parece mucho a un lenguaje existente pero no es compatible con él, puede hacer que los usuarios se enfrenten continuamente a diferencias de “casi Ruby, pero no Ruby”
- En áreas donde el estándar público está separado de las implementaciones, como los parsers JSON o JavaScript, la carga de una implementación alternativa disminuye; pero en ecosistemas donde la implementación canónica es de facto el estándar, se necesita una estrategia distinta
La trampa recurrente de las implementaciones alternativas
- En el mundo del software, se repite el caso de proyectos que comenzaron como una mejor implementación alternativa de un sistema existente, pero terminan atrapados bajo la sombra de la implementación canónica
- Las implementaciones alternativas se comparan con la implementación canónica (canonical implementation), que es aceptada como estándar en funciones, rendimiento, ecosistema y expectativas de los usuarios
- Si la implementación canónica sigue cambiando, la implementación alternativa tiene que gastar mucha energía en seguir esos cambios en vez de definir su propia dirección
- Cuando se añade una implementación JIT a un lenguaje que tradicionalmente era interpretado, las funciones nuevas suelen entrar antes al intérprete, lo que aumenta la carga de seguimiento para el lado del JIT
El patrón observado en PyPy, LuaJIT y TruffleRuby
- PyPy es un compilador JIT avanzado para Python que podía lograr grandes mejoras de velocidad frente a CPython, pero su uso real fue muy limitado
- Python es un objetivo en movimiento: regularmente aparecen nuevas versiones de CPython y nuevas funciones
- PyPy tuvo dificultades para seguir ese ritmo y terminó quedándose siempre varias versiones detrás de Python
- Hacer que el software de Python fuera compatible con PyPy implicaba limitar qué funciones de Python se podían usar, y la mayoría de los programadores de Python no quería preocuparse por eso
- LuaJIT ofreció grandes mejoras de rendimiento frente a la implementación estándar de Lua basada en intérprete, y recibió una alta valoración junto con cierto nivel de adopción real
- Muchas personas consideran que el creador de LuaJIT, Mike Pall, es un programador sobresaliente
- A medida que el lenguaje Lua siguió añadiendo funciones nuevas, LuaJIT también fue quedándose varias versiones atrás
- Algunos usuarios de Lua evitan usar LuaJIT por este motivo
- Lua es un lenguaje conocido por su minimalismo, pero no se hicieron esfuerzos por ralentizar la incorporación de nuevas funciones ni por coordinarse con Mike Pall
- TruffleRuby mostró las cifras de rendimiento más impresionantes entre los JIT para Ruby, pero su despliegue fue limitado
- Una razón práctica es que el tiempo de calentamiento de TruffleRuby es mucho mayor que el de CRuby
- Mientras CRuby seguía añadiendo funciones, los colaboradores de TruffleRuby tenían que esforzarse por mantenerse al día
- Los usuarios de Ruby ven a CRuby como la implementación canónica y consideran que una implementación que no sea totalmente compatible tiene poco valor
El camino distinto que tomó YJIT
- YJIT comenzó como otro JIT para Ruby, pero se construyó dentro del propio CRuby en lugar de ser una implementación separada
- Esta elección generó varios trade-offs de diseño, pero permitió que YJIT fuera 100% compatible con todas las funciones de CRuby desde el inicio
- Actualmente YJIT es el JIT “oficial” de Ruby y está desplegado en Shopify, Discourse y GitHub, entre otros
- Los usuarios que visitan github.com o una tienda de Shopify ya han interactuado con YJIT
- Hasta ahora, YJIT ha sido el compilador JIT para Ruby con mayor éxito, y la compatibilidad ha desempeñado un papel clave en ese éxito
“Si no puedes vencerlos, úneteles” no basta
- Si te posicionas como una implementación alternativa, es muy probable que termines jugando constantemente a alcanzar a la implementación canónica dentro de su sombra
- Si el proyecto canónico sigue evolucionando, la implementación alternativa se ve obligada a ir detrás con un poder de decisión limitado sobre la dirección de su propio proyecto
- Integrarse a la implementación canónica puede dar mejores resultados, pero eso no resuelve todos los casos
- Crystal, dentro del ecosistema Ruby, es un lenguaje compilado estáticamente con una sintaxis similar a Ruby y que usa inferencia de tipos
- Crystal se separó de Ruby deliberadamente sin buscar compatibilidad con Ruby
- Para los rubyistas, se percibe como un lenguaje de “casi Ruby, pero no completamente Ruby”, y en la práctica tiene muchas diferencias sutiles e incompatibilidades
- Esa similitud altera las expectativas de los usuarios y genera confusión
- Crystal podría haber obtenido mejores resultados si no se hubiera promocionado desde el inicio como algo parecido a Ruby
La opción de evitar competir y tener una dirección propia
- La frase de Peter Thiel “competition is for losers” se usa en el sentido de no colocarse uno mismo en una posición donde tenga que competir innecesariamente
- De ahí surge el consejo de que, si vas a crear un nuevo lenguaje de programación, es mejor no intentar hacer un subconjunto de Python o algo superficialmente demasiado cercano a un lenguaje existente
- Si creas algo propio, puedes desarrollar el sistema a tu propio ritmo y en tu propia dirección sin quedar atado a la expectativa de igualar el rendimiento, el conjunto de funciones o el ecosistema de librerías de otra implementación
- Este consejo aplica en situaciones donde existe una implementación canónica para un lenguaje o sistema
- Las áreas con estándares públicos pueden ser una excepción
- Un parser JSON es un objetivo viable para una implementación propia porque tiene una especificación clara, relativamente pequeña y que no cambia rápido
- JavaScript puede tener varias implementaciones basadas en navegadores porque existe un organismo de estandarización externo que gestiona la especificación de JS
- Quienes trabajan en el estándar de JS entienden que las implementaciones con compilación JIT son importantes para el rendimiento y guían la evolución del lenguaje en consecuencia
- No están jugando al juego de añadir la mayor cantidad posible de funciones nuevas lo más rápido posible
1 comentarios
Opiniones en Hacker News
Hay otro punto importante que el OP pasó por alto. Cuando haces una implementación alternativa, normalmente su arquitectura es distinta a la implementación de referencia, y algo que es fácil en la implementación de referencia puede ser muy difícil en la tuya.
Por ejemplo, supongamos que un software privativo para reportes financieros guarda documentos en un formato binario raro. Al crear una alternativa gratuita, elegiste una estructura que lee el documento completo en memoria y, al guardar, vuelve a escribir el archivo entero; mientras que el original, hecho en una época con poca RAM, quizá leía y escribía solo la sección en la que estaba trabajando el usuario e incluso permitía modificaciones in-place.
Después, si el original agrega una función para adjuntar archivos al documento, archivos grandes como grabaciones de llamadas con inversionistas o PDFs escaneados de cientos de páginas funcionan bien gracias a la carga por secciones. En cambio, tu implementación deserializa el documento completo, así que en cuanto el documento supera la RAM del usuario empiezan los problemas, y un cambio que en el original le tomaría una semana a un solo desarrollador podría obligarte a rediseñar todo el software.
Y a la inversa, también hubo funciones que eran más difíciles en la implementación original pero triviales en la segunda. Solo hubo un gran rediseño, y fue en la implementación original, que tenía una suposición que provocaba una explosión exponencial.
Un caso público es el problema de ejecutar aplicaciones de Windows en Linux. El kernel de Linux es una implementación totalmente distinta de NT y ni siquiera apunta a ser compatible, pero ejecutar apps de Windows no requiere rediseñar todo el kernel. Bastan algunas capacidades de kernel relativamente comunes y una capa de compatibilidad en espacio de usuario. Wine requiere mucho esfuerzo para escribirse y mantenerse, pero muchísimo menos que la propia implementación de Windows, y funciona sobre una plataforma que no fue diseñada para ser compatible con Windows. Eso sí, como dice el texto, hay que seguirle el paso continuamente a Windows, y además aparece código que depende de bugs de la implementación de referencia, así que para ser compatible incluso con los bugs primero tienes que averiguar qué bugs debes implementar.
Por eso, cualquier cosa puede modificar otra durante la ejecución. En la práctica no se usa tanto así, pero si intentas quitarlo la gente se enfurece. Una implementación que realmente compile Python tiene que manejar situaciones donde un hilo de repente cambia algo debajo de otro hilo.
Es una de las formas en que una empresa pequeña puede aumentar la carga de una grande, y también algo que puede hacer una empresa que haya descuidado menos su deuda técnica. Además, es uno de esos raros momentos en que puedes mostrarle claramente la deuda técnica a la dirección, porque puedes decir: “Nos toma más tiempo implementar esta función que a Acme”.
Estoy de acuerdo con la idea de que “no intentes hacer un subconjunto de Python”. Los proyectos que se venden como “es Python, pero X es mejor” siempre la tienen difícil para competir contra la implementación de referencia, especialmente si X es velocidad. Quien usa un lenguaje de tipado dinámico muchas veces, al final, no se preocupa tanto por la velocidad de ejecución.
Pero una implementación alternativa no siempre fracasa. MicroPython parece haber tenido bastante éxito aunque casi no soporta más allá del nivel de Python 3.4, porque fue diseñado para correr en microcontroladores y no compite con CPython sino con otros entornos de programación para microcontroladores.
Aun así, imagino que los mantenedores de MicroPython reciben muchas solicitudes para funciones más recientes de Python. En algún momento pensé en una implementación alternativa de Python ligera y enfocada en embebido para usar dentro de aplicaciones; en ese caso la idea no era competir con CPython sino con Lua. Pero la solicitud de función número uno era: “¿Soporta NumPy?”
Al mismo tiempo, parece que del lado de CPython también hay trabajo en marcha para que funcione mejor en el frontend, y el principal punto de dolor es el tamaño de los paquetes.
Aprendí algo parecido al crear una startup. Si lo hiciera de nuevo, evitaría agresivamente las funciones mínimas de entrada de nuestro sector.
En su lugar, habría hecho solo lo mínimo necesario para dar confianza de que nuestra arquitectura podía soportar requisitos de estilo enterprise, y habría concentrado todo en los diferenciadores que hacen que alguien piense “ah, ya veo hasta dónde puede llegar esto”. No en funciones que provoquen una reacción de “ah, solo es una copia de X”.
La estrategia que describes se parece más a implementar las funciones básicas obligatorias, pero sin profundizar demasiado en las ampliaciones “normales” que vienen después. Es una forma de lograr que te vuelvan a contactar por las funciones interesantes y, al mismo tiempo, tener lo suficiente para no quedar fuera por falta de lo esencial.
Me pasa algo parecido con todo el código wrapper. A veces alguien dice: “necesitamos una versión interna de esta API”.
Las razones varían, pero suelen ser cosas como “no podemos confiar en que se use bien la API oficial”. Puede pasar, pero la versión interna tiene menos estándares y peor documentación.
A veces la razón es “necesitamos funcionalidad extra”; si ese es el caso, no hace falta envolver toda la API, basta con agregar 3 funciones. Con el tiempo, el 99% del codebase puede volverse un polyfill.
La idea central es que, si no usas los valores por defecto, después le causas un gran dolor a quien herede el codebase.
Por ejemplo, la librería ziggy-pydust para escribir módulos nativos de Python en Zig definitivamente se ve mejor que un import común de Python.h. Aun así, también tiene
.ffi, que todavía no está implementado, para acceder directamente a funciones que están en Python.h.Si no existe esa opción, normalmente termino descartando ese tipo de librerías y prefiriendo el original. Aunque, en cierto sentido, eso también es un wrapper, o sea, un módulo nativo, y para velocidad de desarrollo a veces conviene más usar ctypes directamente.
Es un buen texto y tiene muchas lecciones excelentes, pero le falta un ingrediente central. Esto también se parece a cualquier alternativa competitiva frente a un producto.
Es parecido a decir que Amazon fracasó porque no tenía librerías físicas tradicionales con las que la gente ya estaba familiarizada, cuando en realidad no fue así.
La razón por la que estas alternativas de JIT fracasaron y siguieron yendo detrás, en términos prácticos, es que a la mayoría de los desarrolladores del lenguaje X no les importa tanto el JIT. Más precisamente, valoran más las funciones del lenguaje y la interoperabilidad que el JIT.
Por eso gana el producto que “se suma” en lugar de competir. No puede ofrecer mayor estabilidad ni mejor interoperabilidad.
Llevo mucho tiempo trabajando con lenguajes y compiladores, y este texto me pareció muy interesante. Dicho de otra manera, un lenguaje es mucho más que la simple velocidad de compilación.
La velocidad de compilación es muy importante y sin duda entra en el top 10 de dimensiones. En especial porque, si mejoras la velocidad de compilación, aumenta la velocidad del ciclo de feedback del desarrollador, y el equipo central puede mejorar más rápido todas las demás dimensiones.
Aun así, en un lenguaje de programación hay más de otras 30 dimensiones muy importantes.
La conclusión sobre cómo puede tener éxito este tipo de proyectos es buena, pero todavía se habla poco de otro factor sobre por qué muchos proyectos no despegan. La compatibilidad de una implementación alternativa suele ser en la práctica menor de lo que se afirma, incluso en características antiguas del lenguaje.
Por ejemplo, en apps de Ruby y Python es muy común que en alguna dependencia haya una extensión nativa en C, y según entiendo, las principales implementaciones alternativas nunca han logrado soportar eso. Lo han intentado, pero por razones técnicas evidentes no ha funcionado bien, y la alternativa de esperar que las librerías ofrezcan múltiples implementaciones tampoco ha tenido una historia muy exitosa.
Si además sumas que estos lenguajes suelen usarse en sitios web CRUD donde la E/S pesa más que la CPU como factor de rendimiento, el atractivo de una alternativa más rápida se reduce mucho.
Es un texto realmente excelente. La sociología de la tecnología es muy interesante.
Quienes mantienen implementaciones de lenguajes quieren la máxima flexibilidad al diseñar y desplegar nuevas funciones que beneficien a los usuarios. No quieren quedar atados a tener que conseguir consenso entre múltiples implementaciones antes de lanzar una función. Como ejemplo, basta ver lo glacial que se sintió durante años la evolución de JavaScript, y lo relativamente rápido que ha evolucionado TypeScript.
Al mismo tiempo, las implementaciones alternativas también pueden ser una señal de que el ecosistema del lenguaje es sólido, así que tienen ventajas. Si una implementación alternativa es realmente buena, aunque solo beneficie a usuarios con necesidades de nicho, puede aportar valor real al ecosistema.
Por eso, si fueras diseñador o mantenedor de un lenguaje, probablemente no serías activamente hostil a las implementaciones alternativas, pero sí tienen desventajas. En general, el feedback de los usuarios va a ir en la dirección de lanzar nuevas funciones y hacer avanzar el lenguaje. No habrá muchas peticiones para frenar el ritmo y dar tiempo a que PyPy, IronRuby o LuaJIT se pongan al día.
Cuando quienes consumen un lenguaje eligen sobre qué implementación construir, su prioridad principal suele ser la seguridad y la estabilidad. Nadie quiere que un codebase de un millón de líneas termine dependiendo de características sutiles de comportamiento de una implementación alternativa que alguna vez hizo un estudiante de doctorado muy brillante, pero que ahora ya se fue a otro proyecto. Entonces los usuarios se concentran en la implementación más usada, y ese hecho a su vez atrae a más usuarios, creando un fuerte bucle de retroalimentación positiva.
Como resultado, salvo que haya una fuerza fuerte empujando en la dirección contraria, la mayoría de los lenguajes convergen hacia una sola implementación de referencia. Incluso se puede argumentar que eso es algo bueno. Casi todo el esfuerzo de ingeniería invertido en la implementación del lenguaje beneficia a todos los usuarios en vez de dividirse entre varias implementaciones. Claro, la desventaja es que la implementación puede quedar atrapada en un óptimo local.
Se mencionó LuaJIT, pero también es un caso que muestra que no siempre se cumple la conclusión del texto. Mucha gente y muchos proyectos eligieron LuaJIT intencionalmente en lugar de Lua.
luajittextambién se consigue fácilmente junto aluatex.Puede que sea una idea menos popular, pero la gente debería revisar de vez en cuando su ego, incluyéndose a sí misma. Puede ser más fácil crear un proyecto paralelo que sea “mío” que contribuir a un proyecto open source existente, pero hay que preguntarse para quién se está haciendo
si es para quien mantiene el proyecto, para el proyecto en sí, para los usuarios o para el propio ego. Si el último punto te molesta, probablemente sea porque te toca de cerca
Agregar un JIT a un lenguaje existente es un trabajo grande, así que el nivel que exigirá la implementación de referencia también será alto. Aun así, creo que el objetivo debería ser ese. Hacer un fork o crear algo nuevo también da la libertad de hacer cosas grandes, pero a menudo debería verse como algo temporal
Si el objetivo es mostrar lo que yo puedo hacer, probablemente no llegará muy lejos. Si el objetivo es crear algo mejor, entonces se aprende a trabajar dentro de las limitaciones de otras personas
Creo que este artículo muestra cómo Python, Lua y Ruby adoptaron ese enfoque y decepcionaron a mucha gente. Como resultado, miles de desarrolladores y millones de usuarios terminan soportando un desarrollo y un software más lentos. No porque sea imposible, sino porque administrativamente no hay incentivos para hacerlo de otra manera