Les optimisations de performances de Java 9

Les optimisations de performances de Java 9

Dans un précédent article sur Java 9, j’avais parcouru les principales nouveautés à destination des développeurs : https://www.loicmathieu.fr/wordpress/informatique/les-nouveautes-de-java-9-pour-les-developeurs.

Je vais ici parcourir les principales nouveautés axées sur la performance

Je vais encore reprendre les principales JEP :

JEP 143: Improve Contended Locking

Optimisation des monitors Java (optimisation des locks) en cas de contention (quand plusieurs threads accèdent au même monitor). Ce sont des changements au sein de la JVM qui permettent des gains de performances significatifs (x2) sur certaines opérations de la JVM sur les primitives de lock des objets (synchronize, wait, …)

Plus d’info dans la présentation de Monica Beckwith sur le sujet :

https://www.slideshare.net/MonicaBeckwith/jfokus-java-9-contended-locking-performance

JEP 193: Variable Handles

Une nouvelle API de Java, à destination des utilisateurs avancés, permettant de remplacer l’usage d’Unsafe pour l’accès performant à des variables de manière atomique.

A variable handle is a typed reference to a variable, which supports read and write access to the variable under a variety of access modes. Supported variable kinds include instance fields, static fields and array elements.

Plus d’info dans la description de la JEP : http://openjdk.java.net/jeps/193

JEP 197: Segmented Code Cache

Le code cache (une partie du Metaspace) a été séparé par type de code (méthode, non-méthode, profilé, non-profilé, …) pour en optimiser les performances et permettre de futures optimisations.

Plus d’info dans la description de la JEP : http://openjdk.java.net/jeps/197

JEP 254: Compact Strings

C’est une des principales JEP autour de la performance!

L’implémentation des String en Java a été revue pour en proposer une version plus compacte.

Jusqu’ici les String étaient stockées en UTF-16, chaque caractère prenait donc deux octets car stockés dans un tableau de char.

Avec les Compact String, a la construction de la String, si elle est compatible ISO-8859-1 (ou Latin-1), chaque caractère sera alors stocké dans un seul octet, le stockage d’une String passant alors d’un tableau de char à un tableau de byte. Le gain potentiel est énorme : diminution de presque 50% de la taille nécessaire pour stocker les String. Les String sont la principale source d’utilisation de la mémoire et bien souvent elles sont compatible ISO-8859-1.

Si la String est non compatible ISO-8859-1, alors les caractères seront quand mêmes stockés dans un tableau de byte mais il faudra deux bytes pour chaque caractère. Un flag ayant était ajouté à la classe String pour connaitre l’encoding de celle-ci.

Ce changement a été fait sans aucun changement dans l’API publique java : en utilisant Java 9, on utilise directement les Compact Strings! StringBuilder/Buffer ont aussi était mis à jour pour en tirer parti.

Lors des tests préliminaires à l’implémentation de cette JEP, les gains suivants ont été avancés (voir http://cr.openjdk.java.net/~shade/density/state-of-string-density-v1.txt) :

  • Gain de 5% à 15% de la taille de la heap (25% des objets sont des String)
  • Gain de performance de 20% pour Latin-1 comparé à UTF-16 (30% de garbage en moins) : microbenchmark d’instanciation/concaténation de String

De plus, ce changement ouvre la porte à de nombreuses optimisations dans le code de Java et de la JVM (entre autre, encoder/décoder des String en ISO-8859-1 ou UTF-8 si compatible).

Plus d’info dans la JEP : http://openjdk.java.net/jeps/254

JEP 274: Enhanced Method Handles

Plusieurs ajout à l’API MethodHandle permettant de créer de nouvelles optimisations au compilateur.

Plus d’info dans la description de la JEP : http://openjdk.java.net/jeps/274

JEP 280: Indify String Concatenation

C’est une des principales JEP autour de la performance!

Après la compaction des String, en voici la concatenation amélioré!

Quelques explications tous d’abord.

Quand on concatène des chaînes de caractères en java, on créé beaucoup d’objets éphémères qui seront ensuite collectés par la Garbage Collector (GC) :

String s = "toto" + "tata" + "titi" + "tutu";

Entraîne la création de 6 String car les String sont immuable : s, « toto », « tata », « titi », »tototata », « tototatatiti », « tototatatititutu » … pour finir par ne garder que s et donc les autres objets seront à nettoyer par le GC.

Depuis Java 6, le compilateur Java (javac) essaye d’optimiser ça en remplaçant, lors de la compilation, les concaténations par l’usage de StringBuilder. Il remplace donc le code précédent par

String s = new StringBuilder("toto").append("tata").append("titi").append("tutu").toString();

Cette optimisation, bien qu’efficace, a quelques limites, entre autre elle dépend de la capacité du compilateur à détecter la concaténation de String. De plus le Just In Time Compiler de Java (JIT) optimise aussi ces concaténation de chaîne à plusieurs niveau.

La JEP 280 remplace celà par l’utilisation du bytecode InvodeDynamic lors de la concaténation de String et l’implémentation au runtime d’une stratégie de concaténation optimisée directement au sein de la JVM (intrinsifiée donc). Plusieurs stratégies de concaténation de chaînes ont été codées dont une à base de MethodHandles (avec StringBuilder ou en inline), c’est celle par défaut dorénavant.

Les tests lors du développement ont monté que (voir http://cr.openjdk.java.net/~shade/8085796/notes.txt):

  • Les performances lorsque le code est compilé par C2 (le dernier niveau du JIT) sont identiques
  • Les performances lorsque le code est uniquement compilé par C1 (le premier niveau du JIT) sont meilleurs
  • Les performances lorsque C1/C2 n’arrivent pas à optimiser les concaténations sont largement meilleurs !

En conclusion : pas de régression de performance, meilleurs performances au démarrage (quand le code est non compilé ou uniquement par C1), meilleurs performances quand javac ou C1/C2 n’arrivent pas à optimiser le code!

Plus d’info dans les détails de la JEP : http://openjdk.java.net/jeps/280

JEP 285: Spin-Wait Hints

Une nouvelle méthode dans la classe Thread permet de notifier la JVM qu’on est dans une spin-wait loop. Libre à la JVM de faire ou ne pas faire quelque chose (cette méthode est native). Une spin-wait loop est une boucle dans laquelle le thread en question ne peut pas avancer car en attente d’un événement externe (busy wait).

Voici un exemple tiré de la JavaDoc

class EventHandler {
  volatile boolean eventNotificationNotReceived;
  
  void waitForEventAndHandleIt() {
    while ( eventNotificationNotReceived ) {
      java.lang.Thread.onSpinWait();
    }
  readAndProcessEvent();
}

  void readAndProcessEvent() {
    // Read event from some source and process it
    . . .
  }
}

Plus d’info dans la description de la JEP : http://openjdk.java.net/jeps/285

2 réflexions sur « Les optimisations de performances de Java 9 »

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.