Pourquoi SsortingngBuilder est-il plus lent que SsortingngBuffer?

Dans cet exemple , SsortingngBuffer est en réalité plus rapide que SsortingngBuilder, alors que je m’attendais à des résultats opposés.

Est-ce que cela a quelque chose à voir avec les optimisations faites par le JIT? Est-ce que quelqu’un sait pourquoi SsortingngBuffer serait plus rapide que SsortingngBuilder, même si ses méthodes sont synchronisées?

Voici le code et les résultats de référence:

public class SsortingngOps { public static void main(Ssortingng args[]) { long sConcatStart = System.nanoTime(); Ssortingng s = ""; for(int i=0; i<1000; i++) { s += String.valueOf(i); } long sConcatEnd = System.nanoTime(); long sBuffStart = System.nanoTime(); StringBuffer buff = new StringBuffer(); for(int i=0; i<1000; i++) { buff.append(i); } long sBuffEnd = System.nanoTime(); long sBuilderStart = System.nanoTime(); StringBuilder builder = new StringBuilder(); for(int i=0; i<1000; i++) { builder.append(i); } long sBuilderEnd = System.nanoTime(); System.out.println("Using + operator : " + (sConcatEnd-sConcatStart) + "ns"); System.out.println("Using StringBuffer : " + (sBuffEnd-sBuffStart) + "ns"); System.out.println("Using StringBuilder : " + (sBuilderEnd-sBuilderStart) + "ns"); System.out.println("Diff '+'/Buff = " + (double)(sConcatEnd-sConcatStart)/(sBuffEnd-sBuffStart)); System.out.println("Diff Buff/Builder = " + (double)(sBuffEnd-sBuffStart)/(sBuilderEnd-sBuilderStart)); } } 

Résultats de référence:

 Using + operator : 17199609ns Using SsortingngBuffer : 244054ns Using SsortingngBuilder : 4351242ns Diff '+'/Buff = 70.47460398108615 Diff Buff/Builder = 0.056088353624091696 

METTRE À JOUR:

Merci à tout le monde. Warmup était en effet le problème. Une fois que du code de mise à jour a été ajouté, les points de repère ont été modifiés comme suit:

 Using + operator : 8782460ns Using SsortingngBuffer : 343375ns Using SsortingngBuilder : 211171ns Diff '+'/Buff = 25.576876592646524 Diff Buff/Builder = 1.6260518726529685 

YMMV, mais au moins les ratios globaux correspondent à ce à quoi on pourrait s’attendre

J’ai jeté un œil à votre code et la raison la plus probable pour laquelle SsortingngBuilder semble être plus lent est que votre sharepoint repère ne prend pas correctement en compte les effets de l’échauffement de la JVM. Dans ce cas:

  • le démarrage de la machine virtuelle Java génère une quantité non négligeable de déchets à traiter, et
  • La compilation JIT peut démarrer en cours de partie.

L’un ou l’autre ou les deux pourraient append au temps mesuré pour la partie SsortingngBuilder de votre test.

Veuillez lire les réponses à cette question pour plus de détails: Comment écrire un micro-benchmark correct en Java?

Le même code, provenant de java.lang.AbstractSsortingngBuilder , est utilisé dans les deux cas et les deux instances sont créées avec la même capacité (16).

La seule différence est l’utilisation de synchronized lors de l’appel initial.

Je conclus que ceci est un artefact de mesure.

SsortingngBuilder:

 228 public SsortingngBuilder append(int i) { 229 super.append(i); 230 return this; 231 } 

SsortingngBuffer:

 345 public synchronized SsortingngBuffer append(int i) { 346 super.append(i); 347 return this; 348 } 

AbstractSsortingngBuilder:

 605 public AbstractSsortingngBuilder append(int i) { 606 if (i == Integer.MIN_VALUE) { 607 append("-2147483648"); 608 return this; 609 } 610 int appendedLength = (i < 0) ? Integer.stringSize(-i) + 1 611 : Integer.stringSize(i); 612 int spaceNeeded = count + appendedLength; 613 if (spaceNeeded > value.length) 614 expandCapacity(spaceNeeded); 615 Integer.getChars(i, spaceNeeded, value); 616 count = spaceNeeded; 617 return this; 618 } 110 void expandCapacity(int minimumCapacity) { 111 int newCapacity = (value.length + 1) * 2; 112 if (newCapacity < 0) { 113 newCapacity = Integer.MAX_VALUE; 114 } else if (minimumCapacity > newCapacity) { 115 newCapacity = minimumCapacity; 116 } 117 value = Arrays.copyOf(value, newCapacity); 118 } 

(expandCapacity n’est pas surchargé)

Ce billet de blog en dit plus sur:

  • la difficulté qu’il y a dans le micro-benchmarking
  • le fait que vous ne devriez pas publier les “résultats” d’un repère sans regarder un peu ce que vous avez mesuré (ici la superclasse commune)

Notez que la “lenteur” de la synchronisation dans le JDK récent peut être considérée comme un mythe. Tous les tests que j’ai faits ou lus concluent qu’il n’y a généralement aucune raison de perdre beaucoup de temps à éviter les synchronisations.

Lorsque vous exécutez ce code sur vous-même, vous constaterez un résultat variable. Parfois, SsortingngBuffer est plus rapide et parfois, SsortingngBuilder est plus rapide. La raison probable en est peut-être le temps nécessaire au JVM warmup avant d’utiliser SsortingngBuffer et SsortingngBuilder comme indiqué par @Stephen, qui peut varier en plusieurs exécutions.

C’est le résultat de 4 courses que j’ai faites: –

 Using SsortingngBuffer : 398445ns Using SsortingngBuilder : 272800ns Using SsortingngBuffer : 411155ns Using SsortingngBuilder : 281600ns Using SsortingngBuffer : 386711ns Using SsortingngBuilder : 662933ns Using SsortingngBuffer : 413600ns Using SsortingngBuilder : 270356ns 

Bien sûr, les chiffres exacts ne peuvent être prédits sur la base de seulement 4 exécutions.

je suggère

  • briser chaque boucle en une méthode séparée afin que l’optimisation de l’une n’impacte pas l’autre.
  • ignorer les premières itérations de 10K
  • Exécutez le test pendant au moins 2 secondes.
  • Exécutez le test plusieurs fois pour vous assurer qu’il est reproductible.

Lorsque vous exécutez du code moins de 10000 fois, il risque de ne pas être compilé avec le code par défaut -XX:ComstackThreshold=10000 . C’est en partie pour recueillir des statistiques sur la meilleure façon d’optimiser le code. Cependant, lorsqu’une boucle déclenche la compilation, elle la déclenche pour l’ ensemble de la méthode, ce qui permet de rendre les boucles ultérieures soit a) meilleures, car elles sont compilées avant de commencer b) à mesure qu’elles sont compilées sans aucune statistique.


Considérons le code suivant

 public static void main(Ssortingng... args) { int runs = 1000; for (int i = 0; i < runs; i++) String.valueOf(i); System.out.printf("%-10s%-10s%-10s%-9s%-9s%n", "+ oper", "SBuffer", "SBuilder", "+/Buff", "Buff/Builder"); for (int t = 0; t < 5; t++) { long sConcatTime = timeStringConcat(runs); long sBuffTime = timeStringBuffer(runs); long sBuilderTime = timeStringBuilder(runs); System.out.printf("%,7dns %,7dns %,7dns ", sConcatTime / runs, sBuffTime / runs, sBuilderTime / runs); System.out.printf("%8.2f %8.2f%n", (double) sConcatTime / sBuffTime, (double) sBuffTime / sBuilderTime); } } public static double dontOptimiseAway = 0; private static long timeStringConcat(int runs) { long sConcatStart = System.nanoTime(); for (int j = 0; j < 100; j++) { String s = ""; for (int i = 0; i < runs; i += 100) { s += String.valueOf(i); } dontOptimiseAway = Double.parseDouble(s); } return System.nanoTime() - sConcatStart; } private static long timeStringBuffer(int runs) { long sBuffStart = System.nanoTime(); for (int j = 0; j < 100; j++) { StringBuffer buff = new StringBuffer(); for (int i = 0; i < runs; i += 100) buff.append(i); dontOptimiseAway = Double.parseDouble(buff.toString()); } return System.nanoTime() - sBuffStart; } private static long timeStringBuilder(int runs) { long sBuilderStart = System.nanoTime(); for (int j = 0; j < 100; j++) { StringBuilder buff = new StringBuilder(); for (int i = 0; i < runs; i += 100) buff.append(i); dontOptimiseAway = Double.parseDouble(buff.toString()); } return System.nanoTime() - sBuilderStart; } 

impressions avec des courses = 1000

 + oper SBuffer SBuilder +/Buff Buff/Builder 6,848ns 3,169ns 3,287ns 2.16 0.96 6,039ns 2,937ns 3,311ns 2.06 0.89 6,025ns 3,315ns 2,276ns 1.82 1.46 4,718ns 2,254ns 2,180ns 2.09 1.03 5,183ns 2,319ns 2,186ns 2.23 1.06 

Cependant, si vous augmentez le nombre de passages = 10 000

 + oper SBuffer SBuilder +/Buff Buff/Builder 3,791ns 400ns 357ns 9.46 1.12 1,426ns 139ns 113ns 10.23 1.23 323ns 141ns 117ns 2.29 1.20 317ns 115ns 78ns 2.76 1.47 317ns 127ns 103ns 2.49 1.23 

et si nous augmentons les courses à 100 000 je reçois

 + oper SBuffer SBuilder +/Buff Buff/Builder 3,946ns 195ns 128ns 20.23 1.52 2,364ns 113ns 86ns 20.80 1.32 2,189ns 142ns 95ns 15.34 1.49 2,036ns 142ns 96ns 14.31 1.48 2,566ns 114ns 88ns 22.46 1.29 

Remarque: L'opération + a ralenti car la complexité temporelle de la boucle est O (N ^ 2).

J’ai légèrement modifié votre code et ajouté les boucles d’échauffement. Mes observations sont cohérentes la plupart du temps, SsortingngBuilder est plus rapide la plupart du temps.

Je suis sous Ubuntu12.04 qui tourne virtuellement sur Windows 7 et dispose de 2 Go de RAM alloués à la machine virtuelle.

 public class SsortingngOps { public static void main(Ssortingng args[]) { for(int j=0;j<10;j++){ StringBuffer buff = new StringBuffer(); for(int i=0; i<1000; i++) { buff.append(i); } buff = new StringBuffer(); long sBuffStart = System.nanoTime(); for(int i=0; i<10000; i++) { buff.append(i); } long sBuffEnd = System.nanoTime(); StringBuilder builder = new StringBuilder(); for(int i=0; i<1000; i++) { builder.append(i); } builder = new StringBuilder(); long sBuilderStart = System.nanoTime(); for(int i=0; i<10000; i++) { builder.append(i); } long sBuilderEnd = System.nanoTime(); if((sBuffEnd-sBuffStart)>(sBuilderEnd-sBuilderStart)) { System.out.println("Ssortingng Builder is faster") ; } else { System.out.println("Ssortingng Buffer is faster") ; } } } 

}

Les résultats sont:

 Ssortingng Builder is faster Ssortingng Builder is faster Ssortingng Builder is faster Ssortingng Builder is faster Ssortingng Buffer is faster Ssortingng Builder is faster Ssortingng Builder is faster Ssortingng Builder is faster Ssortingng Builder is faster Ssortingng Builder is faster