Ist es besser, einen StringBuilder in einer Schleife wiederzuverwenden?


101

Ich habe eine leistungsbezogene Frage zur Verwendung von StringBuilder. In einer sehr langen Schleife manipuliere ich a StringBuilderund übergebe es an eine andere Methode wie diese:

for (loop condition) {
    StringBuilder sb = new StringBuilder();
    sb.append("some string");
    . . .
    sb.append(anotherString);
    . . .
    passToMethod(sb.toString());
}

Ist das Instanziieren StringBuilderbei jedem Schleifenzyklus eine gute Lösung? Und ist es besser, stattdessen ein Löschen aufzurufen, wie im Folgenden?

StringBuilder sb = new StringBuilder();
for (loop condition) {
    sb.delete(0, sb.length);
    sb.append("some string");
    . . .
    sb.append(anotherString);
    . . .
    passToMethod(sb.toString());
}

Antworten:


69

Der zweite ist in meinem Mini-Benchmark ungefähr 25% schneller.

public class ScratchPad {

    static String a;

    public static void main( String[] args ) throws Exception {
        long time = System.currentTimeMillis();
        for( int i = 0; i < 10000000; i++ ) {
            StringBuilder sb = new StringBuilder();
            sb.append( "someString" );
            sb.append( "someString2"+i );
            sb.append( "someStrin4g"+i );
            sb.append( "someStr5ing"+i );
            sb.append( "someSt7ring"+i );
            a = sb.toString();
        }
        System.out.println( System.currentTimeMillis()-time );
        time = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for( int i = 0; i < 10000000; i++ ) {
            sb.delete( 0, sb.length() );
            sb.append( "someString" );
            sb.append( "someString2"+i );
            sb.append( "someStrin4g"+i );
            sb.append( "someStr5ing"+i );
            sb.append( "someSt7ring"+i );
            a = sb.toString();
        }
        System.out.println( System.currentTimeMillis()-time );
    }
}

Ergebnisse:

25265
17969

Beachten Sie, dass dies bei JRE 1.6.0_07 der Fall ist.


Basierend auf Jon Skeets Ideen in der Bearbeitung, hier ist Version 2. Gleiche Ergebnisse.

public class ScratchPad {

    static String a;

    public static void main( String[] args ) throws Exception {
        long time = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for( int i = 0; i < 10000000; i++ ) {
            sb.delete( 0, sb.length() );
            sb.append( "someString" );
            sb.append( "someString2" );
            sb.append( "someStrin4g" );
            sb.append( "someStr5ing" );
            sb.append( "someSt7ring" );
            a = sb.toString();
        }
        System.out.println( System.currentTimeMillis()-time );
        time = System.currentTimeMillis();
        for( int i = 0; i < 10000000; i++ ) {
            StringBuilder sb2 = new StringBuilder();
            sb2.append( "someString" );
            sb2.append( "someString2" );
            sb2.append( "someStrin4g" );
            sb2.append( "someStr5ing" );
            sb2.append( "someSt7ring" );
            a = sb2.toString();
        }
        System.out.println( System.currentTimeMillis()-time );
    }
}

Ergebnisse:

5016
7516

4
Ich habe meiner Antwort eine Änderung hinzugefügt, um zu erklären, warum dies möglicherweise geschieht. Ich werde in einer Weile genauer hinschauen (45 Minuten). Beachten Sie, dass die Verkettung in den Append-Aufrufen den Sinn der Verwendung von StringBuilder in erster Linie etwas verringert :)
Jon Skeet

3
Es wäre auch interessant zu sehen, was passiert, wenn Sie die beiden Blöcke umkehren - die JIT "erwärmt" StringBuilder während des ersten Tests immer noch. Es mag irrelevant sein, aber es ist interessant, es zu versuchen.
Jon Skeet

1
Ich würde immer noch mit der ersten Version gehen, weil es sauberer ist . Aber es ist gut, dass Sie den Benchmark tatsächlich durchgeführt haben :) Nächste vorgeschlagene Änderung: Versuchen Sie # 1 mit einer geeigneten Kapazität, die an den Konstruktor übergeben wird.
Jon Skeet

25
Verwenden Sie sb.setLength (0); Stattdessen ist dies der schnellste Weg, um den Inhalt von StringBuilder zu leeren, um ein Objekt neu zu erstellen oder .delete () zu verwenden. Beachten Sie, dass dies nicht für StringBuffer gilt. Die Parallelitätsprüfungen heben den Geschwindigkeitsvorteil auf.
P Arrayah

1
Ineffiziente Antwort. P Arrayah und Dave Jarvis sind richtig. setLength (0) ist mit Abstand die effizienteste Antwort. StringBuilder wird von einem char-Array unterstützt und ist veränderbar. An dem Punkt, an dem .toString () aufgerufen wird, wird das char-Array kopiert und zum Sichern einer unveränderlichen Zeichenfolge verwendet. Zu diesem Zeitpunkt kann der veränderbare Puffer von StringBuilder wiederverwendet werden, indem einfach der Einfügezeiger auf Null zurückgesetzt wird (über .setLength (0)). sb.toString erstellt eine weitere Kopie (das unveränderliche char-Array), sodass für jede Iteration zwei Puffer erforderlich sind, im Gegensatz zur .setLength (0) -Methode, für die nur ein neuer Puffer pro Schleife erforderlich ist.
Chris

25

In der Philosophie, soliden Code zu schreiben, ist es immer besser, den StringBuilder in die Schleife zu setzen. Auf diese Weise geht es nicht über den Code hinaus, für den es bestimmt ist.

Zweitens besteht die größte Verbesserung in StringBuilder darin, ihm eine Anfangsgröße zu geben, um zu verhindern, dass er größer wird, während die Schleife läuft

for (loop condition) {
  StringBuilder sb = new StringBuilder(4096);
}

1
Sie können das Ganze immer mit geschweiften Klammern versehen, so dass Sie den Stringbuilder nicht draußen haben.
Epaga

@Epaga: Es ist immer noch außerhalb der Schleife. Ja, es verschmutzt nicht den äußeren Bereich, aber es ist eine unnatürliche Methode, den Code für eine Leistungsverbesserung zu schreiben, die im Kontext nicht überprüft wurde .
Jon Skeet

Oder noch besser, setzen Sie das Ganze in seine eigene Methode. ;-) Aber ich höre dich wieder: Kontext.
Epaga

Besser noch mit der erwarteten Größe initialisieren, anstatt eine beliebige Zahl zu summieren (4096) Ihr Code gibt möglicherweise einen String zurück, der auf ein Zeichen [] der Größe 4096 verweist (abhängig vom JDK; soweit ich mich erinnere, war dies bei 1.4 der Fall)
kohlerm

24

Immer noch schneller:

public class ScratchPad {

    private static String a;

    public static void main( String[] args ) throws Exception {
        long time = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder( 128 );

        for( int i = 0; i < 10000000; i++ ) {
            // Resetting the string is faster than creating a new object.
            // Since this is a critical loop, every instruction counts.
            //
            sb.setLength( 0 );
            sb.append( "someString" );
            sb.append( "someString2" );
            sb.append( "someStrin4g" );
            sb.append( "someStr5ing" );
            sb.append( "someSt7ring" );
            setA( sb.toString() );
        }

        System.out.println( System.currentTimeMillis()-time );
    }

    private static void setA( String aString ) {
        a = aString;
    }
}

In der Philosophie des Schreibens von festem Code sollte das Innenleben der Methode vor den Objekten verborgen sein, die die Methode verwenden. Aus Sicht des Systems spielt es also keine Rolle, ob Sie den StringBuilder innerhalb oder außerhalb der Schleife neu deklarieren. Da das Deklarieren außerhalb der Schleife schneller ist und das Lesen des Codes nicht komplizierter wird, sollten Sie das Objekt wiederverwenden, anstatt es erneut zu instanziieren.

Auch wenn der Code komplizierter war und Sie sicher wussten, dass die Objektinstanziierung der Engpass ist, kommentieren Sie ihn.

Drei Läufe mit dieser Antwort:

$ java ScratchPad
1567
$ java ScratchPad
1569
$ java ScratchPad
1570

Drei Läufe mit der anderen Antwort:

$ java ScratchPad2
1663
2231
$ java ScratchPad2
1656
2233
$ java ScratchPad2
1658
2242

Obwohl dies nicht signifikant ist, StringBuilderergibt das Einstellen der anfänglichen Puffergröße eine kleine Verstärkung.


3
Dies ist bei weitem die beste Antwort. StringBuilder wird von einem char-Array unterstützt und ist veränderbar. An dem Punkt, an dem .toString () aufgerufen wird, wird das char-Array kopiert und zum Sichern einer unveränderlichen Zeichenfolge verwendet. Zu diesem Zeitpunkt kann der veränderbare Puffer von StringBuilder wiederverwendet werden, indem einfach der Einfügezeiger auf Null zurückgesetzt wird (über .setLength (0)). Diese Antworten, die darauf hinweisen, einen brandneuen StringBuilder pro Schleife zuzuweisen, scheinen nicht zu erkennen, dass .toString eine weitere Kopie erstellt. Daher erfordert jede Iteration zwei Puffer im Gegensatz zur Methode .setLength (0), für die nur ein neuer Puffer pro Schleife erforderlich ist.
Chris

12

Okay, ich verstehe jetzt, was los ist, und es macht Sinn.

Ich hatte den Eindruck, dass toStringder Basiswert gerade an char[]einen String-Konstruktor übergeben wurde, der keine Kopie erstellt hat. Eine Kopie würde dann bei der nächsten "Schreib" -Operation (z delete. B. ) erstellt. Ich glaube , das war der Fall StringBufferin einigen früheren Version. (Es ist nicht jetzt.) Aber nein - toStringübergibt nur das Array (und den Index und die Länge) an den öffentlichen StringKonstruktor, der eine Kopie erstellt.

Im StringBuilderFall "Wiederverwendung " erstellen wir also wirklich eine Kopie der Daten pro Zeichenfolge, wobei wir die ganze Zeit über dasselbe char-Array im Puffer verwenden. Wenn Sie StringBuilderjedes Mal einen neuen Puffer erstellen, wird natürlich ein neuer zugrunde liegender Puffer erstellt - und dieser Puffer wird dann kopiert (in unserem speziellen Fall etwas sinnlos, aber aus Sicherheitsgründen), wenn Sie einen neuen String erstellen.

All dies führt dazu, dass die zweite Version definitiv effizienter ist - aber gleichzeitig würde ich immer noch sagen, dass es hässlicherer Code ist.


Nur ein paar lustige Infos über .NET, da ist die Situation anders. Der .NET StringBuilder ändert das reguläre "String" -Objekt intern und die toString-Methode gibt es einfach zurück (markiert es als nicht änderbar, sodass nachfolgende StringBuilder-Manipulationen es neu erstellen). Die typische Sequenz "new StringBuilder-> modify it-> to String" erstellt also keine zusätzliche Kopie (nur zum Erweitern oder Verkleinern des Speichers, wenn die resultierende Stringlänge viel kürzer als die Kapazität ist). In Java erstellt dieser Zyklus immer mindestens eine Kopie (in StringBuilder.toString ()).
Ivan Dubrov

Das Sun JDK vor 1.5 hatte die von Ihnen angenommene Optimierung: bugs.sun.com/bugdatabase/view_bug.do?bug_id=6219959
Dan Berindei

9

Da ich glaube, dass aufgrund von im Sun Java-Compiler integrierten Optimierungen, der automatisch StringBuilder (StringBuffers vor J2SE 5.0) erstellt, wenn String-Verkettungen angezeigt werden, noch nicht darauf hingewiesen wurde, entspricht das erste Beispiel in der Frage:

for (loop condition) {
  String s = "some string";
  . . .
  s += anotherString;
  . . .
  passToMethod(s);
}

Welches ist besser lesbar, IMO, der bessere Ansatz. Ihre Optimierungsversuche können auf einigen Plattformen zu Gewinnen führen, auf anderen jedoch möglicherweise zu Verlusten.

Wenn Sie jedoch wirklich auf Leistungsprobleme stoßen, sollten Sie diese optimieren. Ich würde jedoch damit beginnen, die Puffergröße des StringBuilder pro Jon Skeet explizit anzugeben.


4

Die moderne JVM ist wirklich schlau in solchen Dingen. Ich würde es nicht erraten und etwas Hackiges tun, das weniger wartbar / lesbar ist ... es sei denn, Sie führen richtige Benchmarks mit Produktionsdaten durch, die eine nicht triviale Leistungsverbesserung bestätigen (und dokumentieren;)


Wo "nicht trivial" der Schlüssel ist - Benchmarks können zeigen, dass eine Form proportional schneller ist, aber ohne Hinweis darauf, wie viel Zeit in der realen App benötigt wird :)
Jon Skeet

Siehe den Benchmark in meiner Antwort unten. Der zweite Weg ist schneller.
Epaga

1
@Epaga: Ihr Benchmark sagt wenig über die Leistungsverbesserung in der realen App aus, bei der die für die StringBuilder-Zuweisung benötigte Zeit im Vergleich zum Rest der Schleife trivial sein kann. Deshalb ist der Kontext beim Benchmarking wichtig.
Jon Skeet

1
@Epaga: Bis er es mit seinem echten Code gemessen hat, werden wir keine Ahnung haben, wie wichtig es wirklich ist. Wenn es für jede Iteration der Schleife viel Code gibt, vermute ich stark, dass dies immer noch irrelevant ist. Wir wissen nicht, was in der "..."
Jon Skeet

1
(Verstehen Sie mich übrigens nicht falsch - Ihre Benchmark-Ergebnisse sind an sich immer noch sehr interessant. Ich bin fasziniert von Mikrobenchmarks. Ich mag es einfach nicht, meinen Code aus der Form zu bringen, bevor ich auch Tests im wirklichen Leben durchführe.)
Jon Skeet

4

Aufgrund meiner Erfahrung mit der Entwicklung von Software unter Windows würde ich sagen, dass das Löschen des StringBuilder während Ihrer Schleife eine bessere Leistung bietet als das Instanziieren eines StringBuilder bei jeder Iteration. Durch das Löschen wird der Speicher sofort überschrieben, ohne dass eine zusätzliche Zuordnung erforderlich ist. Ich bin mit dem Java-Garbage-Collector nicht vertraut genug, aber ich würde denken, dass das Freigeben und keine Neuzuweisung (es sei denn, Ihr nächster String vergrößert den StringBuilder) vorteilhafter ist als die Instanziierung.

(Meine Meinung widerspricht dem, was alle anderen vorschlagen. Hmm. Zeit, es zu bewerten.)


Die Sache ist, dass ohnehin mehr Speicher neu zugewiesen werden muss, da die vorhandenen Daten am Ende der vorherigen Schleifeniteration von der neu erstellten Zeichenfolge verwendet werden.
Jon Skeet

Oh, das macht Sinn, ich hatte jedoch gedacht, dass toString eine neue Zeichenfolgeninstanz zuweist und zurückgibt und der Bytepuffer für den Builder gelöscht wird, anstatt neu zuzuweisen.
cfeduke

Der Benchmark von Epaga zeigt, dass das Löschen und Wiederverwenden bei jedem Durchgang einen Vorteil gegenüber der Instanziierung darstellt.
cfeduke

1

Der Grund, warum das Ausführen von 'setLength' oder 'delete' die Leistung verbessert, liegt hauptsächlich darin, dass der Code die richtige Größe des Puffers 'lernt' und weniger für die Speicherzuweisung. Im Allgemeinen empfehle ich, den Compiler die Zeichenfolgenoptimierungen durchführen zu lassen . Wenn die Leistung jedoch kritisch ist, berechne ich häufig die erwartete Größe des Puffers vor. Die Standardgröße für StringBuilder beträgt 16 Zeichen. Wenn Sie darüber hinaus wachsen, muss die Größe geändert werden. Beim Ändern der Größe geht die Leistung verloren. Hier ist ein weiterer Mini-Benchmark, der dies veranschaulicht:

private void clear() throws Exception {
    long time = System.currentTimeMillis();
    int maxLength = 0;
    StringBuilder sb = new StringBuilder();

    for( int i = 0; i < 10000000; i++ ) {
        // Resetting the string is faster than creating a new object.
        // Since this is a critical loop, every instruction counts.
        //
        sb.setLength( 0 );
        sb.append( "someString" );
        sb.append( "someString2" ).append( i );
        sb.append( "someStrin4g" ).append( i );
        sb.append( "someStr5ing" ).append( i );
        sb.append( "someSt7ring" ).append( i );
        maxLength = Math.max(maxLength, sb.toString().length());
    }

    System.out.println(maxLength);
    System.out.println("Clear buffer: " + (System.currentTimeMillis()-time) );
}

private void preAllocate() throws Exception {
    long time = System.currentTimeMillis();
    int maxLength = 0;

    for( int i = 0; i < 10000000; i++ ) {
        StringBuilder sb = new StringBuilder(82);
        sb.append( "someString" );
        sb.append( "someString2" ).append( i );
        sb.append( "someStrin4g" ).append( i );
        sb.append( "someStr5ing" ).append( i );
        sb.append( "someSt7ring" ).append( i );
        maxLength = Math.max(maxLength, sb.toString().length());
    }

    System.out.println(maxLength);
    System.out.println("Pre allocate: " + (System.currentTimeMillis()-time) );
}

public void testBoth() throws Exception {
    for(int i = 0; i < 5; i++) {
        clear();
        preAllocate();
    }
}

Die Ergebnisse zeigen, dass die Wiederverwendung des Objekts etwa 10% schneller ist als die Erstellung eines Puffers mit der erwarteten Größe.


1

LOL, zum ersten Mal habe ich Leute gesehen, die die Leistung durch Kombinieren von Strings in StringBuilder verglichen haben. Wenn Sie zu diesem Zweck "+" verwenden, kann es sogar noch schneller sein; D. Der Zweck der Verwendung von StringBuilder, um das Abrufen des gesamten Strings als Konzept der "Lokalität" zu beschleunigen.

In dem Szenario, dass Sie häufig einen String-Wert abrufen, der nicht häufig geändert werden muss, ermöglicht Stringbuilder eine höhere Leistung beim Abrufen von Strings. Und das ist der Zweck der Verwendung von Stringbuilder. Bitte testen Sie nicht den Hauptzweck von Stringbuilder.

Einige Leute sagten, Flugzeug fliegt schneller. Deshalb habe ich es mit meinem Fahrrad getestet und festgestellt, dass sich das Flugzeug langsamer bewegt. Wissen Sie, wie ich die Experimenteinstellungen einstelle? D.


1

Nicht wesentlich schneller, aber aus meinen Tests geht hervor, dass es mit 1.6.0_45 64 Bit im Durchschnitt ein paar Millis schneller ist: Verwenden Sie StringBuilder.setLength (0) anstelle von StringBuilder.delete ():

time = System.currentTimeMillis();
StringBuilder sb2 = new StringBuilder();
for (int i = 0; i < 10000000; i++) {
    sb2.append( "someString" );
    sb2.append( "someString2"+i );
    sb2.append( "someStrin4g"+i );
    sb2.append( "someStr5ing"+i );
    sb2.append( "someSt7ring"+i );
    a = sb2.toString();
    sb2.setLength(0);
}
System.out.println( System.currentTimeMillis()-time );

1

Der schnellste Weg ist die Verwendung von "setLength". Der Kopiervorgang ist nicht erforderlich. Die Möglichkeit, einen neuen StringBuilder zu erstellen, sollte vollständig ausfallen . Die langsame Funktion für StringBuilder.delete (int start, int end) ist, weil das Array für den Größenänderungsteil erneut kopiert wird.

 System.arraycopy(value, start+len, value, start, count-end);

Danach aktualisiert StringBuilder.delete () die Datei StringBuilder.count auf die neue Größe. Während StringBuilder.setLength () nur vereinfacht, aktualisieren Sie StringBuilder.count auf die neue Größe.


0

Das erste ist besser für den Menschen. Wenn die zweite Version bei einigen Versionen einiger JVMs etwas schneller ist, was dann?

Wenn die Leistung so kritisch ist, umgehen Sie StringBuilder und schreiben Sie Ihren eigenen. Wenn Sie ein guter Programmierer sind und berücksichtigen, wie Ihre App diese Funktion verwendet, sollten Sie sie noch schneller machen können. Lohnend? Wahrscheinlich nicht.

Warum wird diese Frage als "Lieblingsfrage" angesehen? Weil Leistungsoptimierung so viel Spaß macht, egal ob praktisch oder nicht.


Es ist nicht nur eine akademische Frage. Während ich die meiste Zeit (lesen Sie 95%) Lesbarkeit und Wartbarkeit bevorzuge, gibt es wirklich Fälle, in denen kleine Verbesserungen große Unterschiede machen ...
Pier Luigi

OK, ich werde meine Antwort ändern. Wenn ein Objekt eine Methode bereitstellt, mit der es gelöscht und wiederverwendet werden kann, tun Sie dies. Untersuchen Sie zuerst den Code, wenn Sie sicherstellen möchten, dass das Löschen effizient ist. Vielleicht gibt es ein privates Array frei! Wenn dies effizient ist, ordnen Sie das Objekt außerhalb der Schleife zu und verwenden Sie es innerhalb wieder.
Dongilmore

0

Ich denke nicht, dass es Sinn macht, zu versuchen, die Leistung so zu optimieren. Heute (2019) laufen beide Statements ungefähr 11 Sekunden für 100.000.000 Schleifen auf meinem I5-Laptop:

    String a;
    StringBuilder sb = new StringBuilder();
    long time = 0;

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        StringBuilder sb3 = new StringBuilder();
        sb3.append("someString");
        sb3.append("someString2");
        sb3.append("someStrin4g");
        sb3.append("someStr5ing");
        sb3.append("someSt7ring");
        a = sb3.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        sb.setLength(0);
        sb.delete(0, sb.length());
        sb.append("someString");
        sb.append("someString2");
        sb.append("someStrin4g");
        sb.append("someStr5ing");
        sb.append("someSt7ring");
        a = sb.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

==> 11000 ms (Deklaration innerhalb der Schleife) und 8236 ms (Deklaration außerhalb der Schleife)

Selbst wenn ich Programme für die Adressveröffentlichung mit einigen Milliarden Schleifen ausführe, beträgt die Differenz 2 Sekunden. Für 100 Millionen Schleifen macht das keinen Unterschied, da diese Programme stundenlang laufen. Beachten Sie auch, dass die Dinge anders sind, wenn Sie nur eine Append-Anweisung haben:

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        StringBuilder sb3 = new StringBuilder();
        sb3.append("someString");
            a = sb3.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        sb.setLength(0);
        sb.delete(0, sb.length());
        sb.append("someString");
        a = sb.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

==> 3416 ms (innere Schleife), 3555 ms (äußere Schleife) Die erste Anweisung, die den StringBuilder innerhalb der Schleife erstellt, ist in diesem Fall schneller. Und wenn Sie die Ausführungsreihenfolge ändern, geht es viel schneller:

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        sb.setLength(0);
        sb.delete(0, sb.length());
        sb.append("someString");
        a = sb.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        StringBuilder sb3 = new StringBuilder();
        sb3.append("someString");
            a = sb3.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

==> 3638 ms (äußere Schleife), 2908 ms (innere Schleife)

Grüße, Ulrich


-2

Einmal deklarieren und jedes Mal zuweisen. Es ist ein pragmatischeres und wiederverwendbares Konzept als eine Optimierung.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.