Warum ist die Klasse StringBuilder#append(int) schneller in Java 7 im Vergleich zu Java 8?
Während der Untersuchung für eine wenig Debatte w.r.t. mit "" + n
und Integer.toString(int)
zum konvertieren einer Ganzzahl primitiv, um eine Zeichenfolge als ich dies schrieb 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;
}
}
Ich habe es mit der Standard-JMH Optionen mit beiden Java VMs, die es gibt auf meinem Linux-Rechner (aktuell Mageia 4 64-bit, Intel i7-3770 CPU, 32GB RAM). Die erste JVM war im Lieferumfang von Oracle JDK
8u5 64-bit:
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-ich bekam so ziemlich das, 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
I. e. mit der StringBuilder
Klasse ist langsamer aufgrund der zusätzlichen Mehraufwand für die Erstellung der StringBuilder
Objekt und Anhängen einer leeren Zeichenfolge. Mit String.format(String, ...)
noch langsamer wird, um eine Größenordnung oder so.
Vertriebs-compiler zur Verfügung gestellt, auf der anderen Seite, basiert 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 interessante:
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)
erscheinen, so viel schneller mit dieser JVM? Blick auf die StringBuilder
class source code offenbart nichts besonders interessant - die Methode in Frage ist fast identisch mit Integer#toString(int)
. Interessanterweise, hängt das Ergebnis von Integer.toString(int)
(die stringBuilder2
microbenchmark) scheint nicht schneller zu sein.
Ist dieser performance-Diskrepanz ein Problem mit den Test-harness? Oder muss mein OpenJDK JVM-Optimierungen enthalten, die beeinflussen würde diesen bestimmten code (anti)-Muster?
EDIT:
Für ein mehr straight-forward-Vergleich, den ich installiert 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)
Die Ergebnisse sind ähnlich 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 eine mehr Allgemeine Java 7 vs Java 8 Problem. Vielleicht Java 7 hatte mehr aggressive string-Optimierungen?
EDIT 2:
Vollständigkeit halber, sind hier die string-bezogene VM-Optionen für diese beiden JVMs:
For 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}
Den UseStringCache
option entfernt wurde, in Java 8 mit keine Ersatzlieferung, so bezweifle ich, dass macht keinen Unterschied. Der rest der Optionen angezeigt, auf den gleichen Einstellungen.
EDIT 3:
Einen side-by-side-Vergleich der Quell-code des AbstractStringBuilder
, StringBuilder
und Integer
Klassen aus der src.zip
Datei zeigt nichts noteworty. Neben einer ganzen Menge von kosmetischen und änderungen an der Dokumentation, Integer
hat jetzt einige Unterstützung für vorzeichenlose Ganzzahlen und StringBuilder
wurde leicht umgestaltet, um zu teilen mehr code mit StringBuffer
. Keine dieser änderungen scheinen auf die code-Pfade StringBuilder#append(int)
, obwohl ich vielleicht etwas verpasst haben.
Einen Vergleich der Assembler-code generiert, für IntStr#integerToString()
und IntStr#stringBuilder0()
ist weitaus interessanter. Das grundlegende layout des generierten code für IntStr#integerToString()
war ähnlich für beide JVMs, obwohl Oracle JDK 8u5 schien mehr zu sein aggressive, w.r.t. inlining einige Anrufe innerhalb der Integer#toString(int)
code. Es war eine klare Korrespondenz mit der Java-Quell-code, selbst für jemanden mit minimalen Montage-Erfahrung.
Den Assembler code für IntStr#stringBuilder0()
war jedoch grundlegend anders. Der erzeugte code von Oracle JDK 8u5 war mal wieder direkt mit der Java-Quell-code - konnte ich leicht erkennen das gleiche layout. Im Gegenteil, der code generiert OpenJDK-7 war fast nicht erkennbar für das ungeübte Auge (wie bei mir). Die new StringBuilder()
rufen wurde scheinbar entfernt, wie war die Erstellung des array in der StringBuilder
Konstruktor. Zusätzlich wird der disassembler-plugin war nicht in der Lage zu bieten, wie viele Verweise auf den source-code, wie in JDK 8.
Ich gehe davon aus, dass dies das Ergebnis von entweder einem viel aggressiver Optimierung pass in OpenJDK 7, oder mehr wahrscheinlich durch das einfügen von hand geschriebene low-level-code für bestimmte StringBuilder
Operationen. Ich bin nicht sicher, warum diese Optimierung geschieht nicht in meinem JVM-8-Implementierung, oder warum die gleichen Optimierungen wurden nicht umgesetzt Integer#toString(int)
im JVM-7. Ich denke, jemand vertraut mit den zugehörigen teilen der JRE source-code hätte, diese Fragen zu beantworten...
- Hast du nicht meine:
new StringBuilder().append(this.counter++).toString();
und einen Dritten test mitreturn "" + this.counter++;
? - Die
stringBuilder
Methode übersetzt in genau der gleichen bytecode alsreturn "" + this.counter++;
. Ich werde sehen, über das hinzufügen von einem Dritten test ohne Anhängen des leeren string... - dort gehen Sie. Kein wirklicher Unterschied, dass ich sehen kann...
- hinzufügen Sie können einen test für
String.format("%d",n);
sowie - wie wäre es damit?
String.format("%d",n)
ist etwa eine Größenordnung langsamer, als alles, was... - danke für die Informationen, ich habe vorausgesagt, es wäre langsamer, aber nicht viel langsamer. Also der Rat wäre don ' T verwenden
String.format()
in zeitkritischen Schleifen oder Aufrufe, kann dauern, bis eine signifikante Mehrheit der Anrufe in einer Anwendung. - IIRC
String.format()
instanziiert einFormatter
- Objekt, das wird sehr teuer für eine sigle zu verwenden. - Ich kann nicht reproduzieren Sie das Problem auf meinem Rechner (Linux, x86-64, Java-1.7.0-55 und Java 1.8.0) mit meine eigene micro-benchmark. Sowohl in Java 7 und Java 8,
StringBuilder
ist etwa 20% schneller alsInteger.toString
. Können Sie die Ausgabe der Mindestlaufzeit zusätzlich um den Mittelwert? - sind Sie mit JMH, oder ein microbenchmark Ihrer eigenen?
- Die letzteren, und ich habe mehr Vertrauen in die Ergebnisse meiner eigenen tests.
- 1. Would you mind posting Ihre benchmark-irgendwo, für mich zu versuchen? Eine einfache microbenchmark meiner eigenen schien zu Stimmen mit JMH. 2. Welche Art von h/w und Betriebssystem verwenden Sie? 3. Wäre es möglich für Sie, um zu versuchen, meine JMH benchmark für einen Vergleich?
- 2. Debian Linux, i7-3517U. 3. Ich ausgeführt den code mit JMH, und ich sehe ähnliche Ergebnisse:
stringBuilder0
ist deutlich schneller in Java 7 (26005
vs.17126
).
Du musst angemeldet sein, um einen Kommentar abzugeben.
TL;DR: Nebenwirkungen in
append
anscheinend brechen StringConcat Optimierungen.Sehr gute Analyse in der ursprünglichen Frage und updates!
Für die Vollständigkeit, unten sind ein paar fehlende Schritte:
Sehen durch die
-XX:+PrintInlining
für beide 7u55 und 8u5. In 7u55, Sie sehen etwas wie dieser:...und in 8u5:
Werden Sie feststellen, dass 7u55-version ist flacher und es sieht aus wie nichts wird aufgerufen, nachdem
StringBuilder
Methoden-das ist ein guter Hinweis darauf, dass die string-Optimierungen in Kraft treten. In der Tat, wenn Sie ausführen 7u55 mit-XX:-OptimizeStringConcat
, die von unteraufrufen wird wieder angezeigt, und die Leistung fällt auf 8u5 Ebenen.OK, wir müssen also herausfinden, warum 8u5 nicht die gleiche Optimierung. Grep http://hg.openjdk.java.net/jdk9/jdk9/hotspot für "StringBuilder", um herauszufinden, wo die VM übernimmt die StringConcat Optimierung; diese erhalten Sie in
src/share/vm/opto/stringopts.cpp
hg log src/share/vm/opto/stringopts.cpp
um herauszufinden, die neuesten änderungen gibt. Einer der Kandidaten wäre:Look für den review-threads auf der OpenJDK-mailing-Listen (leicht genug, um google für den Begriff änderungsmenge Zusammenfassung): http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html
Spot "String-concat-Optimierung-Optimierung reduziert die Muster [...] in einer einzigen Zuweisung eines string und bilden das Ergebnis direkt. Alle möglichen deopts das kommt in den optimierten code neu starten, das Muster von Anfang (ab der StringBuffer-Mittel). Das bedeutet, dass das ganze Muster muss mich nebenwirkungsfreie." Eureka?
Schreiben Sie die kontrastierenden benchmark:
Messen auf JDK-7u55, sehen die gleiche Leistung für inline/gespleißt Nebenwirkungen:
Messen auf JDK 8u5, sehen die Leistungseinbußen, die mit dem inline-Effekt:
Submit-bug-report (https://bugs.openjdk.java.net/browse/JDK-8043677) diskutieren Sie dieses Verhalten mit VM Jungs. Die Gründe für original fix ist grundsolide, interessant wird es jedoch, wenn wir können/sollten zurück diese Optimierung in einigen trivialen Fällen wie diesen.
???
GEWINN.
Yeah und sollte, poste ich die Ergebnisse für die benchmark, die bewegt das Inkrement aus der
StringBuilder
Kette, es zu tun, bevor die gesamte Kette. Auch, geschaltet, um die Durchschnittliche Zeit, und ns/op. Dies ist JDK-7u55:Und das ist 8u5:
stringFormat
ist tatsächlich ein wenig schneller in 8u5, und alle anderen tests sind die gleichen. Dies festigt die Hypothese der Nebeneffekt Bruch in SB-Ketten in den großen übeltäter in Frage zu kommen.StringBuilder
Anrufe und benchmarking. Ich Frage mich, was andere Perlen dieser Art kann es...Ich denke, das hat zu tun mit der
CompileThreshold
- flag, welches steuert, wenn der byte-code kompiliert wird, in Maschinen-code, der durch JIT.Oracle JDK hat eine Standard-Anzahl von 10.000 als Dokument bei http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html.
Wo OpenJDK ich konnte nicht finden eine aktuelle Dokument auf dieser fahne; aber einige E-mail-threads deuten auf eine viel niedrigere Schwelle: http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2010-November/004239.html
Versuchen Sie auch, ein - /ausschalten des Oracle-JDK-flags wie
-XX:+UseCompressedStrings
und-XX:+OptimizeStringConcat
. Ich bin nicht sicher, ob diese flags aktiviert sind standardmäßig auf OpenJDK obwohl. Könnte jemand bitte empfehlen.Einer etwas zu Experimentieren, die Sie tun können, ist Erstens führen Sie das Programm durch eine Menge Zeit, sagen wir, 30.000, die Schleifen tun, ein System.gc() und versuchen Sie dann, ein Blick auf die Entwicklung. Ich glaube, Sie würden ergeben das gleiche.
Und ich nehme an, Ihr GC-Einstellung ist die gleiche wie bei uns. Ansonsten sind Sie Zuordnung einer Menge von Objekten und der GC könnte gut sein, die den größten Teil Ihrer Laufzeit.
CompileThreshold
sollte nicht viel von einer Wirkung...