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 mit return "" + this.counter++; ?
  • Die stringBuilder Methode übersetzt in genau der gleichen bytecode als return "" + 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 ein Formatter - 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 als Integer.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).

InformationsquelleAutor thkala | 2014-05-20
Schreibe einen Kommentar