Comment le compilateur JIT optimise-t-il le code ?

Lorsqu'une méthode est choisie pour être compilée, la JVM envoie ses bytecodes au compilateur JIT (Just-In-Time). Le compilateur doit comprendre la sémantique et la syntaxe des bytecodes pour pouvoir compiler correctement la méthode.

Pour permettre au compilateur JIT d'analyser la méthode, ses bytecodes sont reformulés dans une représentation appelé arbres qui ressemble plus au code machine que les bytecodes. Ensuite, l'analyse et les optimisations sont exécutées sur les arbres de la méthode. A la fin, les arbres sont convertis en code natif. Le reste de cette section présente brièvement les phases de la compilation JIT. Pour plus d'informations, voir Diagnostic d'un problème JIT ou AOT.

Le compilateur JIT peut utiliser plusieurs unités d'exécution de compilation pour effectuer les tâches de compilation JIT. L'utilisation de plusieurs unités d'exécution peut potentiellement aider les applications Java à démarrer plus rapidement. En pratique, elle n'améliore les performances que si le système comporte des fichiers core de traitement inutilisés.

Le nombre par défaut d'unités d'exécution de compilation est identifié par la machine virtuelle Java et dépend de la configuration système. Si le nombre d'unités d'exécution résultant n'est pas optimal, vous pouvez remplacer la décision JVM à l'aide de l'option -XcompilationThreads. Pour plus d'informations sur l'utilisation de cette option, voir -X options.
Remarque: Si votre système ne dispose pas de coeurs de traitement inutilisés, il est peu probable que l'augmentation du nombre d'unités d'exécution de compilation entraîne une amélioration des performances.

Les étapes de la compilation sont les suivantes. Toutes les étapes, à l'exception de la génération du code natif, sont du code interplateformes.

Etape 1 : insertion par référence

L'insertion par référence est le processus par lequel les arbres des petites méthodes sont fusionnées ou "insérées par référence" dans les arbres de leurs appelants. Cela permet d'accélérer les appels de méthodes fréquemment exécutées. Deux algorithmes d'insertion par référence avec différents niveaux d'agressivité sont utilisés, en fonction du niveau d'optimisation en cours. Les optimisations exécutées dans cette étape sont :
  • Trivial inlining
  • Call graph inlining
  • Tail recursion elimination
  • Virtual call guard optimizations

Etape 2 : optimisations locales

Les optimisations locales analysent et améliorent une petite partie du code à la fois. La plupart des optimisations locales implémentent des techniques testées utilisées dans les compilateurs classiques. Les optimisations incluent :
  • l'analyse et l'optimisation du flux de données local
  • l'enregistrement de l'optimisation de l'utilisation
  • Simplifications des idiomes Java
Ces techniques sont appliquées de manière répétitive, notamment après les optimisations globales qui peuvent avoir permis d'identifier plusieurs points à améliorer.

Etape 3 : optimisations du flux de contrôle

Les optimisations du flux de contrôle analysent le flux de contrôle dans une méthode (ou des sections spécifiques du flux) et réorganisent les chemins du code pour améliorer leur efficacité. Les optimisations sont :
  • Code reordering, splitting, and removal
  • Loop reduction and inversion
  • Loop striding and loop-invariant code motion
  • Loop unrolling and peeling
  • Loop versioning and specialization
  • Exception-directed optimization
  • Switch analysis

Etape 4 : optimisations globales

Les optimisations globales fonctionnent sur l'ensemble de la méthode. Elles ont un plus grand impact, car le délai de compilation est plus long, mais elles peuvent améliorer considérablement les performances. Les optimisations sont :
  • l'analyse et l'optimisation du flux de données global
  • l'élimination de redondance partielle
  • l'analyse d'échappement
  • les optimisations de la récupération de place et de l'allocation de mémoire
  • les optimisations de la synchronisation

Etape 5 : génération du code natif

Les processus de génération du code natif varient en fonction de l'architecture de la plateforme. Généralement, au cours de cette étape de la compilation, les arbres des méthodes sont convertis en instructions en code machine ; de petites optimisations sont exécutées en fonction des caractéristiques de l'architecture. Le code compilé est placé dans une partie de l'espace du processus JVM appelé cache du code ; l'emplacement de la méthode dans le cache du code est enregistré de sorte que les appels suivants à la méthode appellent le code compilé. A tout moment, le processus JVM est constitué des fichiers exécutables JVM et d'un ensemble de code compilé JIT lié dynamiquement à l'interpréteur de bytecode dans la JVM.