Gardez à l'esprit que ce qui suit ne fait que comparer la différence entre la compilation native et la compilation JIT, et ne couvre pas les spécificités d'un langage ou d'un framework particulier. Il peut y avoir des raisons légitimes de choisir une plate-forme particulière au-delà.
Lorsque nous affirmons que le code natif est plus rapide, nous parlons du cas d'utilisation typique du code natif compilé par rapport au code compilé JIT, où l'utilisation typique d'une application compilée JIT doit être exécutée par l'utilisateur, avec des résultats immédiats (par exemple, en attente sur le compilateur en premier). Dans ce cas, je ne pense pas que quiconque puisse prétendre de manière franche que le code compilé par JIT puisse correspondre ou battre le code natif.
Supposons que nous ayons un programme écrit dans un langage X et que nous puissions le compiler avec un compilateur natif, et encore avec un compilateur JIT. Chaque flux de travail comporte les mêmes étapes, qui peuvent être généralisées comme suit: (Code -> Représentation intermédiaire -> Code machine -> Exécution). La grande différence entre deux étapes est de savoir quelles étapes sont vues par l'utilisateur et lesquelles sont vues par le programmeur. Avec la compilation native, le programmeur voit tout sauf l'étape d'exécution, mais avec la solution JIT, la compilation en code machine est vue par l'utilisateur, en plus de l'exécution.
L'affirmation selon laquelle A est plus rapide que B fait référence au temps pris par le programme pour s'exécuter, tel que vu par l'utilisateur . Si nous supposons que les deux morceaux de code fonctionnent de manière identique à l'étape d'exécution, nous devons supposer que le flux de travail JIT est plus lent pour l'utilisateur, car il doit également voir le temps T de la compilation en code machine, où T> 0. , pour que le flux de travail JIT puisse fonctionner de la même manière que le flux de travail natif, nous devons réduire le temps d’exécution du code pour que le code Exécution + Compilation en code machine soit inférieur au seul stade d’exécution. du flux de travail natif. Cela signifie que nous devons optimiser le code mieux dans la compilation JIT que dans la compilation native.
Ceci est cependant plutôt infaisable, car pour effectuer les optimisations nécessaires afin d’accélérer l’exécution, nous devons passer plus de temps à l’étape de la compilation vers le code machine, de sorte que chaque fois que nous économisons du fait du code optimisé est perdu, on l'ajoute à la compilation. En d'autres termes, la "lenteur" d'une solution basée sur JIT n'est pas simplement due au temps ajouté pour la compilation JIT, mais au code produit par cette compilation, elle est plus lente qu'une solution native.
Je vais utiliser un exemple: attribution de registre. Comme l'accès mémoire est plusieurs milliers de fois plus lent que l'accès aux registres, nous souhaitons idéalement utiliser des registres dans la mesure du possible et disposer du moins d'accès mémoire possible, mais nous avons un nombre limité de registres et nous devons passer l'état à la mémoire lorsque nous en avons besoin. un registre. Si nous utilisons un algorithme d'allocation de registre qui prend 200 ms à calculer, nous économisons donc 2 ms de temps d'exécution. Nous n'utilisons pas le temps de la meilleure façon possible pour un compilateur JIT. Des solutions telles que l'algorithme de Chaitin, qui peut produire un code hautement optimisé, ne conviennent pas.
Le rôle du compilateur JIT est de trouver le meilleur équilibre entre le temps de compilation et la qualité du code produit, avec toutefois un fort parti pris pour le temps de compilation rapide, car vous ne voulez pas laisser l’utilisateur en attente. Les performances du code en cours d'exécution sont plus lentes dans le cas de JIT, car le compilateur natif n'est pas lié (beaucoup) par le temps dans l'optimisation du code, il est donc libre d'utiliser les meilleurs algorithmes. La possibilité que la compilation globale + l'exécution pour un compilateur JIT puisse battre uniquement le temps d'exécution pour le code compilé en mode natif est effectivement 0.
Mais nos machines virtuelles ne se limitent pas à la compilation JIT. Ils utilisent des techniques de compilation rapides, la mise en cache, le remplacement à chaud et l'optimisation adaptative. Modifions donc notre affirmation selon laquelle la performance correspond à ce que voit l'utilisateur et limitons-la au temps nécessaire à l'exécution du programme (supposons que nous avons compilé AOT). Nous pouvons effectivement rendre le code d'exécution équivalent au compilateur natif (ou peut-être mieux?). Une grande revendication pour les VM est qu’elles peuvent produire un code de meilleure qualité qu’un compilateur natif, car il a accès à plus d’informations - celles du processus en cours, telles que la fréquence d’exécution d’une fonction donnée. La VM peut ensuite appliquer des optimisations adaptatives au code essentiel via un échange à chaud.
Cet argument pose cependant un problème: il suppose que l'optimisation guidée par le profil, entre autres, est unique en son genre pour les ordinateurs virtuels, ce qui n'est pas vrai. Nous pouvons également l'appliquer à la compilation native - en compilant notre application avec le profilage activé, en enregistrant les informations, puis en recompilant l'application avec ce profil. Il est également intéressant de noter que le remplacement à chaud de code n’est pas une chose que seul un compilateur JIT peut faire, nous pouvons le faire pour du code natif - bien que les solutions basées sur JIT soient plus facilement disponibles et beaucoup plus simples pour le développeur. La grande question est donc la suivante: une machine virtuelle peut-elle nous fournir des informations que la compilation native ne peut pas générer, ce qui peut améliorer les performances de notre code?
Je ne peux pas le voir moi-même. Nous pouvons également appliquer la plupart des techniques d’une VM typique au code natif - bien que le processus soit plus complexe. De même, nous pouvons appliquer toutes les optimisations d'un compilateur natif à une machine virtuelle qui utilise la compilation AOT ou des optimisations adaptatives. La réalité est que la différence entre le code natif et celui exécuté dans une machine virtuelle n'est pas aussi grande qu'on le croit. Ils aboutissent finalement au même résultat, mais ils adoptent une approche différente pour y parvenir. La machine virtuelle utilise une approche itérative pour produire un code optimisé, où le compilateur natif l’attend dès le départ (et peut être améliorée avec une approche itérative).
Un programmeur C ++ pourrait faire valoir qu'il a besoin des optimisations dès le départ et qu'il ne devrait pas attendre qu'une machine virtuelle détermine comment les réaliser, voire pas du tout. C’est probablement un point valable avec notre technologie actuelle, car le niveau actuel d’optimisation dans nos VM est inférieur à ce que les compilateurs natifs peuvent offrir - mais cela ne sera peut-être pas toujours le cas si les solutions AOT de nos VM s’améliorent, etc.