URL: https://linuxfr.org/news/openjdk-8-jep-142-false-sharing Title: OpenJDK 8, JEP 142 & False Sharing Authors: ckyl Davy Defaud, ZeroHeure, Florent Zara, palm123, tuiu pol et Benoît Sibaud Date: 2014年04月02日T17:19:51+02:00 License: CC By-SA Tags: jmh, false_sharing, jep142, openjdk et inside_openjdk Score: 28 Java 8 est sorti ce mois‐ci et vous avez même eu droit à une [dépêche, ici‐même, qui parle des lambdas, de l’API flux (_stream API_)](http://linuxfr.org/news/java-8-et-netbeans-8-sont-disponibles), etc. Cependant, derrière ces gros changements qui impactent le monde hétérogène des développeurs Java, il y a des petits changements qui eux servent plutôt aux développeurs qui font des briques de base, de l’infrastructure ou du code qui va vite. Je vous propose donc d’explorer quelques _JDK Enhancement Proposals_ d’OpenJDK. Pour cette première dépêche, on commence avec la [JEP 142](http://openjdk.java.net/jeps/142) : _Reduce Cache Contention on Specified Fields_ soit l’annotation `@Contended` qui vise à proposer une solution aux problèmes de [_false sharing_](http://en.wikipedia.org/wiki/False_sharing). NdM : _merci à ckyl pour son journal._ ---- [Journal à l’origine de la dépêche](http://linuxfr.org/users/cykl/journaux/openjdk-8-jep-142-false-sharing) [LinuxFr.org : Java 8 et NetBeans 8 sont disponibles](https://linuxfr.org/news/java-8-et-netbeans-8-sont-disponibles) [Nouveautés du JDK 8](http://www.oracle.com/technetwork/java/javase/8-whats-new-2157071.html) [JEP 142: Reduce Cache Contention on Specified Fields](http://openjdk.java.net/jeps/142) ---- ## C’est quoi le _false sharing_ ? Le _false sharing_ est un problème de performance en environnement parallèle qui est causé par une _« leaky abstraction »_ du matériel. La présentation suivante est extrêmement grossière et ne vise qu’à faire comprendre le problème aux gens ne connaissant pas du tout le domaine. En tant que développeur, on aime se représenter la mémoire comme un espace d’adressage continu. Plus on travaille dans un langage de haut niveau, plus cela est vrai. Par exemple, les problèmes d’alignement sont une notion totalement inconnue pour beaucoup. Cependant, dans ce monde idéal, la réalité du matériel refait surface périodiquement. Un processeur étant [beaucoup plus rapide](http://www.eecs.berkeley.edu/~rcs/research/interactive_latency.html) que la mémoire vive et le [principe de localité](http://en.wikipedia.org/wiki/Locality_of_reference) ayant été découvert, il dispose de [caches mémoire](http://en.wikipedia.org/wiki/CPU_cache). Ce sont des petits morceaux de mémoire tampon travaillant à une vitesse beaucoup plus proche de celle du processeur. Le pari étant qu’une fois l’accès coûteux à la mémoire centrale effectué, cette valeur va être réutilisée et on ira alors la chercher dans ce cache en gagnant énormément de temps. Le problème du _false sharing_ vient de deux choses : - L’architecture des caches. Un cache est composé d’un certain nombre de lignes de taille fixe (64 octets, par exemple). Lorsqu’une modification est faite, elle affecte la ligne entière. - Le processeur doit gérer la cohérence entre ces caches à l’aide d’un [protocole de cohérence](http://en.wikipedia.org/wiki/Cache_coherence). Il s’agit de s’assurer que lorsqu’un processeur ou un cœur fait une modification, elle sera visible par les autres. Chaque architecture a son propre modèle de cohérence, le x86 étant, par exemple, particulièrement fort. Ce modèle est exposé dans les langages de bas niveau, mais les langages de haut niveau décrivent souvent leur propre modèle mémoire. Charge au compilateur de traduire celui du langage vers celui de la plate‐forme. Si l’on met ces deux choses ensemble, on arrive au _false sharing_ : deux variables théoriquement indépendantes se retrouvent sur la même ligne de cache. Chacune est lue ou modifiée par un processeur distinct, cependant les processeurs doivent passer leur temps à se synchroniser, et les performances s’écroulent. Bref, notre bel espace mémoire uniforme vient d’en prendre un coup. Coller ou espacer deux variables peut faire varier d’un à plusieurs ordres de grandeur la performance de notre structure de données. ## Exemple Commençons avec un banc d’essai très simple : une classe avec un seul membre et quatre fils d’exécution (_threads_). Le premier lit en permanence la valeur de ce membre, les trois autres ne font rien. Le banc d’essai est écrit avec [JMH](http://openjdk.java.net/projects/code-tools/jmh/) : ```java @State(Scope.Benchmark) public static class StateNoFalseSharing { public int readOnly; } @GenerateMicroBenchmark @Group("noFalseSharing") public int reader(StateNoFalseSharing s) { return s.readOnly; } @GenerateMicroBenchmark @Group("noFalseSharing") public void noOp(StateNoFalseSharing s) { } ``` Ce qui nous donne le résultat suivant : ``` Benchmark Mode Samples Mean Mean error Units g.c.Benchmarks.noFalseSharing:noOp avgt 18 0.297 0.002 ns/op g.c.Benchmarks.noFalseSharing:reader avgt 18 0.743 0.003 ns/op ``` Comme on pouvait s’y attendre, c’est très rapide, et on aura du mal à mesurer quelque chose de plus petit. Maintenant, faisons évoluer notre banc d’essai. Nous ajoutons un deuxième membre qui va être accédé par les trois fils d’exécution qui ne faisaient rien. Le premier fil ne change absolument pas et, si les caches n’étaient pas organisés en ligne, il n’y aurait aucune raison que sa performance soit affectée. ```java @State(Scope.Group) public static class StateFalseSharing { int readOnly; volatile int writeOnly; } @GenerateMicroBenchmark @Group("falseSharing") public int reader(StateFalseSharing s) { return s.readOnly; } @GenerateMicroBenchmark @Group("falseSharing") public int writer(StateFalseSharing s) { return s.writeOnly++; } ``` Regardons les résultats : ``` Benchmark Mode Samples Mean Mean error Units g.c.Benchmarks.falseSharing:reader avgt 18 5.038 0.617 ns/op g.c.Benchmarks.falseSharing:writer avgt 18 78.530 3.598 ns/op ``` On vient presque de gagner un facteur 10. Nous pouvons vérifier l’agencement mémoire de notre objet `StateBaseline` avec [_jol_](http://openjdk.java.net/projects/code-tools/jol/), pour voir que nos deux variables ont bien été juxtaposées par le compilateur : ``` gist.contended.Benchmarks.StateFalseSharing object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int StateFalseSharing.readOnly N/A 16 4 int StateFalseSharing.writeOnly N/A 20 4 (loss due to the next object alignment) Instance size: 24 bytes (estimated, the sample instance is not available) Space losses: 0 bytes internal + 4 bytes external = 4 bytes total ``` Sans rentrer dans les détails, statistiquement, il y a de fortes chances qu’elles se retrouvent dans la même ligne de cache. ## @Contended La solution à notre problème est donc simplement d’espacer ces deux variables, quitte à perdre de l’espace. Ça paraît simple, mais, avant OpenJDK 8, cela demandait de très sérieusement connaître la machine virtuelle Java (ou lutter contre elle). Fort du principe de localité, le comportement logique de la machine virtuelle Java est d’essayer d’entasser autant que possible les différents membres comme bon lui semble. La disposition (_layout_) d’un objet peut changer selon beaucoup de critères. Et l’utilisation d’un [ramasse‐miettes](http://fr.wikipedia.org/wiki/Ramasse-miettes_%28informatique%29) n’aide pas, puisque ce dernier peut décider de déplacer un peu tout et n’importe quoi (notamment les tableaux utilisés pour les aligner). Bref, trouver une stratégie qui marche, est une source d’amusement inépuisable. Aleksey Shipilёv en a documenté quelques‐unes dans un [banc d’essai JMH](http://hg.openjdk.java.net/code-tools/jmh/file/3b57fecd61bf/jmh-samples/src/main/java/org/openjdk/jmh/samples/JMHSample_22_FalseSharing.java), de même que [Martin Thompson](http://mechanical-sympathy.blogspot.fr/2011/08/false-sharing-java-7.html). La JEP 142 propose d’ajouter une annotation `@Contended` pour identifier les variables, ou classes, qui doivent se retrouver seules sur une ligne de cache pour éviter le _false sharing_. Essayons de l’utiliser : ```java @State(Scope.Group) public static class StateContended { int readOnly; @Contended volatile int writeOnly; } @GenerateMicroBenchmark @Group("contented") public int reader(StateContended s) { return s.readOnly; } @GenerateMicroBenchmark @Group("contented") public int writer(StateContended s) { return s.writeOnly++; } ``` Vérifions avec _jol_ puis avec JMH : ``` gist.contended.Benchmarks.StateContended object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int StateContended.readOnly N/A 16 128 (alignment/padding gap) N/A 144 4 int StateContended.writeOnly N/A 148 4 (loss due to the next object alignment) Instance size: 152 bytes (estimated, the sample instance is not available) Space losses: 128 bytes internal + 4 bytes external = 132 bytes total Benchmark Mode Samples Mean Mean error Units g.c.Benchmarks.contented:reader avgt 18 0.742 0.006 ns/op g.c.Benchmarks.contented:writer avgt 18 70.811 3.572 ns/op ``` On observe que la variable a bien été décalée, et l’on retrouve les performances initiales. ## Limitations `@Contended` est une JEP d’OpenJDK, c’est‐à‐dire qu’il ne s’agit pas d’une spécification de Java ou de la machine virtuelle Java (JVM). L’annotation se trouve dans un paquet privé d’Oracle, et elle n’est disponible que pour les classes du kit de développement Java (JDK) par défaut (comme beaucoup de choses que le JDK se réserve précieusement). Si l’on veut l’utiliser, et donc se lier à OpenJDK, il faut passer l’option `-XX:-RestrictContended`. Bien entendu, vu l’impact sur la consommation mémoire et la possibilité de réduire l’efficacité du cache, il faut bien savoir ce qu’on fait et l’utiliser avec parcimonie. ## Comment détecter un cas de false sharing Notre exemple était très simple et nous connaissions le problème. Malheureusement, dans la vraie vie, ce n’est pas aussi évident, et il n’existe pas à ma connaissance d’outil simple permettant de détecter le _false sharing_, quel que soit le langage. On peut suivre les conseils d’[Intel](http://software.intel.com/en-us/articles/avoiding-and-identifying-false-sharing-among-threads) et les appliquer avec l’outil qu’ils fournissent ou avec [_perf_](http://blog.gmane.org/gmane.comp.linux.papi/month=20120201), mais ça reste assez empirique. Si l’on garde le principe du _false sharing_ en tête, cela permet de surveiller les mauvaises pratiques dans les bouts d’infrastructure qui peuvent être affectés. En général, il faut que ça commence à aller sérieusement vite, donc avec des structures de données dédiées, pour que ça commence à devenir un problème. ## Cas courants Identiquement à notre exemple, un même objet possède deux membres qui sont utilisés par deux fils d’exécution différents. ~~Ça arrive, par exemple, quand un objet tient des statistiques.~~ Dans ce cas, on va annoter le membre avec `@Contended`. On peut avoir aussi le cas de plusieurs instances d’une même classe qui préféreraient chacune être dans leur propre ligne de cache. Dans ce cas, on va annoter la classe. Cela fonctionne aussi si l’on met les instances dans un tableau ; cas courant, lorsque l’on fait travailler plusieurs fils d’exécution en parallèle. Le dernier cas est le calcul de type matriciel avec plusieurs fils d’exécution. Dans ce cas, l’annotation ne peut rien et il faut concevoir son algorithme pour en tenir compte (tout comme on itère dans le bon sens). [Dr doobs](http://www.drdobbs.com/parallel/eliminate-false-sharing/217500206) fournit un tel exemple. J’ai essayé de fournir quelques exemples dans le [banc d’essai](https://gist.github.com/cykl/9924619#file-src_main_java_gist_contended_benchmarks-java). ## Conclusion `@Contended` ne devrait pas changer la vie de grand monde, hormis celle des gens qui pondent l’infrastructure de service à haute performance. Mais, elle ouvre la porte à une revendication de longue date : marier les bénéfices de la machine virtuelle Java avec les besoins des applications haute performance, en ouvrant l’accès au matériel et à des techniques contre l’esprit initial de Java, mais nécessaires. Cette annotation ne répond absolument pas au besoin de pouvoir contrôler l’agencement d’un objet ou de choisir quels membres d’un objet doivent être regroupés. Elle ne résout pas non plus les problèmes d’indirection dus aux références pour les objets. Mais la pression monte doucement avec le nombre d’applications qui passent en _off‐the‐heap_ ou en _mmap_ lorsque nécessaire. Enfin, le _false sharing_ n’est pas le seul problème lié aux caches, il y a d’[autres exemples](http://danluu.com/3c-conflict/). Et, bien entendu, exploiter les caches correctement a un impact certain sur les performances d’une application.