- YJIT et ZJIT sont des architectures de compilateurs JIT dans Ruby 3.x qui convertissent le code Ruby en langage machine pour améliorer la vitesse d’exécution
- YJIT compte le nombre d’appels de chaque fonction ou bloc et, lorsqu’un certain seuil est atteint, convertit ce code en langage machine
- Le code converti est stocké dans des blocs YJIT, et chaque bloc convertit plusieurs instructions YARV en instructions machine ARM64 correspondantes
- YJIT utilise des branch stubs pour observer les types de données réels à l’exécution et générer sélectivement les instructions machine appropriées
- Cette architecture constitue un mécanisme clé pour améliorer à la fois les performances d’exécution de Ruby et l’efficacité du traitement des types dynamiques
Chapitre 4 : Compiler Ruby en langage machine
Interpreting vs. Compiling Ruby Code
- Le texte original ne contient pas de détails supplémentaires
Counting Method and Block Calls
- YJIT suit le nombre d’appels de fonctions et de blocs du programme afin d’identifier le code hotspot
- Les valeurs jit_entry et jit_entry_calls sont stockées à côté de la séquence d’instructions YARV de chaque fonction ou bloc
jit_entry vaut initialement null, puis stocke plus tard un pointeur vers le code machine généré par YJIT
jit_entry_calls augmente de 1 à chaque appel
- Lorsque le nombre d’appels atteint le seuil, YJIT compile ce code en langage machine
- Dans Ruby 3.5, le seuil par défaut est de 30 appels pour les petits programmes et 120 appels pour les grandes applications
- Il peut être modifié à l’exécution avec l’option
--yjit-call-threshold
- De cette manière, YJIT ne convertit en langage machine que le code fréquemment exécuté, ce qui assure un chemin d’exécution efficace
Blocs YJIT
- YJIT stocke les instructions machine qu’il génère dans des blocs YJIT
- Un bloc YJIT est différent d’un bloc Ruby et correspond à une portion de la séquence d’instructions YARV
- Chaque fonction ou bloc Ruby est composé de plusieurs blocs YJIT
- Dans le programme d’exemple, YJIT commence la compilation lorsque le bloc est exécuté pour la 30e fois
- Il convertit la première instruction YARV
getlocal_WC_1 en langage machine et crée un nouveau bloc YJIT
- Il compile ensuite l’instruction
getlocal_WC_0, qui est incluse dans le même bloc
- D’après la figure 4-8, YJIT génère des instructions ARM64 pour charger des valeurs dans les registres x1 et x9 du processeur M1
getlocal_WC_1 stocke sur la pile une variable locale de la frame de pile précédente, et getlocal_WC_0 une variable de la pile courante
- Les instructions machine générées réalisent le même comportement
Branch Stubs de YJIT
- Lorsque YJIT compile l’instruction
opt_plus, il rencontre le problème de l’inconnaissance du type des opérandes
- Selon le type — entier, chaîne, nombre à virgule flottante, etc. — les instructions machine nécessaires diffèrent
- Par exemple, une addition d’entiers utilise l’instruction
adds, tandis qu’une addition en virgule flottante nécessite d’autres instructions
- Pour résoudre cela, YJIT utilise une approche d’observation à l’exécution plutôt qu’une analyse préalable
- Pendant l’exécution du programme, il vérifie le type réel des valeurs transmises et génère le code machine adapté
- Ce comportement s’appuie sur des branch stubs
- Lorsqu’une nouvelle branche n’a pas encore de bloc connecté, elle est provisoirement reliée à un stub
- Une fois le type réel identifié, ce stub est remplacé par le bloc approprié
ZJIT (simplement mentionné)
- La table des matières inclut une section consacrée à ZJIT, mais le corps du texte ne fournit pas d’explication détaillée
Résumé
- YJIT est, dans Ruby 3.5, un compilateur JIT destiné à améliorer l’efficacité d’exécution d’un langage à typage dynamique
- Les éléments clés sont le déclenchement de la compilation basé sur le nombre d’appels, la structure en blocs YJIT et la vérification des types à l’exécution via des branch stubs
- Sur l’architecture ARM64, il convertit le code en véritables instructions machine pour accélérer l’exécution du code Ruby
- ZJIT est mentionné comme un JIT de nouvelle génération, mais le texte ne donne pas plus de détails
1 commentaires
Commentaire sur Hacker News
Il fut un temps où MacRuby compilait en code natif sur macOS via LLVM et s’intégrait aux frameworks Objective‑C
C’était une idée assez brillante, mais Apple semble finalement avoir bifurqué vers Swift
Si une nouvelle édition sort, je compte absolument acheter et lire Ruby Under a Microscope. J’aime toujours Ruby, mais j’ai eu peu d’occasions de l’utiliser en pratique
Aujourd’hui, d’autres ont repris le flambeau, mais l’attention semble davantage portée sur DragonRuby (une implémentation de Ruby orientée jeu)
À noter qu’il existe aussi un article Wikipédia
En revanche, certaines anciennes API ne sont peut-être plus prises en charge
VB6 permettait vraiment de développer très vite, et on pouvait aussi bien faire du Direct3D que de l’ASP Classic
L’élégance et la facilité de développement de Ruby me rappellent cette époque
Si Ruby avait eu des outils GUI du niveau de VB6, sa popularité aurait sans doute été assez différente
Ça fait vraiment plaisir de voir Pat continuer à faire avancer le projet
Son premier livre Ruby Under a Microscope et ses billets de blog m’ont beaucoup inspiré
Je l’avais même rencontré autrefois à la conférence Euruko, et c’était vraiment quelqu’un de formidable
J’avais vraiment pris plaisir à lire Ruby Under a Microscope la première fois
Cela m’a même servi autrefois pour la résolution de problèmes de CTF
Je n’ai pas suivi l’implémentation interne de Ruby ces derniers temps, mais s’il y a une nouvelle édition, je compte bien l’acheter
Cet article m’a donné envie de relire cette nouvelle édition
Puisqu’on parle de compilation Ruby, je me demande si quelqu’un a déjà essayé le compilateur Sorbet, créé par des développeurs de Stripe
Annonce de l’open source du compilateur Sorbet
La compilation AOT est vraiment difficile avec Ruby
L’approche de Sorbet est intéressante parce qu’elle permet de créer des chemins rapides à partir du typage de Ruby
Je développe moi aussi un compilateur Ruby comme projet personnel, en m’appuyant sur hokstad.com/compiler et
writing-a-compiler-in-ruby
Pour l’instant, je me concentre sur le passage de RubySpec, et plus tard j’aimerais aussi tenter des optimisations fondées sur les types
Ce n’est pas directement lié à la compilation de Ruby, mais le livre Enterprise Integration with Ruby m’a apporté beaucoup de perspectives sur l’usage de Ruby en dehors du web
Depuis que j’ai découvert MRuby, je prends beaucoup de plaisir à transformer mes projets et scripts en exécutables autonomes
Je suis heureux de voir que Ruby Under a Microscope continue d’être mis à jour
À mon avis, c’est une lecture incontournable pour quiconque veut comprendre le fonctionnement interne de Ruby
Je me demandais comment, lorsqu’un bloc YJIT est exécuté plusieurs fois, le suivi de la compilation se fait selon les types d’entrée
J’aimerais comprendre comment Ruby gère différents types comme int ou float
Il utilise une approche « wait‑and‑see » qui retarde la compilation jusqu’à ce que les types réels soient connus
Il conserve une version distincte du bloc pour chaque type, puis appelle celle qui convient selon le contexte
Cet algorithme s’appelle Basic Block Versioning
Maxime Chevalier‑Boisvert de Shopify l’explique très bien dans sa présentation RubyConf 2021
Le nouveau moteur JIT, ZJIT, semble employer une autre approche
Rendre rapide un langage à typage dynamique avec du JIT se paie généralement par une augmentation de l’utilisation mémoire
En dehors d’une grande entreprise comme Shopify, cela peut être un problème encore plus important
Aujourd’hui, les instances cloud offrent souvent autour de 4 GiB de mémoire par cœur, donc quelques centaines de Mo de code JIT restent tout à fait gérables
La manière dont YJIT repère les hotspots en comptant simplement les appels de fonctions m’a semblé assez rudimentaire
Je me demandais s’il n’existait pas, comme dans les JIT JavaScript, un mécanisme pour détecter les calculs lourds à l’intérieur des boucles
La structure en blocs de Ruby pourrait peut-être aider à ce genre d’optimisation
Le JIT peut donc traiter ces blocs comme des fonctions séparées et optimiser naturellement les itérations
Ce point sera abordé plus en profondeur dans le chapitre suivant