Compilation JIT
Catnip utilise un compilateur Just-In-Time (JIT) pour optimiser automatiquement le code fréquemment exécuté.
Pourquoi un JIT
Le JIT résout trois limitations majeures de l'interprétation :
Performance : Code natif 100-200x plus rapide que l'interpréteur VM pour les boucles numériques
Stack overflow : Élimine les limites de profondeur pour les fonctions récursives compilées
Transparence : Activation automatique sans intervention utilisateur (détection à chaud)
Architecture : Trace-based JIT
Catnip utilise une approche trace-based plutôt que method-based :
- On enregistre l'exécution réelle d'une boucle ou fonction (trace)
- On compile cette trace linéaire en code natif
- Les branches rarement prises sont ignorées (guards + deoptimization)
Cette approche simplifie la compilation et optimise les chemins chauds réels plutôt que tous les chemins possibles.
La trace JIT regarde le réel, puis lui colle un circuit rapide en natif. Si ça part en freestyle, retour VM sans drame. On bétonne les trajets du quotidien, on tague le reste.
Références académiques
Trace compilation : Gal et al. 2009 - "Trace-based Just-in-Time Type Specialization for Dynamic Languages" (ACM PLDI)
Deoptimization : Hölzle et al. 1992 - "Debugging Optimized Code with Dynamic Deoptimization" (ACM PLDI)
Backend : Cranelift
Catnip utilise Cranelift comme backend de compilation :
- Bibliothèque Rust pour génération de code machine (x86-64, ARM64, etc.)
- Temps de compilation rapide (adapté au JIT)
- Utilisé par Wasmtime, SpiderMonkey, autres projets production
- Alternative moderne à LLVM pour cas d'usage JIT
Pourquoi Cranelift :
- Intégration Rust native (pas de FFI)
- Compile en ~100µs (vs millisecondes pour LLVM)
- API sûre (pas d'UB possible en Rust safe)
- Maintenance active (Bytecode Alliance)
Détection de code chaud
Le JIT surveille deux types de "hot paths" :
Loops (boucles)
Seuil : 100 itérations du même loop body
# Devient hot après 100 itérations
for i in range(10000) {
sum = sum + i
}
# Itération 1-100 : interpréteur + profiling
# Itération 100 : compilation trace
# Itération 101+ : code natif
Functions (fonctions)
Seuil : 100 appels de la même fonction
fib = (n) => {
if n < 2 { n } else { fib(n-1) + fib(n-2) }
}
fib(30)
# Appels 1-100 : interpréteur + profiling
# Appel 100 : compilation trace (avec CallSelf natif)
# Appel 101+ : code natif récursif
Identification : Fonction identifiée par hash stable du bytecode + nom + nombre d'arguments
Optimisations supportées
Le JIT applique automatiquement :
Spécialisation de types : Génère code natif pour int/float détectés dans la trace
Élimination d'overhead : Pas de boxing/unboxing, dispatch direct
Inline de fonctions pures : Petites fonctions pures (\<20 opcodes) inlinées automatiquement
Récursion native : Appels récursifs compilés en CALL x86-64 natif (avec protection overflow)
Activation et contrôle
Défaut : JIT activé automatiquement en mode VM
Désactiver :
catnip -o jit:off script.cat
Pragma :
pragma("jit", False) # Désactive JIT pour ce fichier
Variables d'environnement :
CATNIP_OPTIMIZE=jit:off catnip script.cat
Limitations et fallback
Le JIT ne compile pas tout le code :
Non compilable :
- Appels à fonctions Python externes
- Opérations non supportées (I/O, réflexion)
- Branches froides (rarement exécutées)
Comportement : Fallback transparent vers l'interpréteur VM, aucune erreur
Deoptimization : Si une guard échoue (type change, condition inattendue), retour à l'interpréteur
Performances typiques
| Type de code | Speedup vs VM |
|---|---|
| Boucles arithmétiques (int) | 100-200x |
| Boucles arithmétiques (float) | 50-100x |
| Fonctions récursives simples | 1.1-2x |
| Fonctions avec inline | 1.2-1.4x |
| Code avec beaucoup d'I/O | 1.0x (JIT off) |
Le JIT est câblé pour la baston numérique. Si ton code attend sa promise réseau 90% du temps, il fera pas de miracles. Mais si tu calcules
fib(40)en boucle, là on discute.