Pourquoi StringBuilder#append (int) est-il plus rapide en Java 7 qu'en Java 8?
While investigating for a little debate W. R. T. en utilisant "" + n
et Integer.toString(int)
pour convertir un entier primitif en chaîne, j'ai écrit ce JMH microbenchmark:
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class IntStr {
protected int counter;
@GenerateMicroBenchmark
public String integerToString() {
return Integer.toString(this.counter++);
}
@GenerateMicroBenchmark
public String stringBuilder0() {
return new StringBuilder().append(this.counter++).toString();
}
@GenerateMicroBenchmark
public String stringBuilder1() {
return new StringBuilder().append("").append(this.counter++).toString();
}
@GenerateMicroBenchmark
public String stringBuilder2() {
return new StringBuilder().append("").append(Integer.toString(this.counter++)).toString();
}
@GenerateMicroBenchmark
public String stringFormat() {
return String.format("%d", this.counter++);
}
@Setup(Level.Iteration)
public void prepareIteration() {
this.counter = 0;
}
}
Je l'ai lancé avec les options JMH par défaut avec les deux VM Java qui existent sur ma machine Linux (Mageia à jour 64 bits, Intel I7-3770 CPU, 32 Go RAM). La première JVM a été celle fournie avec Oracle JDK 8u5 64 bits:
java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)
avec cette JVM j'ai eu à peu près ce que j'attendais:
Benchmark Mode Samples Mean Mean error Units
b.IntStr.integerToString thrpt 20 32317.048 698.703 ops/ms
b.IntStr.stringBuilder0 thrpt 20 28129.499 421.520 ops/ms
b.IntStr.stringBuilder1 thrpt 20 28106.692 1117.958 ops/ms
b.IntStr.stringBuilder2 thrpt 20 20066.939 1052.937 ops/ms
b.IntStr.stringFormat thrpt 20 2346.452 37.422 ops/ms
i. e. l'utilisation de la classe StringBuilder
est plus lente en raison des frais généraux supplémentaires liés à la création de l'objet StringBuilder
et à l'ajout d'une chaîne de caractères vide. L'utilisation de String.format(String, ...)
est encore plus lente, d'un ordre de grandeur.
le compilateur fourni par distribution, d'autre part, est basé sur OpenJDK 1.7:
java version "1.7.0_55"
OpenJDK Runtime Environment (mageia-2.4.7.1.mga4-x86_64 u55-b13)
OpenJDK 64-Bit Server VM (build 24.51-b03, mixed mode)
les résultats ici étaient intéressant :
Benchmark Mode Samples Mean Mean error Units
b.IntStr.integerToString thrpt 20 31249.306 881.125 ops/ms
b.IntStr.stringBuilder0 thrpt 20 39486.857 663.766 ops/ms
b.IntStr.stringBuilder1 thrpt 20 41072.058 484.353 ops/ms
b.IntStr.stringBuilder2 thrpt 20 20513.913 466.130 ops/ms
b.IntStr.stringFormat thrpt 20 2068.471 44.964 ops/ms
pourquoi StringBuilder.append(int)
apparaît-il tellement plus vite avec cette JVM? L'examen du code source de la classe StringBuilder
n'a rien révélé de particulièrement intéressant - la méthode en question est presque identique à Integer#toString(int)
. Chose intéressante, ajouter le résultat de Integer.toString(int)
(le stringBuilder2
microbenchmark) ne semble pas être plus rapide.
cet écart de rendement pose-t-il un problème avec le harnais d'essai? Ou est-ce que mon OpenJDK JVM contient des optimisations qui affecteraient ce code (anti) - pattern particulier?
EDIT:
pour une comparaison plus directe, J'ai installé Oracle JDK 1.7u55:
java version "1.7.0_55"
Java(TM) SE Runtime Environment (build 1.7.0_55-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)
les résultats sont similaires à ceux D'OpenJDK:
Benchmark Mode Samples Mean Mean error Units
b.IntStr.integerToString thrpt 20 32502.493 501.928 ops/ms
b.IntStr.stringBuilder0 thrpt 20 39592.174 428.967 ops/ms
b.IntStr.stringBuilder1 thrpt 20 40978.633 544.236 ops/ms
il semble que ce soit un problème plus général de Java 7 par rapport à Java 8. Peut-être que Java 7 avait des optimisations de chaînes plus agressives?
EDIT 2 :
pour plus d'exhaustivité, voici les options VM liées aux chaînes de caractères pour ces deux JVM:
pour Oracle JDK 8u5:
$ /usr/java/default/bin/java -XX:+PrintFlagsFinal 2>/dev/null | grep String
bool OptimizeStringConcat = true {C2 product}
intx PerfMaxStringConstLength = 1024 {product}
bool PrintStringTableStatistics = false {product}
uintx StringTableSize = 60013 {product}
Pour OpenJDK 1.7:
$ java -XX:+PrintFlagsFinal 2>/dev/null | grep String
bool OptimizeStringConcat = true {C2 product}
intx PerfMaxStringConstLength = 1024 {product}
bool PrintStringTableStatistics = false {product}
uintx StringTableSize = 60013 {product}
bool UseStringCache = false {product}
l'option UseStringCache
a été supprimée dans Java 8 sans aucun remplacement, donc I le doute qui fait toute la différence. Les autres options semblent avoir les mêmes paramètres.
EDIT 3:
une comparaison side-by-side du code source des classes AbstractStringBuilder
, StringBuilder
et Integer
du fichier src.zip
ne révèle rien de noteworty. En dehors d'un grand nombre de changements cosmétiques et de la documentation, Integer
a maintenant un certain soutien pour les entiers non signés et StringBuilder
a a été légèrement remanié pour partager plus de code avec StringBuffer
. Aucun de ces changements ne semble affecter les chemins de code utilisés par StringBuilder#append(int)
, bien que j'ai pu manquer quelque chose.
une comparaison du code d'assemblage généré pour IntStr#integerToString()
et IntStr#stringBuilder0()
est beaucoup plus intéressante. La disposition de base du code généré pour IntStr#integerToString()
était similaire pour les deux JVM, bien que Oracle JDK 8u5 semble être plus agressif W. R. T. en ligne certains appels à l'intérieur de la Integer#toString(int)
du code. Il y avait une correspondance claire avec le code source Java, même pour quelqu'un avec une expérience minimale d'assemblage.
le code d'assemblage pour IntStr#stringBuilder0()
était radicalement différent. Le code généré par Oracle JDK 8u5 était à nouveau directement lié au code source Java - je pouvais facilement reconnaître la même mise en page. Au contraire, le code généré par OpenJDK 7 était presque méconnaissable à l'œil non formé (comme le mien). Le new StringBuilder()
l'appel a été apparemment supprimé, tout comme la création du tableau dans le constructeur StringBuilder
. De plus, le plugin de démontage n'a pas pu fournir autant de références au code source que dans JDK 8.
je suppose que c'est soit le résultat d'un passage d'optimisation beaucoup plus agressif dans OpenJDK 7, soit plus probablement le résultat de l'insertion de code de bas niveau écrit à la main pour certaines opérations StringBuilder
. Je ne sais pas pourquoi cette optimisation ne se produire dans ma mise en œuvre JVM 8 ou pourquoi les mêmes optimisations n'ont pas été mises en œuvre pour Integer#toString(int)
dans JVM 7. Je suppose que quelqu'un qui connaît les parties du code source de JRE devrait répondre à ces questions...
2 réponses
TL;DR: effets Secondaires append
apparemment pause StringConcat optimisations.
très bonne analyse dans la question originale et mises à jour!
pour être complet, voici quelques étapes manquantes:
-
voir à travers le
-XX:+PrintInlining
pour 7u55 et 8u5. En 7u55, vous verrez quelque chose comme ceci:@ 16 org.sample.IntStr::inlineSideEffect (25 bytes) force inline by CompilerOracle @ 4 java.lang.StringBuilder::<init> (7 bytes) inline (hot) @ 18 java.lang.StringBuilder::append (8 bytes) already compiled into a big method @ 21 java.lang.StringBuilder::toString (17 bytes) inline (hot)
...et en 8u5:
@ 16 org.sample.IntStr::inlineSideEffect (25 bytes) force inline by CompilerOracle @ 4 java.lang.StringBuilder::<init> (7 bytes) inline (hot) @ 3 java.lang.AbstractStringBuilder::<init> (12 bytes) inline (hot) @ 1 java.lang.Object::<init> (1 bytes) inline (hot) @ 18 java.lang.StringBuilder::append (8 bytes) inline (hot) @ 2 java.lang.AbstractStringBuilder::append (62 bytes) already compiled into a big method @ 21 java.lang.StringBuilder::toString (17 bytes) inline (hot) @ 13 java.lang.String::<init> (62 bytes) inline (hot) @ 1 java.lang.Object::<init> (1 bytes) inline (hot) @ 55 java.util.Arrays::copyOfRange (63 bytes) inline (hot) @ 54 java.lang.Math::min (11 bytes) (intrinsic) @ 57 java.lang.System::arraycopy (0 bytes) (intrinsic)
vous pourriez remarquer que la version 7u55 est moins profonde, et il semble que rien n'est appelé après les méthodes
StringBuilder
-- c'est une bonne indication que les optimisations des chaînes sont en vigueur. En effet , si vous exécutez 7u55 avec-XX:-OptimizeStringConcat
, les sous-appels réapparaîtront, et la performance descendra à 8u5 niveaux. -
OK, donc on doit trouver pourquoi 8u5 ne fait pas la même optimisation. Grep http://hg.openjdk.java.net/jdk9/jdk9/hotspot pour "StringBuilder" pour savoir où VM gère l'optimisation StringConcat; cela vous mènera dans
src/share/vm/opto/stringopts.cpp
-
hg log src/share/vm/opto/stringopts.cpp
pour comprendre les derniers changements. L'un des candidats:changeset: 5493:90abdd727e64 user: iveresov date: Wed Oct 16 11:13:15 2013 -0700 summary: 8009303: Tiered: incorrect results in VM tests stringconcat...
-
chercher passer en revue les threads sur les listes de diffusion OpenJDK (assez facile à google pour le résumé de changeset): http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html
-
Spot " String optimization concat optimization faillis the pattern [...] en une seule attribution d'une chaîne et formant le résultat directement. Tous les désopulations possibles qui peuvent se produire dans le code optimisé redémarrez Ce modèle dès le début (à partir de L'allocation StringBuffer). cela signifie que l'ensemble du schéma doit être exempt d'effets secondaires. " Eureka?
-
écrire le benchmark contrastant:
@Fork(5) @Warmup(iterations = 5) @Measurement(iterations = 5) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) public class IntStr { private int counter; @GenerateMicroBenchmark public String inlineSideEffect() { return new StringBuilder().append(counter++).toString(); } @GenerateMicroBenchmark public String spliceSideEffect() { int cnt = counter++; return new StringBuilder().append(cnt).toString(); } }
-
Mesurer sur JDK 7u55, de voir les mêmes performances pour inline/épissé effets secondaires:
Benchmark Mode Samples Mean Mean error Units o.s.IntStr.inlineSideEffect avgt 25 65.460 1.747 ns/op o.s.IntStr.spliceSideEffect avgt 25 64.414 1.323 ns/op
-
Mesurer sur JDK 8u5, en voyant la dégradation des performances avec les inline effet:
Benchmark Mode Samples Mean Mean error Units o.s.IntStr.inlineSideEffect avgt 25 84.953 2.274 ns/op o.s.IntStr.spliceSideEffect avgt 25 65.386 1.194 ns/op
-
soumettre le rapport de bogue ( https://bugs.openjdk.java.net/browse/JDK-8043677 ) pour discuter de ce comportement avec les VM guys. La raison d'être de la solution originale est solide comme la roche, il est intéressant cependant si nous pouvons / devrions récupérer cette optimisation dans certains cas triviaux comme ils.
-
???
-
PROFIT.
et oui, je devrais poster les résultats pour le benchmark qui déplace l'incrément de la chaîne StringBuilder
, le faisant avant toute la chaîne. Aussi, commuté à temps moyen, et ns/op. C'est JDK 7u55:
Benchmark Mode Samples Mean Mean error Units o.s.IntStr.integerToString avgt 25 153.805 1.093 ns/op o.s.IntStr.stringBuilder0 avgt 25 128.284 6.797 ns/op o.s.IntStr.stringBuilder1 avgt 25 131.524 3.116 ns/op o.s.IntStr.stringBuilder2 avgt 25 254.384 9.204 ns/op o.s.IntStr.stringFormat avgt 25 2302.501 103.032 ns/op
et ici 8u5:
Benchmark Mode Samples Mean Mean error Units o.s.IntStr.integerToString avgt 25 153.032 3.295 ns/op o.s.IntStr.stringBuilder0 avgt 25 127.796 1.158 ns/op o.s.IntStr.stringBuilder1 avgt 25 131.585 1.137 ns/op o.s.IntStr.stringBuilder2 avgt 25 250.980 2.773 ns/op o.s.IntStr.stringFormat avgt 25 2123.706 25.105 ns/op
stringFormat
est en fait un peu plus rapide en 8u5, et tous les autres tests sont les mêmes. Ceci consolide l'hypothèse de la rupture des effets secondaires dans les chaînes SB dans le principal coupable de la question originale.
je pense que cela a à voir avec le drapeau CompileThreshold
qui contrôle quand le code octet est compilé en code machine par JIT.
L'Oracle JDK a un nombre par défaut de 10 000 comme document à http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html .
où OpenJDK Je ne pouvais pas trouver un dernier document sur ce drapeau; mais certains threads de courrier suggèrent un seuil beaucoup plus bas: http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2010-November/004239.html
aussi, essayez d'allumer / éteindre les drapeaux Oracle JDK comme -XX:+UseCompressedStrings
et -XX:+OptimizeStringConcat
. Je ne suis pas sûr que ces options soient activées par défaut sur OpenJDK. Quelqu'un pourrait s'il vous plaît suggérer.
une expérience que vous pouvez faire, est d'abord d'exécuter le programme par un grand nombre de fois, disons, 30.000 boucles, faire un système.gc () et ensuite essayer de regarder la performance. Je je crois qu'ils donneraient la même chose.
et je suppose que votre configuration GC est la même. Sinon, vous attribuez beaucoup d'objets et le GC pourrait bien être la majeure partie de votre temps d'exécution.