- YJIT y ZJIT son arquitecturas de compiladores JIT en Ruby 3.x que convierten código Ruby a lenguaje máquina para aumentar la velocidad de ejecución
- YJIT cuenta cuántas veces se llama cada función o bloque y, al llegar a cierto umbral, convierte ese código a lenguaje máquina
- El código convertido se guarda en bloques YJIT, y cada bloque transforma varias instrucciones YARV en instrucciones de lenguaje máquina ARM64
- Usa Branch Stubs para observar en tiempo de ejecución los tipos de datos reales y generar selectivamente las instrucciones de lenguaje máquina adecuadas
- Esta estructura es un mecanismo clave para lograr al mismo tiempo mejor rendimiento de ejecución de Ruby y eficiencia al manejar tipos dinámicos
Capítulo 4: Compilar Ruby a lenguaje máquina
Interpreting vs. Compiling Ruby Code
- No hay detalles en el texto original
Counting Method and Block Calls
- YJIT rastrea la cantidad de llamadas a funciones y bloques del programa para identificar código hotspot
- Junto a la secuencia de instrucciones YARV de cada función o bloque, almacena los valores jit_entry y jit_entry_calls
jit_entry es inicialmente null y más adelante guarda el puntero al código máquina generado por YJIT
jit_entry_calls aumenta en 1 cada vez que hay una llamada
- Cuando la cantidad de llamadas alcanza el umbral, YJIT compila ese código a lenguaje máquina
- En Ruby 3.5, el umbral predeterminado es de 30 llamadas en programas pequeños y 120 llamadas en aplicaciones grandes
- Puede modificarse en tiempo de ejecución con la opción
--yjit-call-threshold
- Con este enfoque, YJIT convierte a lenguaje máquina solo el código que se ejecuta con frecuencia para asegurar una ruta de ejecución eficiente
YJIT Blocks
- YJIT guarda las instrucciones de lenguaje máquina que genera en bloques YJIT
- Un bloque YJIT es distinto de un bloque Ruby y corresponde a una sección parcial de instrucciones YARV
- Cada función o bloque Ruby está compuesto por varios bloques YJIT
- En el programa de ejemplo, cuando el bloque se ejecuta por trigésima vez, YJIT empieza a compilar
- Convierte la primera instrucción YARV
getlocal_WC_1 a lenguaje máquina y crea un nuevo bloque YJIT
- Después compila la instrucción
getlocal_WC_0 y la incluye en el mismo bloque
- Según la Figura 4-8, YJIT genera instrucciones ARM64 para cargar valores en los registros x1 y x9 del procesador M1
getlocal_WC_1 guarda en la pila una variable local del frame de pila anterior, y getlocal_WC_0 una variable del frame de pila actual
- Las instrucciones de lenguaje máquina generadas realizan la misma operación
YJIT Branch Stubs
- Cuando YJIT compila la instrucción
opt_plus, surge el problema de no poder conocer el tipo de los operandos
- Según el tipo, como entero, cadena o punto flotante, se requieren distintas instrucciones de lenguaje máquina
- Por ejemplo, la suma de enteros usa la instrucción
adds, mientras que la suma de punto flotante necesita otra instrucción
- Para resolverlo, YJIT usa observación en tiempo de ejecución en lugar de análisis previo
- Mientras el programa corre, verifica los tipos reales de los valores recibidos y genera el código máquina correspondiente
- Para este comportamiento usa Branch Stubs
- Cuando una nueva rama (branch) todavía no tiene un bloque conectado, se enlaza temporalmente a un stub
- Después, cuando se confirma el tipo real, ese stub se reemplaza por el bloque apropiado
ZJIT (solo se menciona)
- El índice incluye una sección relacionada con ZJIT, pero el cuerpo del texto no ofrece una explicación concreta
Resumen
- YJIT es un compilador JIT para Ruby 3.5 orientado a mejorar la eficiencia de ejecución de un lenguaje de tipos dinámicos
- Sus puntos clave son el disparador de compilación basado en cantidad de llamadas, la estructura de bloques YJIT y la verificación de tipos en tiempo de ejecución mediante Branch Stubs
- En la arquitectura ARM64, convierte el código a instrucciones reales de lenguaje máquina para mejorar la velocidad de ejecución del código Ruby
- ZJIT se menciona como un JIT de próxima generación, pero el texto no entra en detalles
1 comentarios
Comentarios de Hacker News
Hace tiempo existió MacRuby, que usaba LLVM para compilar a código nativo en macOS e integrarse con frameworks de Objective‑C
Era una idea bastante genial, pero al final parece que Apple cambió de rumbo hacia Swift
Si sale una nueva versión, pienso comprar y leer sí o sí Ruby Under a Microscope. Ruby todavía me gusta mucho, aunque no he tenido muchas oportunidades de usarlo en la práctica
Ahora otras personas lo continúan, pero da la impresión de que hoy están más enfocados en DragonRuby (una implementación de Ruby centrada en juegos)
Como referencia, también está este artículo de Wikipedia
Aunque puede que algunas APIs antiguas ya no tengan soporte
VB6 era realmente rapidísimo para desarrollar, y hasta permitía trabajar con Direct3D y ASP Classic
La elegancia y facilidad de desarrollo de Ruby me recuerdan esa época
Si Ruby hubiera tenido herramientas GUI al nivel de VB6, creo que su popularidad habría sido bastante distinta
Da mucho gusto ver que Pat sigue adelante con el proyecto
Su primer libro Ruby Under a Microscope y sus publicaciones del blog fueron una gran inspiración para mí
Incluso llegué a conocerlo en persona en la conferencia Euruko, y de verdad era una persona excelente
La primera vez que leí Ruby Under a Microscope me divertí muchísimo
Gracias a eso incluso lo aproveché antes para resolver retos de CTF
Últimamente no he seguido la implementación interna de Ruby, pero si sale una nueva versión, definitivamente la voy a comprar
Al ver este artículo, me dieron ganas de volver a leer la nueva edición del libro
Ya que salió el tema de compilar Ruby, me pregunto si alguien ha probado el compilador Sorbet hecho por desarrolladores de Stripe
Publicación sobre la liberación de Sorbet Compiler
La compilación AOT es realmente difícil en Ruby
Lo interesante del enfoque de Sorbet es que puede crear rutas rápidas basadas en la verificación de tipos de Ruby
Yo también estoy haciendo un compilador de Ruby como proyecto personal, y estoy tomando como referencia hokstad.com/compiler y
writing-a-compiler-in-ruby
Por ahora estoy concentrado en pasar RubySpec, y más adelante quiero probar optimizaciones basadas en tipos
No está directamente relacionado con compilar Ruby, pero el libro Enterprise Integration with Ruby me dio mucha perspectiva sobre cómo usar Ruby fuera del mundo web
Desde que conocí MRuby, me he enganchado a la diversión de convertir mis proyectos y scripts en ejecutables independientes
Me alegra que Ruby Under a Microscope siga actualizándose
Creo que es una lectura obligatoria para cualquiera que quiera entender cómo funciona Ruby por dentro
Siempre tuve curiosidad sobre cómo YJIT rastrea la compilación por tipo de entrada cuando un bloque se ejecuta varias veces
Quería entender cómo Ruby maneja distintos tipos como int o float
Usa un enfoque de “esperar y ver” que retrasa la compilación hasta que se proporcionan tipos reales
Mantiene versiones separadas del bloque para cada tipo y llama la adecuada según la situación
A este algoritmo se le llama Basic Block Versioning
Maxime Chevalier‑Boisvert de Shopify lo explica muy bien en su charla de RubyConf 2021
El nuevo motor JIT, ZJIT, parece usar un enfoque diferente
Hacer que un lenguaje de tipado dinámico sea rápido con JIT normalmente tiene como costo un mayor uso de memoria
Si no eres una empresa grande como Shopify, eso podría ser un problema todavía mayor
Hoy en día, las instancias en la nube suelen dar alrededor de 4GiB de memoria por núcleo, así que unos cientos de MB de código JIT son perfectamente manejables
Me pareció simple que YJIT encuentre hotspots contando solo cuántas veces se llama una función
Me preguntaba si no tendría algo como los JIT de JavaScript, que detectan operaciones pesadas dentro de los bucles
La estructura de bloques de Ruby quizá podría ayudar con ese tipo de optimización
el JIT puede manejar esos bloques como si fueran funciones separadas y optimizar naturalmente los bucles repetitivos
Ese tema se va a tratar con más profundidad en el siguiente capítulo