Während ich nach einer kleinen Debatte über die Verwendung "" + n
und Integer.toString(int)
Konvertierung eines ganzzahligen Grundelements in einen String suchte, schrieb ich dieses JMH- Mikrobenchmark:
@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;
}
}
Ich habe es mit den Standard-JMH-Optionen für beide Java-VMs ausgeführt, die auf meinem Linux-Computer vorhanden sind (aktuelle Mageia 4 64-Bit, Intel i7-3770-CPU, 32 GB RAM). Die erste JVM war die mit Oracle JDK 8u5 64-Bit gelieferte:
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)
Mit dieser JVM habe ich ziemlich genau das bekommen, was ich erwartet hatte:
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
Das heißt, die Verwendung der StringBuilder
Klasse ist langsamer, da das Erstellen des StringBuilder
Objekts und das Anhängen einer leeren Zeichenfolge zusätzlichen Aufwand bedeutet . Die Verwendung String.format(String, ...)
ist sogar um eine Größenordnung langsamer.
Der von der Distribution bereitgestellte Compiler basiert dagegen auf 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)
Die Ergebnisse hier waren interessant :
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
Warum StringBuilder.append(int)
erscheint diese JVM so viel schneller? Ein Blick auf den StringBuilder
Klassenquellcode ergab nichts besonders Interessantes - die fragliche Methode ist fast identisch mit Integer#toString(int)
. Interessanterweise scheint das Anhängen des Ergebnisses von Integer.toString(int)
(der stringBuilder2
Mikrobenchmark) nicht schneller zu sein.
Ist diese Leistungsdiskrepanz ein Problem mit dem Testkabel? Oder enthält meine OpenJDK-JVM Optimierungen, die sich auf dieses bestimmte Code- (Anti-) Muster auswirken würden?
BEARBEITEN:
Für einen einfacheren Vergleich habe ich Oracle JDK 1.7u55 installiert:
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)
Die Ergebnisse ähneln denen von 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
Es scheint, dass dies ein allgemeineres Problem zwischen Java 7 und Java 8 ist. Vielleicht hatte Java 7 aggressivere String-Optimierungen?
EDIT 2 :
Der Vollständigkeit halber sind hier die stringbezogenen VM-Optionen für diese beiden JVMs aufgeführt:
Für 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}
Für 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}
Die UseStringCache
Option wurde in Java 8 ohne Ersatz entfernt, daher bezweifle ich, dass dies einen Unterschied macht. Die restlichen Optionen scheinen dieselben Einstellungen zu haben.
EDIT 3:
Ein Side-by-Side - Vergleich des Quellcodes von den AbstractStringBuilder
, StringBuilder
und Integer
Klassen aus der src.zip
Datei verrät nichts noteworty. Abgesehen von vielen Änderungen an Kosmetik und Dokumentation, unterstützt es Integer
jetzt einige vorzeichenlose Ganzzahlen und StringBuilder
wurde leicht überarbeitet, um mehr Code mit anderen zu teilen StringBuffer
. Keine dieser Änderungen scheint sich auf die von verwendeten Codepfade auszuwirken StringBuilder#append(int)
, obwohl ich möglicherweise etwas übersehen habe.
Ein Vergleich des für IntStr#integerToString()
und generierten Assembler-Codes IntStr#stringBuilder0()
ist weitaus interessanter. Das Grundlayout des Codes, für den generiert wurde, IntStr#integerToString()
war für beide JVMs ähnlich, obwohl Oracle JDK 8u5 aggressiver zu sein schien, wenn einige Aufrufe innerhalb des Integer#toString(int)
Codes eingefügt wurden . Es gab eine klare Übereinstimmung mit dem Java-Quellcode, selbst für jemanden mit minimaler Montageerfahrung.
Der Assembler-Code für IntStr#stringBuilder0()
war jedoch radikal anders. Der von Oracle JDK 8u5 generierte Code stand erneut in direktem Zusammenhang mit dem Java-Quellcode - ich konnte das gleiche Layout leicht erkennen. Im Gegenteil, der von OpenJDK 7 generierte Code war für das ungeübte Auge (wie meins) fast nicht wiederzuerkennen. Der new StringBuilder()
Aufruf wurde anscheinend entfernt, ebenso wie die Erstellung des Arrays im StringBuilder
Konstruktor. Außerdem konnte das Disassembler-Plugin nicht so viele Verweise auf den Quellcode bereitstellen wie in JDK 8.
Ich gehe davon aus, dass dies entweder das Ergebnis eines viel aggressiveren Optimierungsdurchlaufs in OpenJDK 7 ist oder eher das Ergebnis des Einfügens von handgeschriebenem Low-Level-Code für bestimmte StringBuilder
Operationen. Ich bin mir nicht sicher, warum diese Optimierung in meiner JVM 8-Implementierung nicht auftritt oder warum dieselben Optimierungen Integer#toString(int)
in JVM 7 nicht implementiert wurden . Ich denke, jemand, der mit den verwandten Teilen des JRE-Quellcodes vertraut ist, müsste diese Fragen beantworten ...
new StringBuilder().append(this.counter++).toString();
und einen dritten Test mitreturn "" + this.counter++;
?