En quoi un compilateur JIT est-il différent d'un compilateur ordinaire?
22
Il y a eu beaucoup de battage médiatique sur les compilateurs JIT pour des langages comme Java, Ruby et Python. En quoi les compilateurs JIT sont-ils différents des compilateurs C / C ++, et pourquoi les compilateurs sont-ils écrits pour Java, Ruby ou Python appelés compilateurs JIT, alors que les compilateurs C / C ++ sont simplement appelés compilateurs?
Les compilateurs JIT compilent le code à la volée, juste avant leur exécution ou même lorsqu'ils sont déjà en cours d'exécution. De cette façon, la machine virtuelle sur laquelle le code s'exécute peut rechercher des modèles dans l'exécution du code pour permettre des optimisations qui ne seraient possibles qu'avec des informations d'exécution. De plus, si la machine virtuelle décide que la version compilée n'est pas assez bonne pour quelque raison que ce soit (par exemple, trop de ratés de cache ou de code lançant fréquemment une exception particulière), elle peut décider de la recompiler d'une manière différente, conduisant à une solution beaucoup plus intelligente. compilation.
De l'autre côté, les compilateurs C et C ++ ne sont traditionnellement pas JIT. Ils compilent en une seule fois une seule fois sur la machine du développeur, puis un exécutable est produit.
Existe-t-il des compilateurs JIT qui gardent une trace des erreurs de cache et adaptent leur stratégie de compilation en conséquence? @Victor
Martin Berger
@MartinBerger Je ne sais pas si des compilateurs JIT disponibles le font mais pourquoi pas? À mesure que les ordinateurs deviennent plus puissants et que l'informatique se développe, les gens peuvent appliquer plus d'optimisations au programme. Par exemple, lorsque Java est né, il est très lent, peut-être 20 fois par rapport à ceux compilés, mais maintenant les performances ne sont pas tellement en retard et peuvent être comparables à certains compilateurs
phuclv
J'ai entendu cela sur un article de blog il y a longtemps. Le compilateur peut compiler différemment selon le processeur actuel. Par exemple, il peut vectoriser automatiquement du code s'il détecte que le CPU prend en charge SSE / AVX. Lorsque de nouvelles extensions SIMD sont disponibles, elles peuvent être compilées vers les plus récentes afin que le programmeur n'ait rien à changer. Ou s'il peut organiser les opérations / données pour tirer parti du cache plus grand ou pour réduire les
échecs de
@ LưuVĩnhPhúc Chào! Je suis heureux de le croire, mais la différence est que, par exemple, le type de processeur ou la taille du cache sont quelque chose de statique qui ne change pas tout au long du calcul, donc pourrait facilement être fait par un compilateur statique aussi. Cache manque OTOH sont très dynamiques.
Martin Berger
12
JIT est l'abréviation de compilateur juste à temps, et son nom est misson: pendant l'exécution, il détermine les optimisations de code intéressantes et les applique. Il ne remplace pas les compilateurs habituels mais fait partie des interprètes. Notez que les langages comme Java qui utilisent du code intermédiaire ont les deux : un compilateur normal pour la traduction du code source vers le code intermédiaire, et un JIT inclus dans l'interpréteur pour augmenter les performances.
Les optimisations de code peuvent certainement être effectuées par des compilateurs "classiques", mais notez la principale différence: les compilateurs JIT ont accès aux données lors de l'exécution. C'est un énorme avantage; l'exploiter correctement peut être difficile, évidemment.
Considérez, par exemple, un code comme celui-ci:
m(a : String, b : String, k : Int) {
val c : Int;
switch (k) {
case 0 : { c = 7; break; }
...
case 17 : { c = complicatedMethod(k, a+b); break; }
}
return a.length + b.length - c + 2*k;
}
Un compilateur normal ne peut pas en faire trop. Un compilateur JIT, cependant, peut détecter que l' mon n'appelle jamais avec k==0pour une raison quelconque (des choses comme cela peuvent se produire lorsque le code change avec le temps); il peut ensuite créer une version plus petite du code (et le compiler en code natif, bien que je considère cela comme un point mineur, conceptuellement):
À ce stade, il va probablement même incorporer l'appel de méthode car il est trivial maintenant.
Apparemment, le Soleil a rejeté la plupart des optimisations javacutilisées dans Java 6; On m'a dit que ces optimisations ont rendu difficile pour JIT de faire beaucoup, et le code compilé naïvement a finalement fonctionné plus rapidement. Allez comprendre.
Soit dit en passant, en présence d'effacement de type (par exemple génériques en Java), JIT est en fait désavantagé par rapport aux types.
Raphael
Le runtime est donc plus compliqué que celui d'un langage compilé car l'environnement d'exécution doit collecter des données afin d'optimiser.
saadtaame
2
Dans de nombreux cas, un JIT ne pourrait pas être remplacé mpar une version non vérifiée kcar il ne pourrait pas prouver qu'il nem serait jamais appelé avec un non-zéro k, mais même sans pouvoir prouver qu'il pouvait remplacer avec static miss_count; if (k==0) return a.length+b.length-7; else if (miss_count++ < 16) { ... unoptimized code for m ...} else { ... consider other optimizations...}.
supercat
1
Je suis d'accord avec @supercat et je pense que votre exemple est en fait assez trompeur. Ce n'est que si k=0toujours , c'est-à-dire que ce n'est pas une fonction de l'entrée ou de l'environnement, qu'il est sûr de laisser tomber le test - mais le déterminer nécessite une analyse statique, qui est très coûteuse et donc uniquement abordable au moment de la compilation. Un JIT peut gagner lorsqu'un chemin à travers un bloc de code est utilisé beaucoup plus souvent que d'autres, et une version du code spécialisée pour ce chemin serait beaucoup plus rapide. Mais le JIT émettra toujours un test pour vérifier si le chemin rapide s'applique et prendre le "chemin lent" sinon.
j_random_hacker
Un exemple: une fonction prend un Base* pparamètre et appelle des fonctions virtuelles à travers lui; l'analyse d'exécution montre que l'objet réel pointé vers toujours (ou presque toujours) semble être de Derived1type. Le JIT pourrait produire une nouvelle version de la fonction avec des appels aux Derived1méthodes résolus statiquement (ou même en ligne) . Ce code serait précédé d'une conditionnelle qui vérifie si ple pointeur vtable pointe vers la Derived1table attendue ; sinon, il passe à la version originale de la fonction avec ses appels de méthode résolus dynamiquement plus lentement.
JIT est l'abréviation de compilateur juste à temps, et son nom est misson: pendant l'exécution, il détermine les optimisations de code intéressantes et les applique. Il ne remplace pas les compilateurs habituels mais fait partie des interprètes. Notez que les langages comme Java qui utilisent du code intermédiaire ont les deux : un compilateur normal pour la traduction du code source vers le code intermédiaire, et un JIT inclus dans l'interpréteur pour augmenter les performances.
Les optimisations de code peuvent certainement être effectuées par des compilateurs "classiques", mais notez la principale différence: les compilateurs JIT ont accès aux données lors de l'exécution. C'est un énorme avantage; l'exploiter correctement peut être difficile, évidemment.
Considérez, par exemple, un code comme celui-ci:
Un compilateur normal ne peut pas en faire trop. Un compilateur JIT, cependant, peut détecter que l'
m
on n'appelle jamais aveck==0
pour une raison quelconque (des choses comme cela peuvent se produire lorsque le code change avec le temps); il peut ensuite créer une version plus petite du code (et le compiler en code natif, bien que je considère cela comme un point mineur, conceptuellement):À ce stade, il va probablement même incorporer l'appel de méthode car il est trivial maintenant.
Apparemment, le Soleil a rejeté la plupart des optimisations
javac
utilisées dans Java 6; On m'a dit que ces optimisations ont rendu difficile pour JIT de faire beaucoup, et le code compilé naïvement a finalement fonctionné plus rapidement. Allez comprendre.la source
m
par une version non vérifiéek
car il ne pourrait pas prouver qu'il nem
serait jamais appelé avec un non-zérok
, mais même sans pouvoir prouver qu'il pouvait remplacer avecstatic miss_count; if (k==0) return a.length+b.length-7; else if (miss_count++ < 16) { ... unoptimized code for
m...} else { ... consider other optimizations...}
.k=0
toujours , c'est-à-dire que ce n'est pas une fonction de l'entrée ou de l'environnement, qu'il est sûr de laisser tomber le test - mais le déterminer nécessite une analyse statique, qui est très coûteuse et donc uniquement abordable au moment de la compilation. Un JIT peut gagner lorsqu'un chemin à travers un bloc de code est utilisé beaucoup plus souvent que d'autres, et une version du code spécialisée pour ce chemin serait beaucoup plus rapide. Mais le JIT émettra toujours un test pour vérifier si le chemin rapide s'applique et prendre le "chemin lent" sinon.Base* p
paramètre et appelle des fonctions virtuelles à travers lui; l'analyse d'exécution montre que l'objet réel pointé vers toujours (ou presque toujours) semble être deDerived1
type. Le JIT pourrait produire une nouvelle version de la fonction avec des appels auxDerived1
méthodes résolus statiquement (ou même en ligne) . Ce code serait précédé d'une conditionnelle qui vérifie sip
le pointeur vtable pointe vers laDerived1
table attendue ; sinon, il passe à la version originale de la fonction avec ses appels de méthode résolus dynamiquement plus lentement.