Benchmarking et Optimisations en Java
Optimisations dynamiques
Cette partie traite des optimisations dynamiques effectuées par HotSpot.Deoptimization
La JVM peut décider, au cours de l'exécution, d'arrêter d'utiliser une méthode compilée, et de retourner à sa version interprétée avant de la recompiler.Dans quel cas ?
- Chargement de classes : invalidation d'appels "monomorphiques". Pour rappel, on parle d'appels mornomorphiques lorsqu'une méthode est appelée directement sans appel virtuel ( pas de mécanisme de VTABLE ).
- Gestion d'exceptions : la version compilée n'inclue pas les branches concernées.
Enseignement : ces deux cas soulignent la nécessité de détecter la compilation JIT dans son benchmark.
On-Stack Replacement
La JVM HotSpot est désormais dôté d'un mécanisme qui permet de compter les itérations d'un bloc de code et de le remplacer par une version compilée au milieu de l'appel.Voici un exemple qui permet de mieux comprendre cette fonctionnalité :
- L'interpréteur commence à interpréter le contenu de la méthode main.
- A 10 000 itérations s'amorce la compilation, mais l'interprétation suit son cours en attendant.
- La compilation est terminée, l'interprétation est toujours en cours.
- Le compteur atteint 14 000 itérations, arrêt de l'interprétation, OSR intervient !
- La méthode main est recompilée pour pouvoir entrer en milieu de boucle.
- La version compilée s'exécute.
- main() est terminée.
A première vue, cela peut sembler génial mais le bonheur est de courte durée lorsqu'on apprend que le code généré par OSR n'est pas optimal.
En effet, avec OSR, nous sommes garantis de ne pas profiter des optimisations suivantes :
- Pas de déroulement de boucles
- Pas d'élimination de vérification de taille de tableaux
- Pas de loop-hoisting (déplacement de code en dehors de la boucle si indépendant de la valeur du compteur)
Ainsi, si OSR est utilisé, on ne mesurera pas le temps d'exécution optimal.
Enseignement : il est donc préconisé de déplacer les boucles dans différentes méthodes afin que ces dernières soient intégralement compilées (gain de temps appréciable et visibilité améliorée).
Dead Code Elimination
Cette optimisation consiste à éliminer purement et simplement les morceaux de code inutiles.C'est d'ailleurs une des seules optimisations qui peut être réalisée en amont par le compilateur javac, mais la JVM suit également une politique très agressive à ce sujet.
Ces éliminations sans concession sont bien sûr sources de résultats eronnés.
Soit le code suivant :
Nous voyons ici que la variable result est utilisée dans l'affichage à la fin du traitement. Si nous l'enlevons, le temps mesuré va considérablement chuter, quitte à devenir insignifiant, preuve que la JVM a suffisamment été intelligente pour supprimer les lignes inutiles.
Enseignement : il faut toujours s'assurer que le résultat d'un traitement soit utilisé pour l'affichage final.