En enquêtant pour un petit débat w.r.t. en utilisant "" + n
et Integer.toString(int)
pour convertir une primitive entière en une chaîne, j'ai écrit ceci 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 exécuté avec les options JMH par défaut avec les deux Java qui existent sur ma machine Linux (Mageia 4 64 bits à jour, processeur Intel i7-3770, 32 Go de RAM). la première JVM était 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 machine virtuelle Java, j'ai obtenu à 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
C'est à dire. l'utilisation de la classe StringBuilder
est plus lente en raison de la surcharge supplémentaire liée à la création de l'objet StringBuilder
et à l'ajout d'une chaîne vide. L'utilisation de String.format(String, ...)
est encore plus lente, d'un ordre de grandeur environ.
Le compilateur fourni par la 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éressants:
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 beaucoup plus rapidement avec cette machine virtuelle Java? 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)
. Il est intéressant de noter que l'ajout du résultat de Integer.toString(int)
(la microbenchmark stringBuilder2
) Ne semble pas être plus rapide.
Cette différence de performances est-elle un problème avec le faisceau de test? Ou est-ce que ma machine virtuelle Java OpenJDK contient des optimisations qui affecteraient ce code (anti) modèle particulier?
MODIFIER:
Pour une comparaison plus simple, 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 Java 7 vs Java 8 problème. Peut-être Java 7 avait des optimisations de chaînes plus agressives) ?
EDIT 2 :
Pour être complet, voici les options liées à la chaîne VM pour ces deux machines virtuelles Java:
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 remplacement, donc je doute que cela fasse une différence. Les autres options semblent avoir les mêmes paramètres.
EDIT 3:
Une comparaison côte à côte du code source des classes AbstractStringBuilder
, StringBuilder
et Integer
du fichier src.Zip
De ne révèle rien de remarquable. Mis à part de nombreux changements cosmétiques et de documentation, Integer
prend désormais en charge les entiers non signés et StringBuilder
a été légèrement refactorisé pour partager plus de code avec StringBuffer
. Aucune de ces modifications ne semble affecter les chemins de code utilisés par StringBuilder#append(int)
, même si j'ai peut-être manqué quelque chose.
Une comparaison du code Assembly 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 machines virtuelles Java, bien qu'Oracle JDK 8u5 semble être plus agressif avec w.r.t. en insérant certains appels dans le code Integer#toString(int)
. Il y avait une correspondance claire avec le code source Java, même pour quelqu'un avec une expérience d'assemblage minimale.
Le code d'assembly pour IntStr#stringBuilder0()
, cependant, était radicalement différent. Le code généré par Oracle JDK 8u5 était encore une fois directement lié au code source Java - je pouvais facilement reconnaître la même disposition. Au contraire, le code généré par OpenJDK 7 était presque méconnaissable par le oeil inexpérimenté (comme le mien). L'appel new StringBuilder()
a apparemment été supprimé, tout comme la création du tableau dans le constructeur StringBuilder
. De plus, le plugin désassembleur n'a pas pu fournir autant de références au code source comme il l'a fait dans JDK 8.
Je suppose que c'est soit le résultat d'une passe d'optimisation beaucoup plus agressive dans OpenJDK 7, soit plus probablement le résultat de l'insertion de code bas niveau manuscrit pour certaines opérations StringBuilder
. Je ne sais pas pourquoi cette optimisation ne se produit pas dans mon implémentation JVM 8 ou pourquoi les mêmes optimisations n'ont pas été implémentées pour Integer#toString(int)
dans JVM 7. Je suppose qu'une personne familière avec les parties connexes du code source JRE devrait répondez à ces questions...
TL; DR: Les effets secondaires dans append
cassent apparemment les optimisations de StringConcat.
Très bonne analyse dans la question d'origine et les 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 remarquerez peut-être que la version 7u55 est moins profonde, et il semble que rien ne soit appelé après les méthodes StringBuilder
- c'est une bonne indication que les optimisations de chaîne sont en vigueur. En effet, si vous exécutez 7u55 avec -XX:-OptimizeStringConcat
, les sous-appels réapparaîtront et les performances chuteront à 8u5.
OK, nous devons donc comprendre 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 permettra vous mettre dans src/share/vm/opto/stringopts.cpp
hg log src/share/vm/opto/stringopts.cpp
pour y découvrir les derniers changements. L'un des candidats serait:
changeset: 5493:90abdd727e64 user: iveresov date: Wed Oct 16 11:13:15 2013 -0700 summary: 8009303: Tiered: incorrect results in VM tests stringconcat...
Recherchez les fils de lecture sur les listes de diffusion OpenJDK (assez facile pour google pour un résumé des modifications): http://mail.openjdk.Java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html
L'optimisation d'optimisation de concaténation "String" réduit le modèle [...] en une seule allocation d'une chaîne et forme directement le résultat. Toutes les déoptions possibles qui peuvent se produire dans le code optimisé redémarrent ce modèle depuis le début (à partir de l'allocation StringBuffer) Cela signifie que tout le motif doit être libre d'effets secondaires. "Eureka?
Écrivez la référence contrastée:
@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(); } }
Mesurez-le sur JDK 7u55, en voyant les mêmes performances pour les effets secondaires intégrés/épissés:
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
Mesurez-le sur JDK 8u5, en voyant la dégradation des performances avec l'effet intégré:
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
Soumettez le rapport de bogue ( https://bugs.openjdk.Java.net/browse/JDK-8043677 ) pour discuter de ce comportement avec VM les gars. La justification de le correctif d'origine est solide, il est cependant intéressant si nous pouvons/devons récupérer cette optimisation dans certains cas triviaux comme ceux-ci.
???
PROFIT.
Et oui, je devrais poster les résultats pour le benchmark qui déplace l'incrément de la chaîne StringBuilder
, en le faisant avant toute la chaîne. Aussi, passé en temps moyen et ns/op. Voici 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 c'est 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. Cela solidifie l'hypothèse de la rupture d'effets secondaires dans les chaînes SB chez le principal coupable de la question d'origine.
Je pense que cela a à voir avec le drapeau CompileThreshold
qui contrôle quand le code d'octet est compilé en code machine par JIT.
Le JDK Oracle 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 n'ai pas pu trouver un dernier document sur ce drapeau; mais certains fils de discussion suggèrent un seuil beaucoup plus bas: http://mail.openjdk.Java.net/pipermail/hotspot-compiler-dev/2010-November/004239.html
Essayez également d'activer/désactiver les indicateurs Oracle JDK comme -XX:+UseCompressedStrings
et -XX:+OptimizeStringConcat
. Je ne sais pas si ces drapeaux sont activés par défaut sur OpenJDK. Quelqu'un pourrait-il suggérer.
Une expérience que vous pouvez faire consiste à exécuter le programme plusieurs fois, disons 30 000 boucles, à faire un System.gc (), puis à essayer de regarder les performances. Je pense qu'ils donneraient la même chose.
Et je suppose que votre paramètre GC est le même aussi. Sinon, vous allouez beaucoup d'objets et le GC pourrait bien être la majeure partie de votre temps d'exécution.