Warum ist StringBuilder # append (int) in Java 7 schneller als in Java 8?
Während der Untersuchung für akleine Debatte w.r.t. mit"" + n
undInteger.toString(int)
Um ein Integer-Primitiv in einen String umzuwandeln, habe ich dies geschriebenJMH Mikrobank:
@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 auf beiden 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 wurde mit Oracle JDK 8u5 64-Bit ausgeliefert:
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 Verwendung derStringBuilder
Klasse ist langsamer aufgrund des zusätzlichen Aufwands beim Erstellen derStringBuilder
Objekt und Anhängen einer leeren Zeichenfolge. VerwendenString.format(String, ...)
ist noch langsamer, um eine Größenordnung oder so.
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 wareninteressant:
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 tutStringBuilder.append(int)
mit dieser JVM so viel schneller erscheinen? Mit Blick auf dieStringBuilder
Der Quellcode der Klasse enthüllte nichts besonders Interessantes - die fragliche Methode ist fast identisch mitInteger#toString(int)
. Interessanterweise hängt das Ergebnis vonInteger.toString(int)
(dasstringBuilder2
microbenchmark) scheint nicht schneller zu sein.
Ist diese Leistungsdiskrepanz ein Problem mit dem Testgurt? 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?
BEARBEITEN 2:
Der Vollständigkeit halber sind hier die zeichenfolgenbezogenen 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}
DasUseStringCache
Die Option wurde in Java 8 ersatzlos 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 derAbstractStringBuilder
, StringBuilder
undInteger
Klassen aus dersrc.zip
Datei enthüllt nichts Bemerkenswertes. Abgesehen von einer ganzen Reihe von kosmetischen und Dokumentationsänderungen,Integer
hat jetzt einige Unterstützung für vorzeichenlose ganze Zahlen undStringBuilder
wurde leicht überarbeitet, um mehr Code mit anderen zu teilenStringBuffer
. Keine dieser Änderungen scheint die von verwendeten Codepfade zu beeinflussenStringBuilder#append(int)
, obwohl ich vielleicht etwas verpasst habe.
Ein Vergleich des Assemblycodes, der für generiert wurdeIntStr#integerToString()
undIntStr#stringBuilder0()
ist weitaus interessanter. Das grundlegende Layout des Codes, der für generiert wurdeIntStr#integerToString()
war für beide JVMs ähnlich, obwohl Oracle JDK 8u5 aggressiver zu sein schien. Inlining einiger Anrufe innerhalb derInteger#toString(int)
Code. Es gab eine eindeutige Übereinstimmung mit dem Java-Quellcode, selbst für jemanden mit minimaler Assemblerfahrung.
Der Assembler-Code fürIntStr#stringBuilder0()
war jedoch radikal anders. Der von Oracle JDK 8u5 generierte Code stand wieder 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. Dasnew StringBuilder()
Der Aufruf wurde anscheinend entfernt, ebenso wie die Erstellung des Arrays in derStringBuilder
Konstrukteur. Außerdem konnte das Disassembler-Plugin nicht so viele Verweise auf den Quellcode liefern 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 sicheren Einfügens von handgeschriebenem Low-Level-CodeStringBuilder
Operationen. Ich bin nicht sicher, warum diese Optimierung in meiner JVM 8-Implementierung nicht erfolgt oder warum dieselben Optimierungen nicht für implementiert wurdenInteger#toString(int)
in JVM 7. Ich denke, jemand, der mit den verwandten Teilen des JRE-Quellcodes vertraut ist, müsste diese Fragen beantworten ...