web-dev-qa-db-fra.com

Comprendre les différences: interprète traditionnel, compilateur JIT, interprète JIT et compilateur AOT

J'essaie de comprendre les différences entre un interprète traditionnel, un compilateur JIT, un interprète JIT et un compilateur AOT.

Un interprète n'est qu'une machine (virtuelle ou physique) qui exécute des instructions dans un langage informatique. En ce sens, la JVM est un interprète et les CPU physiques sont des interprètes.

La compilation en amont signifie simplement de compiler le code dans un langage avant de l'exécuter (l'interpréter).

Cependant, je ne suis pas sûr des définitions exactes d'un compilateur JIT et d'un interprète JIT.

Selon une définition que j'ai lue, la compilation JIT consiste simplement à compiler le code juste avant de l'interpréter.

Donc, fondamentalement, la compilation JIT est une compilation AOT, faite juste avant l'exécution (interprétation)?

Et un interpréteur JIT, est un programme qui contient à la fois un compilateur JIT et un interprète, et compile du code (JITs) juste avant de l'interpréter?

Veuillez clarifier les différences.

135
Aviv Cohn

Aperçu

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 n'importe quel programme p écrit en langage X de telle sorte qu'il effectue les effets et évalue les résultats comme prescrit par la spécification de X . Les processeurs sont généralement des interprètes pour leurs jeux d'instructions respectifs, bien que les processeurs de poste de travail hautes performances modernes soient en réalité plus complexes que cela; ils peuvent en fait avoir un jeu d'instructions privé sous-jacent et traduire (compiler) ou interpréter le jeu d'instructions public visible de l'extérieur.

A compiler from X to Y is a program (or a machine, or just some kind of mechanism in general) that translates any program p from some language X into a semantically equivalent program p′ in some language Y in such a way that the semantics of the program are preserved, i.e. that interpreting p′ with an interpreter for Y will yield the same results and have the same effects as interpreting p with an interpreter for X. (Note that X and Y may be the same language.)

Les termes Ahead-of-Time (AOT) et Just-in-Time (JIT) fait référence à lorsque la compilation a lieu: le "temps" auquel il est fait référence dans ces termes est "runtime", c'est-à-dire qu'un compilateur JIT compile pendant son exécution , un compilateur AOT compile le programme avant son exécution . Notez que cela nécessite qu'un compilateur JIT de la langue X vers la langue Y doit en quelque sorte travailler avec un interprète pour la langue 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 pas de sens sans CPU x86; il compile le programme pendant qu'il est en cours d'exécution, mais sans CPU x86, le programme ne serait pas exécuté.)

Notez que cette distinction n'a pas de sens pour les interprètes: un interprète exécute le programme. L'idée d'un interprète AOT qui exécute un programme avant son exécution ou d'un interpréteur JIT qui exécute un programme pendant son exécution est absurde.

Donc nous avons:

  • Compilateur AOT: compile avant l'exécution
  • Compilateur JIT: compile pendant l'exécution
  • interprète: exécute

Compilateurs JIT

Au sein de la famille des compilateurs JIT, il existe encore de nombreuses différences quant au moment exact de compilation, à quelle fréquence , et à quelle granularité.

Le compilateur JIT dans le CLR de Microsoft, par exemple, ne compile le code qu'une seule fois (lorsqu'il est chargé) et compile un assembly entier à la fois. D'autres compilateurs peuvent recueillir 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 pourraient être dangereuses: s'il s'avère que vous étiez trop agressif, vous pouvez simplement revenir en arrière, alors que , avec un compilateur JIT qui ne peut pas désoptimiser, vous n'auriez pas pu exécuter les optimisations agressives en premier lieu.

Les compilateurs JIT peuvent soit compiler une unité statique de code en une seule fois (un module, une classe, une fonction, une méthode,…; ceux-ci sont généralement appelés méthode à la fois JIT, par exemple) ou ils peuvent tracer l'exécution dynamique du code pour trouver dynamique trace (généralement des boucles) qu'ils compileront ensuite (ceux-ci sont appelés traçage JIT).

Combiner interprètes et compilateurs

Les interprètes et les compilateurs peuvent être combinés en un seul moteur d'exécution de langage. Il y a deux scénarios typiques où cela est fait.

Combiner un compilateur AOT de X à Y avec un interpréteur pour Y . Ici, généralement X est un langage de niveau supérieur optimisé pour la lisibilité par les humains, tandis que Y est un langage compact (souvent une sorte de bytecode) optimisé pour l'interprétabilité par les machines. Par exemple, le moteur d'exécution CPython Python possède un compilateur AOT qui compile Python code source en code byte CPython et un interpréteur qui interprète le bytecode CPython. De même, le YARV = Ruby moteur d'exécution a un compilateur AOT qui compile Ruby code source en bytecode YARV et un interpréteur qui interprète le bytecode YARV. Pourquoi voudriez-vous faire cela? Ruby et Python sont à la fois des langages de très haut niveau et quelque peu complexes, nous les compilons donc d'abord dans un langage plus facile à analyser et à interpréter, puis à interpréter 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 "mélangeons" deux "modes" d'implémentation du même langage, c'est-à-dire 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 et ensuite alimentant le résultat dans l'interpréteur, ici nous avons les deux côtés de travail -by-side sur le même langage.) Le code qui a été compilé par un compilateur a tendance à s'exécuter plus rapidement que le code exécuté par un interpréteur, mais en réalité, la compilation du code prend du temps (et en particulier, si vous voulez optimiser fortement le code pour qu'il s'exécute vraiment rapidement, il faut beaucoup de temps). Donc, pour combler cette fois où le compilateur JIT est occupé à compiler le code, l'interpréteur peut déjà commencer à exécuter le code, et une fois la compilation JIT terminée, nous pouvons basculer l'exécution sur le code compilé. Cela signifie que nous obtenons à la fois les meilleures performances possibles du code compilé, mais nous n'avons pas à attendre la fin de la compilation, et notre application démarre immédiatement (bien que pas aussi vite que possible).

Il s'agit en fait de l'application la plus simple possible d'un moteur d'exécution en mode mixte. Les possibilités les plus intéressantes sont, par exemple, de ne pas commencer à compiler tout de suite, mais de laisser l'interprète s'exécuter un peu, et de collecter des statistiques, des informations de profilage, des informations de type, des informations sur la probabilité de prendre des branches conditionnelles spécifiques, quelles méthodes sont appelées le plus souvent, etc., puis transmettez ces informations dynamiques au compilateur afin qu'il puisse générer du code plus optimisé. C'est également un moyen de mettre en œuvre la désoptimisation dont j'ai parlé ci-dessus: s'il s'avère que vous avez été trop agressif dans l'optimisation, vous pouvez jeter (une partie de) le code et revenir à l'interprétation. La machine virtuelle Java HotSpot le fait, par exemple. Il contient à la fois un interpréteur pour le bytecode JVM ainsi qu'un compilateur pour le bytecode JVM. (En fait, il contient en fait deux compilateurs!)

Il est également possible et en fait courant de combiner ces deux approches: deux phases, la première étant un compilateur AOT qui compile X to Y et la deuxième phase étant un moteur en mode mixte qui interprète Y et compile Y à Z . Le moteur d'exécution Rubinius Ruby fonctionne de cette façon, par exemple: il a un compilateur AOT qui compile Ruby sourcecode en Rubinius bytecode et un moteur en mode mixte qui le premier) interprète le bytecode Rubinius et une fois qu'il a rassemblé certaines informations compile les méthodes les plus souvent appelées en code machine natif.

Notez que le rôle que joue l'interpréteur dans le cas d'un moteur d'exécution en mode mixte, à savoir fournir un démarrage rapide, et également potentiellement collecter des informations et fournir une capacité de secours, peut également être joué par un deuxième compilateur JIT. C'est ainsi que fonctionne V8, par exemple. La V8 n'interprète jamais, elle compile toujours. Le premier compilateur est un compilateur très rapide et très mince qui démarre très rapidement. Le code qu'il produit n'est cependant pas très rapide. 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 un code beaucoup plus rapide et il peut utiliser les informations de profilage collectées en exécutant le code compilé par le premier compilateur.

204
Jörg W Mittag