Explication des performances: le code est plus rapide avec la variable non utilisée

Je faisais quelques tests de performance plus tôt et je ne peux pas expliquer les résultats que j’ai obtenus.

Lors de l’exécution du test ci-dessous, si je ne commente pas private final List list = new ArrayList(); listez private final List list = new ArrayList(); la performance s’améliore considérablement. Sur ma machine, le test s’exécute en 70-90 ms lorsque ce champ est présent, contre 650 ms lorsqu’il est commenté.

J’ai également remarqué que si je modifie l’instruction print en System.out.println((end - start) / 1000000); , le test sans la variable s’exécute dans 450-500 ms au lieu de 650 ms. Cela n’a aucun effet lorsque la variable est présente.

Mes questions:

  1. Quelqu’un peut-il expliquer le facteur de presque 10 avec ou sans la variable, étant donné que je n’utilise même pas cette variable?
  2. Comment cette instruction print peut-elle modifier les performances (en particulier puisqu’elle vient après la fenêtre de mesure des performances)?

ps: lorsqu’ils sont exécutés séquentiellement, les 3 scénarios (avec variable, sans variable, avec une instruction d’impression différente) prennent environ 260 ms.

 public class SOTest { private static final int ITERATIONS = 10000000; private static final int THREADS = 4; private volatile long id = 0L; //private final List list = new ArrayList(); public static void main(Ssortingng[] args) throws Exception { ExecutorService executor = Executors.newFixedThreadPool(THREADS); final List objects = new ArrayList(); for (int i = 0; i < THREADS; i++) { objects.add(new SOTest()); } //warm up for (SOTest t : objects) { getRunnable(t).run(); } long start = System.nanoTime(); for (SOTest t : objects) { executor.submit(getRunnable(t)); } executor.shutdown(); executor.awaitTermination(10, TimeUnit.SECONDS); long end = System.nanoTime(); System.out.println(objects.get(0).id + " " + (end - start) / 1000000); } public static Runnable getRunnable(final SOTest object) { Runnable r = new Runnable() { @Override public void run() { for (int i = 0; i < ITERATIONS; i++) { object.id++; } } }; return r; } } 

MODIFIER

Voir ci-dessous les résultats de 10 essais avec les 3 scénarios:

  • sans la variable, en utilisant l’instruction short print
  • sans la variable, en utilisant le long print statement (imprime un des objects)
  • exécution séquentielle (1 thread)
  • avec la variable
 1 657 473 261 74 2 641 501 261 78 3 651 465 259 86 4 585 462 259 78 5 639 506 259 68 6 659 477 258 72 7 653 479 259 82 8 645 486 259 72 9 650 457 259 78 10 639 487 272 79 

Effacer (faux) partage

en raison de la disposition dans la mémoire, les objects partagent des lignes de cache … Cela a été expliqué à maintes resockets (même sur ce site): voici une bonne source pour une lecture plus approfondie. Le problème est tout autant applicable à C # (ou C / C ++)

Lorsque vous complétez l’object en ajoutant la ligne commentée, le partage est moindre et la performance s’améliore.

Edit: j’ai raté la 2ème question:


Comment cette instruction print peut-elle modifier les performances (en particulier puisqu’elle vient après la fenêtre de mesure des performances)?

J’imagine que le réchauffement est insuffisant. Imprimez à la fois les journaux du GC et de la compilation afin de vous assurer qu’il n’y a pas d’interférence et que le code est réellement compilé. java -server besoin de 10k itérations de préférence pas toutes en boucle principale pour générer du bon code.

Vous frappez un effet subtil du matériel en cours d’exécution. Comme vos objects SOTest sont très petits en mémoire, les 4 instances peuvent tenir dans la même ligne de cache en mémoire. Étant donné que vous utilisez une unité volatile, cela entraînera un cache en cache entre différents cœurs (un seul cœur peut avoir la ligne de cache en mauvais état).

Lorsque vous commentez dans ArrayList, la disposition de la mémoire change (la liste ArrayList est créée entre deux instances SOTest) et les champs volatiles sont maintenant placés dans des lignes de cache différentes. Le problème pour le processeur disparaît, entraînant une montée en flèche des performances.

Preuve: commentez ArrayList et placez-le dans:

 long waste1, waste2, waste3, waste4, waste5, waste6, waste7, waste8; 

Ceci agrandit vos objects SOTest de 64 octets (la taille d’une ligne de cache sur les processeurs Pentium). Les performances sont maintenant les mêmes qu’avec ArrayList dans.

Ceci est juste une idée, et je ne sais pas comment la vérifier, mais cela pourrait être lié à la mise en cache. Avec la présentation ArrayList, vos objects deviennent beaucoup plus volumineux. Par conséquent, un nombre plus petit d’entre eux s’insère dans une zone de mémoire cache donnée, ce qui entraîne un plus grand nombre d’absence de mémoire cache.

Ce que vous pouvez réellement essayer, c’est d’utiliser des tableaux de taille différente, modifiant ainsi l’empreinte mémoire des instances de votre classe, et de voir si cela a un effet sur les performances.

Voyage assez intéressant. C’est plus un “voici ma réponse aux résultats”. Je soupçonne / espère que les autres proposeront de meilleures réponses.

Vous frappez évidemment des points d’optimisation intéressants. Je soupçonne que l’ajout de objects.get(0).id dans la longue instruction println supprime certaines optimisations relatives à l’utilisation du champ id . Mis à part le ++ il n’y a pas d’autre usage de id donc possible que l’optimiseur optimise un certain nombre d’access à l’ volatile id ce qui entraîne une amélioration de la vitesse. Il suffit d’accéder au champ id avec un long x = objects.get(0).id; provoque la même amélioration de performance.

Le champ List est beaucoup plus intéressant. La même amélioration de performance se produit si le champ private Ssortingng foo = new Ssortingng("weofjwe"); est ajouté mais pas s’il est private Ssortingng foo = "weofjwe"; ce qui ne crée pas d’object puisque le "..." est fait au moment de la compilation. J’étais sûr que la final était pertinente mais cela ne semble pas être le cas. Je ne peux que spéculer sur le fait que cela a quelque chose à voir avec les optimisations de constructeurs avec l’ajout de la new entraînant l’arrêt de l’optimisation bien que j’aurais pensé que volatile aurait pu le faire plus efficacement.