Chapitre 11 Compilation et optimisation du backend

Aperçu

Quand et dans quel état le compilateur convertit le fichier de classe en code machine binaire lié à l'infrastructure locale peut être considéré comme le back-end de l'ensemble du processus de compilation

Il existe donc deux types: le compilateur à l'avance et le compilateur juste à temps

Compilateur juste à temps

Java est initialement interprété et exécuté via un interpréteur. Lorsque la machine virtuelle détecte qu'une méthode ou un bloc de code s'exécute très fréquemment, il sera considéré comme un code actif. Lorsqu'il est en cours d'exécution, il compilera ces codes dans le code de la machine locale et utilisera chaque Optimisation du code autant que possible

Interprète et compilateur

La machine virtuelle hotspot contient à la fois un interpréteur et un compilateur. Lorsque le programme doit être démarré et exécuté rapidement, l'interpréteur peut jouer un rôle en premier, économisant le temps de compilation et s'exécutant immédiatement. Au fil du temps, le compilateur prend progressivement effet, compilant de plus en plus de codes dans le code machine local, réduisant la perte intermédiaire de l'interpréteur et augmentant l'efficacité d'exécution.

L'interpréteur économise de la mémoire et le compilateur améliore l'efficacité

L'interpréteur peut également être utilisé comme porte de secours pour le compilateur lors de l'optimisation radicale. Autrement dit, le compilateur peut choisir la plupart des méthodes d'optimisation pour augmenter la vitesse d'exécution en fonction de la probabilité, mais ce n'est pas correct à chaque fois. Lorsque l'hypothèse d'optimisation radicale ne tient pas, elle peut être renvoyée par optimisation inverse. Continuer l'exécution pour expliquer l'état

Intégré deux compilateurs juste à temps C1 (compilateur côté client) et C2 (compilateur côté serveur)

Avant le mode hiérarchique, l'interpréteur travaille directement avec l'un des compilateurs, et la machine virtuelle sélectionne automatiquement le mode de fonctionnement en fonction de sa propre version et des performances matérielles de la machine hôte

Utiliser généralement le mode mixte

Compilation en couches

Étant donné que le compilateur juste à temps compile le code local prend le temps d'exécution du programme et que le code avec un haut degré d'optimisation doit être compilé, plus cela prendra du temps. Afin de trouver un équilibre entre la vitesse de réponse au démarrage du programme et l'efficacité d'exécution, la compilation hiérarchique est utilisée

  • Niveau 0, exécution d'interprétation pure, et l'interpréteur n'ouvre pas la surveillance des performances
  • La première couche, utilisez C1 pour compiler le bytecode en code local sans activer la surveillance des performances
  • La deuxième couche consiste à utiliser C1 pour compiler le bytecode en code local, en ouvrant uniquement les méthodes et les statistiques back-end et d'autres contrôles de performances limités
  • Au niveau de la couche 3, utilisez C1 pour compiler le bytecode en code local et activez toute la surveillance des performances
  • La première couche consiste à utiliser C2 pour compiler le bytecode en code local et activer toute la surveillance des performances. Certaines optimisations radicales peu fiables seront également effectuées en fonction des informations de surveillance des performances.

Après avoir implémenté la compilation hiérarchique, l'interpréteur, C1, C2 fonctionnent en même temps, le code actif peut être compilé plusieurs fois, utilisez C1 pour obtenir une vitesse de compilation plus élevée, utilisez C2 pour obtenir une meilleure qualité de compilation, et il n'est pas nécessaire de supporter une collection supplémentaire pendant l'interprétation et l'exécution. La tâche d'information de surveillance des performances, et lorsque C2 utilise des algorithmes d'optimisation de haute complexité, C1 peut d'abord utiliser des optimisations simples pour gagner plus de temps de compilation pour lui

Objet de compilation et condition de déclenchement

Code actif: méthode qui est appelée plusieurs fois, corps de boucle qui est exécuté plusieurs fois

Dans les deux cas, l'objet compilé est le corps entier de la méthode, pas un corps de boucle séparé

Pour ce dernier, l'entrée d'exécution est légèrement différente. Le numéro de séquence du bytecode du point d'entrée d'exécution est passé lors de la compilation, en pensant que la compilation se produit pendant l'exécution de la méthode, on l'appelle donc remplacement de pile, c'est-à-dire que le frame de pile de la méthode est toujours sur la pile, et la méthode Fut remplacé

Détection des points chauds

  1. Sur la base de la détection des points chauds d'échantillonnage, la machine virtuelle vérifie périodiquement le haut de la pile d'appels de chaque thread. S'il constate qu'une méthode apparaît souvent en haut de la pile, il s'agit d'un code actif. L'inconvénient est qu'il est difficile de confirmer avec précision la popularité d'une méthode
  2. Sur la base de la détection des points chauds basée sur un compteur, la machine virtuelle crée un compteur pour chaque méthode et compte le nombre d'exécutions de la méthode. Relativement plus précis et rigoureux

Implémentation de machine virtuelle HotSpot

En utilisant la détection de points chauds basée sur un compteur, un compteur d'appels de méthode est préparé et un compteur de rebord (fait référence à un saut de frontière de boucle). Une fois le seuil du compteur dépassé, il déclenchera la compilation immédiate.

Par défaut, le moteur d'exécution n'attendra pas l'achèvement de la demande de compilation de manière synchrone. Au lieu de cela, il continuera à entrer dans l'interpréteur pour exécuter le bytecode de la manière interprétée jusqu'à ce que la demande soumise soit compilée par le compilateur juste à temps. Lorsque la compilation est terminée, l'entrée d'appel de méthode sera modifiée par le système. Est la nouvelle valeur, la version compilée sera utilisée la prochaine fois que cette méthode sera appelée

Compteur d'appels de méthode

Le compteur d'appels de méthode n'est pas le nombre absolu d'appels de méthode, mais la fréquence d'exécution relative, c'est-à-dire le nombre d'appels de méthode dans une période de temps.

Lorsque plus d'une période de temps, le nombre d'appels de méthode n'est toujours pas suffisant pour qu'il soit soumis au compilateur juste à temps pour la compilation, le compteur d'appels de la méthode sera divisé par deux. Ce processus est appelé la décroissance du compteur d'appels de méthode. Cette période s'appelle la période de demi-vieIMG_20200828_0846415

Compteur arrière

Compter le nombre d'exécutions du code du corps de la boucle dans une méthode. Dans le bytecode, l'instruction selon laquelle le flux de contrôle saute en arrière est appelée le bord arrière. Le but est de déclencher la compilation de remplacement sur la pile.
Lorsque le seuil est dépassé, elle sera soumise à une pile. Remplacez la demande de compilation et réduisez légèrement la valeur du compteur de bouclage pour continuer à exécuter la boucle dans l'interpréteur et attendez que le compilateur affiche le résultat de la compilation. Le
compteur de bouclage ne compte pas l'atténuation des points chauds et le nombre est le nombre absolu d'exécutions de boucle de la méthode

Processus de compilation

Dans les conditions par défaut, qu'il s'agisse d'une demande de compilation standard générée par un appel de méthode ou une demande de compilation de remplacement sur la pile, la machine virtuelle continue d'exécuter le code de la manière interprétée avant que le compilateur n'ait terminé la compilation, et l'action de compilation est effectuée dans le thread de compilation en arrière-plan En cours

Compilateur client

Est un compilateur en trois étapes relativement simple et rapide, l'accent principal est sur l'optimisation locale et abandonne de nombreuses méthodes d'optimisation globale chronophages

  1. Un frontal indépendant de la plate-forme construit le bytecode en une représentation de code intermédiaire de haut niveau (HIR)
  2. Un backend lié à la plate-forme génère une représentation de code intermédiaire de bas niveau (LIR) à partir de HIR
  3. Utilisez un algorithme d'analyse linéaire sur le backend lié à la plate-forme pour générer du code machine

Compilateur côté serveur

Spécifiquement pour les scénarios d'application typiques côté serveur. C'est un compilateur avancé qui peut tolérer une complexité d'optimisation élevée. Effectuez la plupart des opérations d'optimisation classiques et certaines optimisations radicales prédictives instables (telles que la prédiction de fréquence de branche, etc.)

Combat réel: visualisez et analysez même les résultats compilés

Même si le processus de compilation de la machine virtuelle Java est complètement transparent pour l'utilisateur et le programme, la
boucle vide n'est en fait pas exécutée dans le code natif final.

Compilateur Ahead

Avantages et inconvénients de la compilation à l'avance

Deux branches

  1. Le travail de traduction statique qui compile le code du programme en code machine avant que le programme ne s'exécute. Sa valeur est la plus grande faiblesse du compilateur juste à temps: la compilation juste à temps prend du temps d'exécution du programme et des ressources informatiques. Par conséquent, une optimisation chronophage est effectuée dans la compilation statique pour générer un code de haute qualité. L'effet secondaire est que l'installation est lente
  2. Préparez et enregistrez le travail de compilation que le compilateur juste à temps d'origine doit effectuer au moment de l'exécution, et chargez-le directement lorsque vous exécutez le code la prochaine fois. Essentiellement, il s'agit d'accélérer le cache du compilateur instantané pour améliorer le temps de démarrage du programme Java et le problème qu'il faut une période de préchauffage pour atteindre les meilleures performances. Il peut être appelé cache de compilation juste à temps. L'inconvénient est que cette méthode de pré-compilation doit non seulement être liée à la machine cible, mais doit également être liée aux paramètres de fonctionnement de la machine virtuelle HotSpot.

Avantages naturels des compilateurs juste à temps

Optimisation des conseils d'analyse des performances

Pendant l'exécution de l'interpréteur ou du compilateur côté client, les informations de surveillance des performances sont collectées en permanence. Ces informations ne sont généralement pas disponibles pendant l'analyse statique, ou il n'y a pas de solution unique définie. Cependant, leurs préférences peuvent être clairement vues pendant le fonctionnement dynamique, et le code instantané peut être concentré et optimisé pour allouer des ressources.

Optimisation prédictive agressive

Vous pouvez audacieusement optimiser en fonction d'hypothèses à haute probabilité. En cas d'erreur, vous pouvez recourir à un compilateur à faible coût ou même à un interpréteur pour l'exécution.

Optimiser au moment de la liaison

Le langage Java est intrinsèquement lié de manière dynamique. Les fichiers de classe sont chargés dans la mémoire de la machine virtuelle pendant l'exécution, puis le code optimisé est généré dans le compilateur juste à temps

Techniques d'optimisation du compilateur

L'objectif du compilateur est le processus de traduction du code du programme en code machine natif, mais la qualité de l'optimisation du code de sortie est la clé pour déterminer si le compilateur est excellent ou non. Le
compilateur instantané optimise le code et le modifie au milieu du code ou sur le code machine. , Au lieu du code
optimisé sur le code source Java , l'effet est le même, mais si de nombreuses instructions de code sont omises, plus l'écart entre le bytecode et les instructions de code machine est grand.

Inlining de méthode

Il s'agit de la méthode d'optimisation la plus importante du compilateur. Elle est généralement placée en haut de la séquence d'optimisation.
Il n'y a pas de méthode à insérer et la plupart des autres optimisations ne peuvent pas être effectuées efficacement, car les opérations de nombreuses méthodes peuvent être des
comportements d'optimisation significatifs à comprendre. Il s'agit de "copier" le code de la méthode cible intact dans la méthode qui a initié l'appel, mais l'implémentation Java est assez compliquée

objectif

  1. Supprimez le coût des appels de méthode (comme la recherche de versions de méthode, l'établissement de cadres de pile, etc.)
  2. Construire une bonne base pour d'autres optimisations

Problème en ligne de la méthode d'implémentation Java

En général, seules les méthodes privées, les constructeurs d'instances, les méthodes parentes, les méthodes statiques et les méthodes finales seront résolues au moment de la compilation. Tous les autres appels de méthode Java doivent effectuer une sélection polymorphe des récepteurs de méthode au moment de l'exécution, et il peut y avoir plus d'une version de récepteurs de méthode. La méthode d'instance par défaut est donc une méthode virtuelle.
Par conséquent, la méthode doit être allouée dynamiquement en fonction du type réel, et le type réel doit être déterminé après l'exécution de la ligne de code. Il est difficile d'obtenir une conclusion absolument précise au moment de la compilation. La
méthode par défaut des objets Java est les méthodes virtuelles. Java encourage indirectement les programmeurs à utiliser beaucoup de Méthodes virtuelles pour implémenter la logique du programme

Méthode de mise en œuvre Java méthode en ligne

Utilisez l'analyse des relations d'héritage de type pour déterminer les informations d'interface et de classe parente de la classe actuellement chargée. De cette façon, le compilateur prendra différents traitements pour différentes situations lors de l'inlining:

  1. S'il s'agit d'une méthode non virtuelle, directement en ligne
  2. S'il s'agit d'une méthode virtuelle et qu'il n'existe qu'une seule version, on suppose qu'il n'y a que celle-ci. Cependant, il s'agit d'une optimisation prédictive radicale, et une porte de secours doit être réservée, c'est-à-dire une issue lorsque l'hypothèse n'est pas vraie. Si vous chargez ultérieurement une nouvelle classe qui entraîne une modification de la relation d'héritage, vous devez ignorer le code compilé, revenir à l'état interprété pour continuer l'exécution ou recompiler.
  3. S'il existe plusieurs versions, la mise en cache en ligne est utilisée. Le cache en ligne est un cache construit avant l'entrée normale de la méthode cible. Avant l'appel de méthode, l'état du cache en ligne est vide. Lorsque le premier appel est effectué, le cache enregistre les informations de version du récepteur de méthode et compare la version du récepteur à chaque fois que la méthode est appelée, chaque fois qu'elle entre Le récepteur de la méthode d'appel est le même, il s'agit d'un cache en ligne monomorphe

Par conséquent, dans la plupart des cas, la méthode d'inlining effectuée par la machine virtuelle Java est une optimisation radicale.S'il y a un événement de faible probabilité, la porte de secours sera utilisée pour revenir à l'état d'interprétation et s'exécuter à nouveau.

Analyse d'évasion

L'une des techniques d'optimisation les plus avancées est une technique d'analyse qui fournit une base pour d'autres optimisations. Cependant, le coût du calcul de l'évasion est très élevé et l'effet peut être instable.

principe

Analyse la portée dynamique d'un objet. Lorsqu'un objet est défini dans une méthode, il peut être référencé par une méthode externe, par exemple être passé à d'autres méthodes en tant que paramètre d'appel. C'est ce qu'on appelle la méthode escape. Il peut même être accédé par des threads externes, comme l'attribution de valeurs à des variables d'instance accessibles dans d'autres threads, ce qui est appelé échappement de thread.
Si vous pouvez prouver qu'un objet ne s'échappera pas en dehors de la méthode ou du thread, ou si le degré d'échappement est relativement faible, vous pouvez utiliser les techniques suivantes

Allocation sur la pile

Java alloue presque toujours de l'espace mémoire pour créer des objets sur le tas. Les objets du tas sont partagés et visibles par tous les threads. Tant que vous détenez une référence à cet objet, vous pouvez accéder aux données d'objet stockées dans le tas.
Si vous êtes sûr qu'un objet n'échappera pas au thread, vous pouvez laisser l'objet allouer de la mémoire sur la pile. Peut prendre en charge la méthode d'échappement, ne prend pas en charge l'échappement de thread

Substitution scalaire

Si un élément de données ne peut pas être décomposé en éléments de données plus petits, il s'agit d'un scalaire. (Int, long, référence, etc.)
Si l'objet java est rompu et que les variables membres utilisées sont restaurées au type d'origine pour l'accès, ce processus est une substitution scalaire.
Si l'analyse d'échappement peut prouver qu'un objet ne sera pas utilisé en dehors de la méthode et que l'objet peut être divisé, alors l'objet peut ne pas être créé lorsqu'il est réellement exécuté, mais plusieurs variables membres de celui-ci seront créées. Il s'agit d'un cas particulier d'allocation sur la pile, qui est plus simple à mettre en œuvre et a des exigences plus élevées pour l'évacuation

Élimination de la synchronisation

La synchronisation des threads est une opération relativement longue. Si une variable n'échappe pas au thread, il n'y a pas de concurrence en lecture et en écriture, et les mesures de synchronisation implémentées sur la variable peuvent être éliminées

Élimination des sous-expressions courantes

L'une des techniques d'optimisation classiques indépendantes du langage.
Si une expression E a été calculée auparavant et que toutes les valeurs de variable de E restent inchangées d'avant à maintenant, vous pouvez remplacer E par le résultat calculé précédemment

Élimination de la vérification des limites du tableau

L'une des techniques d'optimisation classiques liées au
langage, la sécurité dynamique Java, l'accès au système d'éléments de tableau vérifiera automatiquement les limites supérieure et inférieure, ce qui est convivial pour les développeurs, mais chaque fois qu'un élément de tableau est lu et écrit, il y a une opération de jugement conditionnel implicite, qui en est une. Type de charge de performance.
Si l'accès au tableau se produit dans une boucle et que la variable de boucle est utilisée pour l'accès au tableau, si le compilateur peut déterminer que la plage de valeurs de la variable de boucle se trouve dans la longueur du tableau grâce à l'analyse du flux de données, les vérifications des limites supérieure et inférieure de la boucle peuvent être éliminées Laissez tomber

Combat réel: compréhension approfondie du compilateur Graal

La vérification de plage est conviviale pour les développeurs, mais chaque fois qu'un élément de tableau est lu ou écrit, il y a une opération de jugement conditionnel implicite, ce qui est une charge de performance.
Si l'accès au tableau se produit dans une boucle et que la variable de boucle est utilisée pour l'accès au tableau, si le compilateur peut déterminer que la plage de valeurs de la variable de boucle se trouve dans la longueur du tableau grâce à l'analyse du flux de données, les vérifications des limites supérieure et inférieure de la boucle peuvent être éliminées Laissez tomber

Combat réel: compréhension approfondie du compilateur Graal

Graal Compiler: La dernière réalisation du compilateur juste à temps et du compilateur précoce. Il devrait devenir une efficacité de compilation de haute qualité, une qualité de sortie élevée, un support pour une compilation précoce, une compilation juste à temps.

Je suppose que tu aimes

Origine blog.csdn.net/weixin_42249196/article/details/108374475
conseillé
Classement