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 ...

Antworten auf die Frage(2)

Ihre Antwort auf die Frage