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++;
?stringBuilder
Methode übersetzt in genau den gleichen Bytecode wiereturn "" + this.counter++;
. Ich werde sehen, wie man einen dritten Test hinzufügt, ohne die leere ZeichenfolgeString.format("%d",n);
undString.format("%d",n)
ist ungefähr eine Größenordnung langsamer als alles ...Antworten:
TL; DR: Nebenwirkungen bei
append
anscheinend defekten StringConcat-Optimierungen.Sehr gute Analyse in der ursprünglichen Frage und Updates!
Der Vollständigkeit halber sind nachfolgend einige fehlende Schritte aufgeführt:
Sehen Sie sich das
-XX:+PrintInlining
für 7u55 und 8u5 an. In 7u55 sehen Sie ungefähr Folgendes:... und in 8u5:
Möglicherweise stellen Sie fest, dass die 7u55-Version flacher ist und nach
StringBuilder
Methoden nichts aufgerufen wird. Dies ist ein guter Hinweis darauf, dass die Zeichenfolgenoptimierungen wirksam sind. Wenn Sie 7u55 mit ausführen-XX:-OptimizeStringConcat
, werden die Unteraufrufe erneut angezeigt und die Leistung sinkt auf 8u5.OK, also müssen wir herausfinden, warum 8u5 nicht die gleiche Optimierung durchführt. Suchen Sie unter http://hg.openjdk.java.net/jdk9/jdk9/hotspot nach "StringBuilder", um herauszufinden, wo VM die StringConcat-Optimierung übernimmt. das wird dich dazu bringen
src/share/vm/opto/stringopts.cpp
hg log src/share/vm/opto/stringopts.cpp
um die neuesten Änderungen dort herauszufinden. Einer der Kandidaten wäre:Suchen Sie nach den Überprüfungsthreads in OpenJDK-Mailinglisten (einfach genug, um eine Zusammenfassung der Änderungssätze zu googeln): http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html
Spot "Die Optimierung der String-Concat-Optimierung reduziert das Muster [...] in eine einzelne Zuordnung eines Strings und bildet das Ergebnis direkt. Alle möglichen Deopts, die im optimierten Code auftreten können, starten dieses Muster von Anfang an neu (beginnend mit der StringBuffer-Zuordnung). . das bedeutet , dass das gesamte Muster muss ich Nebeneffekt frei. "Eureka?
Schreiben Sie den kontrastierenden Benchmark auf:
Messen Sie es auf JDK 7u55 und sehen Sie die gleiche Leistung für inline / gespleißte Nebenwirkungen:
Messen Sie es auf JDK 8u5 und sehen Sie den Leistungsabfall mit dem Inline-Effekt:
Senden Sie den Fehlerbericht ( https://bugs.openjdk.java.net/browse/JDK-8043677 ), um dieses Verhalten mit VM-Mitarbeitern zu besprechen. Die Begründung für die ursprüngliche Korrektur ist absolut solide. Es ist jedoch interessant, ob wir diese Optimierung in einigen trivialen Fällen wie diesen zurückerhalten können / sollten.
???
PROFITIEREN.
Und ja, ich sollte die Ergebnisse für den Benchmark veröffentlichen, der das Inkrement aus der
StringBuilder
Kette verschiebt und dies vor der gesamten Kette tut. Auch auf Durchschnittszeit und ns / op umgeschaltet. Dies ist JDK 7u55:Und das ist 8u5:
stringFormat
ist in 8u5 tatsächlich etwas schneller und alle anderen Tests sind gleich. Dies bestätigt die Hypothese, dass der Nebenwirkungsbruch in SB-Ketten der Hauptschuldige in der ursprünglichen Frage ist.quelle
StringBuilder
Aufrufen und dem Benchmarking verschoben habe. Ich frage mich, welche anderen kleinen Edelsteine dieser Art es vielleicht gibt ...Ich denke, das hat mit dem
CompileThreshold
Flag zu tun, das steuert, wann der Bytecode von JIT in Maschinencode kompiliert wird.Das Oracle JDK hat eine Standardanzahl von 10.000 als Dokument unter http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html .
Wo OpenJDK konnte ich kein aktuelles Dokument auf dieser Flagge finden; Einige Mail-Threads schlagen jedoch einen viel niedrigeren Schwellenwert vor: http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2010-November/004239.html
Versuchen Sie auch, die Oracle JDK-Flags wie
-XX:+UseCompressedStrings
und ein- und auszuschalten-XX:+OptimizeStringConcat
. Ich bin mir nicht sicher, ob diese Flags in OpenJDK standardmäßig aktiviert sind. Könnte jemand bitte vorschlagen.Ein Experiment, das Sie durchführen können, besteht darin, das Programm zunächst häufig auszuführen, z. B. 30.000 Schleifen, ein System.gc () auszuführen und dann zu versuchen, die Leistung zu überprüfen. Ich glaube, sie würden das gleiche ergeben.
Und ich gehe davon aus, dass Ihre GC-Einstellung auch dieselbe ist. Andernfalls weisen Sie viele Objekte zu, und der GC ist möglicherweise der Hauptteil Ihrer Laufzeit.
quelle
CompileThreshold
sollte keine große Wirkung haben ...