Vue d'ensemble
Un interprète pour la langue X est un programme (ou une machine, ou tout simplement une sorte de mécanisme en général) qui exécute un programme p écrit en langage X tel qu'il exécute les effets et évalue les résultats prescrits par le cahier des charges de X . Les processeurs sont généralement des interprètes pour leurs jeux d'instructions respectifs, bien que les processeurs des stations de travail hautes performances modernes soient en réalité plus complexes que cela; ils peuvent en réalité avoir un jeu d'instructions privées propriétaires sous-jacent et soit traduire (compiler), soit interpréter le jeu d'instructions publiques visibles de l'extérieur.
Un compilateur de X à Y est un programme (ou une machine, ou simplement un mécanisme en général) qui traduit tout programme p d’une langue X en un programme sémantiquement équivalent p ′ dans une langue Y de telle manière que la sémantique du programme sont conservés, à savoir que l' interprétation de p ' avec un interprète pour Y donnera les mêmes résultats et ont les mêmes effets que l' interprétation de p avec un interprète pour X . (Notez que X et Y peuvent être la même langue.)
Les termes AOT (Ahead) et Just-in-Time (JIT) désignent le moment où la compilation a lieu: le "temps" mentionné dans ces termes est "runtime", c'est-à-dire qu'un compilateur JIT compile le programme tel quel. En cours d’exécution , un compilateur AOT compile le programme avant son exécution . Notez que cela nécessite qu’un compilateur JIT du langage X au langage Y doit en quelque sorte travailler avec un interprète pour le langage Y, sinon il n’y aurait aucun moyen d’exécuter le programme. (Ainsi, par exemple, un compilateur JIT qui compile JavaScript en code machine x86 n’a aucun sens sans un processeur x86; il compile le programme en cours d’exécution, mais sans le processeur x86, le programme ne serait pas exécuté.)
Notez que cette distinction n’a aucun sens pour les interprètes: un interprète exécute le programme. L'idée d'un interpréteur AOT qui exécute un programme avant son exécution ou d'un interpréteur JIT qui exécute un programme en cours d'exécution est absurde.
Nous avons donc:
- Compilateur AOT: compile avant d'exécuter
- Compilateur JIT: compile en cours d'exécution
- interprète: pistes
Compilateurs JIT
Au sein de la famille des compilateurs JIT, il existe encore de nombreuses différences quant au moment exact où ils compilent, à quelle fréquence et à quelle granularité.
Le compilateur JIT dans le CLR de Microsoft, par exemple, compile le code une seule fois (lorsqu'il est chargé) et compile un assemblage entier à la fois. D'autres compilateurs peuvent collecter des informations pendant l'exécution du programme et recompiler le code plusieurs fois à mesure que de nouvelles informations deviennent disponibles, ce qui leur permet de mieux l'optimiser. Certains compilateurs JIT sont même capables de désoptimiser le code. Maintenant, vous pourriez vous demander pourquoi on voudrait jamais faire ça? La désoptimisation vous permet d’effectuer des optimisations très agressives qui risquent d’être dangereuses: si vous deveniez trop agressif, vous pouvez tout simplement revenir en arrière, alors qu’avec un compilateur JIT qui ne peut pas désoptimiser, vous ne pourriez plus exécuter le optimisations agressives en premier lieu.
Les compilateurs JIT peuvent soit compiler une unité statique de code en une fois (un module, une classe, une fonction, une méthode,…; ils sont généralement appelés JIT méthode à la fois , par exemple) ou tracer la dynamique exécution de code pour trouver des traces dynamiques (généralement des boucles) qu’ils compileront ensuite (ils sont appelés JIT de traçage ).
Combinaison d'interprètes et de compilateurs
Les interprètes et les compilateurs peuvent être combinés dans un seul moteur d’exécution linguistique. Il existe deux scénarios typiques où cela est fait.
La combinaison d' un compilateur AOT de X à Y avec un interprète pour Y . Ici, typiquement, X est un langage de niveau supérieur optimisé pour la lisibilité par les humains, alors que Yest un langage compact (souvent une sorte de bytecode) optimisé pour être interprété par des machines. Par exemple, le moteur d'exécution CPython Python dispose d'un compilateur AOT qui compile le code source Python en bytecode CPython et d'un interpréteur interprétant le bytecode CPython. De même, le moteur d'exécution YARV Ruby dispose d'un compilateur AOT qui compile le code source de Ruby en bytecode YARV et d'un interpréteur interprétant le bytecode YARV. Pourquoi voudriez-vous faire ça? Ruby et Python sont à la fois très haut niveau et des langues peu complexes, donc nous avons d' abord les compiler dans une langue qui est plus facile à analyser et plus facile à interpréter, et interprètent cette langue.
L'autre façon de combiner un interpréteur et un compilateur est un moteur d'exécution en mode mixte . Ici, nous « modes » « mix » deux de la mise en œuvre de la même langue ensemble, à savoir un interprète pour X et un compilateur JIT de X à Y . (Donc, la différence ici est que dans le cas ci-dessus, nous avons eu plusieurs "étapes" avec le compilateur compilant le programme puis transmettant le résultat à l'interpréteur, ici nous avons les deux travaillant côte à côte sur le même langage. ) Le code compilé par un compilateur a tendance à être plus rapide que le code exécuté par un interpréteur, mais sa compilation prend du temps (et particulièrement si vous souhaitez optimiser considérablement le code à exécuter).très vite, cela prend beaucoup de temps). Donc, pour combler le temps où le compilateur JIT est occupé à compiler le code, l'interpréteur peut déjà commencer à exécuter le code, et une fois que le JIT est terminé, nous pouvons basculer l'exécution vers le code compilé. Cela signifie que nous obtenons à la fois les meilleures performances possibles du code compilé, mais nous n’avons pas besoin d’attendre la fin de la compilation, et notre application commence à s’exécuter immédiatement (bien que pas aussi vite qu’il pourrait être).
Il s’agit en fait de l’application la plus simple possible d’un moteur d’exécution en mode mixte. Des possibilités plus intéressantes sont, par exemple, de ne pas commencer à compiler tout de suite, mais de laisser l'interpréteur s'exécuter pendant un moment, et collecter des statistiques, des informations de profil, des informations de type, des informations sur la probabilité de création de branches conditionnelles spécifiques, ainsi que de méthodes appelées. le plus souvent, etc., puis transmettez ces informations dynamiques au compilateur afin qu'il puisse générer un code plus optimisé. C'est également une façon de mettre en œuvre la désoptimisation dont j'ai parlé plus haut: s'il s'avère que vous avez été trop agressif dans l'optimisation, vous pouvez jeter (une partie du) code et revenir à l'interprétation. C'est ce que fait la machine virtuelle HotSpot, par exemple. Il contient à la fois un interpréteur pour le pseudo-code JVM et un compilateur pour le pseudo-code JVM. (En réalité,deux compilateurs!)
Il est également possible et en fait commun à combiner ces deux approches: deux phases dont la première est un compilateur qui compile AOT X à Y et la seconde phase étant un moteur en mode mixte qui interprète la fois Y et compile Y à Z . Le moteur d’exécution Rubinius Ruby fonctionne de cette manière, par exemple: il a un compilateur AOT qui compile le code source Ruby en bytecode Rubinius et un moteur en mode mixte qui interprète d’abord le bytecode Rubinius et une fois quelques informations rassemblées, compile les méthodes les plus souvent appelées en natif. langage machine.
Notez que le rôle joué par l'interprète dans le cas d'un moteur d'exécution en mode mixte, à savoir fournir un démarrage rapide, potentiellement collecter des informations et fournir une capacité de repli, peut également être joué par un deuxième compilateur JIT. Voici comment fonctionne V8, par exemple. V8 n'interprète jamais, il compile toujours. Le premier compilateur est un compilateur très rapide et très fin qui démarre très rapidement. Le code qu'il produit n'est pas très rapide, cependant. Ce compilateur injecte également du code de profilage dans le code qu'il génère. L'autre compilateur est plus lent et utilise plus de mémoire, mais produit du code beaucoup plus rapidement et peut utiliser les informations de profilage collectées en exécutant le code compilé par le premier compilateur.