Frage Warum ist StringBuilder in Java 7 schneller als in Java 8?


Während der Untersuchung für ein wenig Debatte w.r. verwenden "" + n und Integer.toString(int) Um ein Integer-Primitiv in eine Zeichenkette umzuwandeln, schrieb ich dies JMH Mikrobenmark:

@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 mit beiden Java-VMs ausgeführt, die auf meinem Linux-Rechner existieren (aktuelle Mageia 4 64-bit, Intel i7-3770 CPU, 32 GB RAM). Die erste JVM wurde mit Oracle JDK ausgeliefert 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 bekam ich ziemlich genau 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. Verwendung der StringBuilder Die Klasse ist aufgrund des zusätzlichen Aufwands für die Erstellung der StringBuilder Objekt und Anhängen einer leeren Zeichenfolge. Verwenden String.format(String, ...) ist sogar langsamer, um eine Größenordnung oder so.

Der Compiler für die Verteilung 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 tut StringBuilder.append(int) So viel schneller mit dieser JVM? Mit Blick auf die StringBuilder Der Quellcode der Klasse ergab nichts besonders Interessantes - die betreffende Methode ist fast identisch mit Integer#toString(int). Interessanterweise hängt das Ergebnis von Integer.toString(int) (das stringBuilder2 Microbenchmark) scheint nicht schneller zu sein.

Ist diese Leistungsdiskrepanz ein Problem mit dem Prüfkabelbaum? Oder enthält meine OpenJDK-JVM Optimierungen, die sich auf diesen bestimmten (Anti-) Code auswirken?

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 Java 7 gegenüber Java 8 Problem ist. Vielleicht hatte Java 7 aggressivere String-Optimierungen?

BEARBEITEN 2:

Der Vollständigkeit halber finden Sie hier die stringbezogenen VM-Optionen für diese beiden JVMs:

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}   

Das UseStringCache Option wurde in Java 8 ohne Ersatz entfernt, also bezweifle ich, dass das einen Unterschied macht. Der Rest der Optionen scheint die gleichen Einstellungen zu haben.

EDIT 3:

Ein direkter Vergleich des Quellcodes der AbstractStringBuilder, StringBuilder und Integer Klassen von der src.zip Datei von enthüllt nichts bemerkenswertes. Abgesehen von einer ganzen Reihe von kosmetischen und Dokumentationsänderungen, Integer hat jetzt eine Unterstützung für unsigned Ganzzahlen und StringBuilder wurde etwas überarbeitet, um mehr Code mit zu teilen StringBuffer. Keine dieser Änderungen scheint die von StringBuilder#append(int)obwohl ich vielleicht etwas verpasst habe.

Ein Vergleich des für IntStr#integerToString() und IntStr#stringBuilder0() ist viel interessanter. Das grundlegende Layout des generierten Codes IntStr#integerToString() war für beide JVMs ähnlich, obwohl Oracle JDK 8u5 schien aggressiver w.r.t. Einige Anrufe innerhalb der Integer#toString(int) Code. Es gab eine klare Übereinstimmung mit dem Java-Quellcode, sogar für jemanden mit minimaler Assembly-Erfahrung.

Der Assemblercode für IntStr#stringBuilder0()war jedoch radikal anders. Der von Oracle JDK 8u5 generierte Code stand wiederum 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 ungeschulte Auge (wie meiner) fast nicht erkennbar. Das new StringBuilder()Aufruf wurde scheinbar entfernt, ebenso wie die Erstellung des Arrays in der StringBuilder Konstrukteur. Außerdem konnte das Disassembler-Plugin nicht so viele Referenzen auf den Quellcode bereitstellen wie in JDK 8.

Ich nehme an, dass dies entweder das Ergebnis eines viel aggressiveren Optimierungspasses in OpenJDK 7 ist, oder eher das Ergebnis des manuellen Einfügens von handgeschriebenem Low-Level-Code StringBuilder Operationen. Ich bin mir nicht sicher, warum diese Optimierung in meiner JVM 8-Implementierung nicht stattfindet oder warum dieselben Optimierungen nicht implementiert wurden Integer#toString(int) in JVM 7. Ich denke, jemand, der mit den entsprechenden Teilen des JRE-Quellcodes vertraut ist, müsste diese Fragen beantworten ...


76
2018-05-20 10:13


Ursprung


Antworten:


TL; DR: Nebenwirkungen in append anscheinend StringConcat Optimierungen zu brechen.

Sehr gute Analyse in der ursprünglichen Frage und Updates!

Der Vollständigkeit halber sind im Folgenden einige fehlende Schritte aufgeführt:

  • Sieh durch die -XX:+PrintInlining für beide 7u55 und 8u5. In 7u55 wirst du so etwas sehen:

     @ 16   org.sample.IntStr::inlineSideEffect (25 bytes)   force inline by CompilerOracle
       @ 4   java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
       @ 18   java.lang.StringBuilder::append (8 bytes)   already compiled into a big method
       @ 21   java.lang.StringBuilder::toString (17 bytes)   inline (hot)
    

    ... und in 8u5:

     @ 16   org.sample.IntStr::inlineSideEffect (25 bytes)   force inline by CompilerOracle
       @ 4   java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
         @ 3   java.lang.AbstractStringBuilder::<init> (12 bytes)   inline (hot)
           @ 1   java.lang.Object::<init> (1 bytes)   inline (hot)
       @ 18   java.lang.StringBuilder::append (8 bytes)   inline (hot)
         @ 2   java.lang.AbstractStringBuilder::append (62 bytes)   already compiled into a big method
       @ 21   java.lang.StringBuilder::toString (17 bytes)   inline (hot)
         @ 13   java.lang.String::<init> (62 bytes)   inline (hot)
           @ 1   java.lang.Object::<init> (1 bytes)   inline (hot)
           @ 55   java.util.Arrays::copyOfRange (63 bytes)   inline (hot)
             @ 54   java.lang.Math::min (11 bytes)   (intrinsic)
             @ 57   java.lang.System::arraycopy (0 bytes)   (intrinsic)
    

    Sie werden vielleicht bemerken, dass die 7u55-Version flacher ist, und es scheint, als ob nichts danach aufgerufen wird StringBuilder Methoden - dies ist ein guter Hinweis darauf, dass die String-Optimierungen wirksam sind. In der Tat, wenn Sie 7u55 mit ausführen -XX:-OptimizeStringConcatDie Unteralben werden wieder angezeigt und die Leistung sinkt auf 8u5.

  • OK, also müssen wir herausfinden, warum 8u5 nicht die selbe Optimierung durchführt. Grep http://hg.openjdk.java.net/jdk9/jdk9/hotspot für "StringBuilder", um herauszufinden, wo VM die StringConcat-Optimierung behandelt; das wird dich dazu bringen src/share/vm/opto/stringopts.cpp

  • hg log src/share/vm/opto/stringopts.cpp um die letzten Änderungen dort herauszufinden. Einer der Kandidaten wäre:

    changeset:   5493:90abdd727e64
    user:        iveresov
    date:        Wed Oct 16 11:13:15 2013 -0700
    summary:     8009303: Tiered: incorrect results in VM tests stringconcat...
    
  • Suchen Sie nach den Review-Themen in OpenJDK-Mailing-Listen (einfach genug, um nach der Änderungssatz-Übersicht zu googlen): http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2013-Oktober/012084.html

  • Spot „String concat Optimierung Optimierung kollabiert das Muster [...] in eine einzige Zuweisung eines Strings und bildet das Ergebnis direkt. Alle möglichen deopts, die im optimierten Code neu starten dieses Muster von Anfang an (ausgehend von der String Zuteilung) passieren können . Das bedeutet, dass das ganze Muster mir nebenwirkungsfrei sein muss."Eureka?

  • Schreiben Sie den kontrastierenden Benchmark aus:

    @Fork(5)
    @Warmup(iterations = 5)
    @Measurement(iterations = 5)
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @State(Scope.Benchmark)
    public class IntStr {
        private int counter;
    
        @GenerateMicroBenchmark
        public String inlineSideEffect() {
            return new StringBuilder().append(counter++).toString();
        }
    
        @GenerateMicroBenchmark
        public String spliceSideEffect() {
            int cnt = counter++;
            return new StringBuilder().append(cnt).toString();
        }
    }
    
  • Messen Sie es auf JDK 7u55 und sehen Sie die gleiche Leistung für inline / gespleißte Nebenwirkungen:

    Benchmark                       Mode   Samples         Mean   Mean error    Units
    o.s.IntStr.inlineSideEffect     avgt        25       65.460        1.747    ns/op
    o.s.IntStr.spliceSideEffect     avgt        25       64.414        1.323    ns/op
    
  • Messen Sie es auf JDK 8u5, und sehen Sie die Leistungseinbuße mit dem Inline-Effekt:

    Benchmark                       Mode   Samples         Mean   Mean error    Units
    o.s.IntStr.inlineSideEffect     avgt        25       84.953        2.274    ns/op
    o.s.IntStr.spliceSideEffect     avgt        25       65.386        1.194    ns/op
    
  • Reichen Sie den Fehlerbericht ein (https://bugs.openjdk.java.net/browse/JDK-8043677) um dieses Verhalten mit VM-Jungs zu diskutieren. Das Grundprinzip für die ursprüngliche Lösung ist felsenfest, aber es ist interessant, ob wir diese Optimierung in einigen trivialen Fällen wie diesen erhalten können / sollten.

  • ???

  • PROFITIEREN.

Und ja, ich sollte die Ergebnisse für den Benchmark veröffentlichen, der das Inkrement von der StringBuilder Kette, es vor der ganzen Kette tun. Auch auf Durchschnittszeit geschaltet, und ns / op. Das ist JDK 7u55:

Benchmark                      Mode   Samples         Mean   Mean error    Units
o.s.IntStr.integerToString     avgt        25      153.805        1.093    ns/op
o.s.IntStr.stringBuilder0      avgt        25      128.284        6.797    ns/op
o.s.IntStr.stringBuilder1      avgt        25      131.524        3.116    ns/op
o.s.IntStr.stringBuilder2      avgt        25      254.384        9.204    ns/op
o.s.IntStr.stringFormat        avgt        25     2302.501      103.032    ns/op

Und das ist 8u5:

Benchmark                      Mode   Samples         Mean   Mean error    Units
o.s.IntStr.integerToString     avgt        25      153.032        3.295    ns/op
o.s.IntStr.stringBuilder0      avgt        25      127.796        1.158    ns/op
o.s.IntStr.stringBuilder1      avgt        25      131.585        1.137    ns/op
o.s.IntStr.stringBuilder2      avgt        25      250.980        2.773    ns/op
o.s.IntStr.stringFormat        avgt        25     2123.706       25.105    ns/op

stringFormatist in 8u5 tatsächlich ein bisschen schneller, und alle anderen Tests sind gleich. Dies festigt die Hypothese, dass der Nebeneffektbruch in SB-Ketten der Hauptschuldige in der ursprünglichen Frage ist.


94
2018-05-21 19:23



Ich denke, das hat mit dem zu tun CompileThreshold Flag, das steuert, wenn der Bytecode von JIT in Maschinencode kompiliert wird.

Das Oracle JDK hat eine Standardanzahl von 10.000 als Dokument bei http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html.

Wo OpenJDK Ich konnte ein spätestes Dokument auf dieser Flagge nicht finden; aber einige Mail-Threads schlagen 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 folgt ein- / auszuschalten -XX:+UseCompressedStrings und -XX:+OptimizeStringConcat. Ich bin mir nicht sicher, ob diese Flags standardmäßig auf OpenJDK aktiviert sind. Könnte jemand bitte vorschlagen.

Ein Experiment, das Sie tun können, besteht darin, zuerst das Programm viele Male auszuführen, sagen wir, 30.000 Schleifen, machen Sie eine System.gc () und versuchen Sie dann, die Leistung zu betrachten. Ich glaube, sie würden das Gleiche bringen.

Und ich nehme an, dass Ihre GC-Einstellung auch gleich ist. Andernfalls werden viele Objekte zugewiesen, und der GC könnte der Hauptbestandteil Ihrer Laufzeit sein.


5
2018-05-20 10:36