È meglio riutilizzare uno StringBuilder in un ciclo?


101

Ho una domanda relativa alle prestazioni sull'uso di StringBuilder. In un ciclo molto lungo sto manipolando a StringBuildere passandolo a un altro metodo come questo:

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

Istanziare StringBuilderad ogni ciclo di loop è una buona soluzione? E invece è meglio chiamare un'eliminazione, come la seguente?

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

Risposte:


69

Il secondo è circa il 25% più veloce nel mio mini-benchmark.

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 );
    }
}

Risultati:

25265
17969

Nota che questo è con JRE 1.6.0_07.


Basata sulle idee di Jon Skeet nella modifica, ecco la versione 2. Stessi risultati però.

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 );
    }
}

Risultati:

5016
7516

4
Ho aggiunto una modifica alla mia risposta per spiegare perché questo potrebbe accadere. Guarderò più attentamente tra un po '(45 minuti). Nota che la concatenazione nelle chiamate di accodamento riduce in qualche modo il punto di utilizzare StringBuilder in primo luogo :)
Jon Skeet

3
Inoltre sarebbe interessante vedere cosa succede se si invertono i due blocchi - il JIT sta ancora "riscaldando" StringBuilder durante il primo test. Potrebbe essere irrilevante, ma interessante da provare.
Jon Skeet,

1
Preferisco ancora la prima versione perché è più pulita . Ma è positivo che tu abbia effettivamente eseguito il benchmark :) Prossima modifica suggerita: prova # 1 con una capacità appropriata passata al costruttore.
Jon Skeet,

25
Usa sb.setLength (0); invece, è il modo più veloce per svuotare il contenuto di StringBuilder contro la creazione di oggetti o l'utilizzo di .delete (). Nota che questo non si applica a StringBuffer, i suoi controlli di concorrenza annullano il vantaggio di velocità.
P Arrayah

1
Risposta inefficiente. P Arrayah e Dave Jarvis hanno ragione. setLength (0) è di gran lunga la risposta più efficiente. StringBuilder è supportato da un array di caratteri ed è modificabile. Nel momento in cui viene chiamato .toString (), l'array char viene copiato e viene utilizzato per eseguire il backup di una stringa immutabile. A questo punto, il buffer mutabile di StringBuilder può essere riutilizzato, semplicemente spostando nuovamente il puntatore di inserimento a zero (tramite .setLength (0)). sb.toString crea ancora un'altra copia (l'array char immutabile), quindi ogni iterazione richiede due buffer rispetto al metodo .setLength (0) che richiede solo un nuovo buffer per ciclo.
Chris

25

Nella filosofia di scrivere codice solido è sempre meglio mettere il tuo StringBuilder all'interno del tuo ciclo. In questo modo non esce dal codice a cui è destinato.

In secondo luogo, il più grande miglioramento in StringBuilder deriva dal dargli una dimensione iniziale per evitare che diventi più grande durante l'esecuzione del ciclo

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

1
Puoi sempre definire l'intera cosa con parentesi graffe, in questo modo non hai lo Stringbuilder all'esterno.
Epaga

@ Epaga: è ancora fuori dal ciclo stesso. Sì, non inquina l'ambito esterno, ma è un modo innaturale per scrivere il codice per un miglioramento delle prestazioni che non è stato verificato nel contesto .
Jon Skeet,

O ancora meglio, metti il ​​tutto nel suo metodo. ;-) Ma ti sento dire: contesto.
Epaga

Meglio ancora inizializzare con la dimensione prevista invece della somma numero arbitrario (4096) Il tuo codice potrebbe restituire una stringa che fa riferimento a un carattere [] di dimensione 4096 (dipende dal JDK; per quanto mi ricordo che era il caso per 1.4)
kohlerm

24

Ancora più veloce:

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;
    }
}

Nella filosofia di scrivere codice solido, il funzionamento interno del metodo dovrebbe essere nascosto agli oggetti che utilizzano il metodo. Pertanto, dal punto di vista del sistema, non fa differenza se si dichiara nuovamente StringBuilder all'interno o all'esterno del ciclo. Dato che dichiararlo al di fuori del ciclo è più veloce e non rende il codice più complicato da leggere, riutilizzare l'oggetto invece di ripristinarlo.

Anche se il codice era più complicato e sapevi per certo che l'istanza dell'oggetto era il collo di bottiglia, commentalo.

Tre piste con questa risposta:

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

Tre piste con l'altra risposta:

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

Sebbene non significativo, l'impostazione della StringBuilderdimensione del buffer iniziale di darà un piccolo guadagno.


3
Questa è di gran lunga la migliore risposta. StringBuilder è supportato da un array di caratteri ed è modificabile. Nel momento in cui viene chiamato .toString (), l'array char viene copiato e viene utilizzato per eseguire il backup di una stringa immutabile. A questo punto, il buffer mutabile di StringBuilder può essere riutilizzato, semplicemente spostando nuovamente il puntatore di inserimento su zero (tramite .setLength (0)). Quelle risposte che suggeriscono di allocare un StringBuilder nuovo di zecca per ciclo non sembrano rendersi conto che .toString crea ancora un'altra copia, quindi ogni iterazione richiede due buffer rispetto al metodo .setLength (0) che richiede solo un nuovo buffer per ciclo.
Chris

12

Ok, ora capisco cosa sta succedendo e ha senso.

Avevo l'impressione che toStringpassasse il sottostante char[]a un costruttore String che non ne prendeva una copia. Una copia verrebbe quindi eseguita nella successiva operazione di "scrittura" (ad esempio delete). Credo che questo fosse il caso StringBufferdi alcune versioni precedenti. (Non è ora.) Ma no - toStringpassa semplicemente l'array (e l'indice e la lunghezza) al Stringcostruttore pubblico che ne prende una copia.

Quindi nel caso "riutilizza il StringBuilder" creiamo veramente una copia dei dati per stringa, utilizzando lo stesso array di caratteri nel buffer per tutto il tempo. Ovviamente crearne uno nuovo StringBuilderogni volta crea un nuovo buffer sottostante - e poi quel buffer viene copiato (in qualche modo inutilmente, nel nostro caso particolare, ma fatto per ragioni di sicurezza) quando si crea una nuova stringa.

Tutto ciò fa sì che la seconda versione sia decisamente più efficiente, ma allo stesso tempo direi ancora che è un codice più brutto.


Solo alcune informazioni divertenti su .NET, la situazione è diversa. .NET StringBuilder modifica internamente il normale oggetto "stringa" e il metodo toString lo restituisce semplicemente (contrassegnandolo come non modificabile, quindi le successive manipolazioni di StringBuilder lo ricreano). Quindi, la tipica sequenza "new StringBuilder-> edit it-> to String" non farà alcuna copia extra (solo per espandere la memoria o rimpicciolirla, se la lunghezza della stringa risultante è molto più breve della sua capacità). In Java questo ciclo fa sempre almeno una copia (in StringBuilder.toString ()).
Ivan Dubrov

Il Sun JDK pre-1.5 aveva l'ottimizzazione che stavi assumendo: bugs.sun.com/bugdatabase/view_bug.do?bug_id=6219959
Dan Berindei

9

Dato che non credo sia stato ancora indicato, a causa delle ottimizzazioni integrate nel compilatore Sun Java, che crea automaticamente StringBuilders (StringBuffers pre-J2SE 5.0) quando vede concatenazioni di stringhe, il primo esempio nella domanda è equivalente a:

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

Che è più leggibile, IMO, l'approccio migliore. I tuoi tentativi di ottimizzazione possono comportare guadagni in alcune piattaforme, ma potenzialmente perdite su altre.

Ma se stai davvero riscontrando problemi con le prestazioni, allora certo, ottimizza. Inizierei specificando esplicitamente la dimensione del buffer di StringBuilder, secondo Jon Skeet.


4

La moderna JVM è davvero intelligente su cose come questa. Non vorrei indovinarlo e fare qualcosa di hacky che è meno manutenibile / leggibile ... a meno che non si eseguano adeguati benchmark con dati di produzione che convalidano un miglioramento delle prestazioni non banale (e lo documentano;)


Dove "non banale" è la chiave: i benchmark possono mostrare che una forma è proporzionalmente più veloce, ma senza alcun suggerimento su quanto tempo sta impiegando nell'app reale :)
Jon Skeet

Vedi il benchmark nella mia risposta di seguito. Il secondo modo è più veloce.
Epaga

1
@Epaga: il tuo benchmark dice poco sul miglioramento delle prestazioni nell'app reale, dove il tempo impiegato per eseguire l'allocazione di StringBuilder può essere insignificante rispetto al resto del ciclo. Ecco perché il contesto è importante nel benchmarking.
Jon Skeet,

1
@Epaga: Finché non lo avrà misurato con il suo codice reale, non avremo idea di quanto sia davvero significativo. Se c'è molto codice per ogni iterazione del ciclo, sospetto fortemente che sarà comunque irrilevante. Non sappiamo cosa c'è nel "..."
Jon Skeet

1
(Non fraintendetemi, btw - i risultati dei vostri benchmark sono ancora molto interessanti di per sé. Sono affascinato dai microbenchmark. Semplicemente non mi piace deformare il mio codice prima di eseguire anche test di vita reale.)
Jon Skeet,

4

Sulla base della mia esperienza con lo sviluppo di software su Windows, direi che cancellare lo StringBuilder durante il ciclo ha prestazioni migliori rispetto all'istanza di uno StringBuilder con ogni iterazione. La cancellazione libera la memoria da sovrascrivere immediatamente senza necessità di allocazioni aggiuntive. Non ho abbastanza familiarità con il garbage collector di Java, ma penserei che la liberazione e nessuna riallocazione (a meno che la stringa successiva non faccia crescere StringBuilder) sia più vantaggiosa dell'istanziazione.

(La mia opinione è contraria a ciò che suggeriscono tutti gli altri. Hmm. È ora di confrontarlo.)


Il fatto è che più memoria deve essere riallocata comunque, poiché i dati esistenti vengono utilizzati dalla stringa appena creata alla fine dell'iterazione del ciclo precedente.
Jon Skeet,

Oh, ha senso, avevo pensato che toString stesse allocando e restituendo una nuova istanza di stringa e il buffer di byte per il builder si cancellasse invece di riallocare.
cfeduke

Il benchmark di Epaga mostra che la cancellazione e il riutilizzo sono un vantaggio rispetto alla creazione di istanze ad ogni passaggio.
cfeduke

1

Il motivo per cui fare un 'setLength' o 'delete' migliora le prestazioni è principalmente il codice che 'apprende' la giusta dimensione del buffer e meno per l'allocazione della memoria. In generale, consiglio di lasciare che il compilatore esegua le ottimizzazioni delle stringhe . Tuttavia, se le prestazioni sono critiche, spesso calcolerò in anticipo la dimensione prevista del buffer. La dimensione predefinita di StringBuilder è di 16 caratteri. Se cresci oltre, allora deve ridimensionarsi. Il ridimensionamento è dove le prestazioni si perdono. Ecco un altro mini-benchmark che lo illustra:

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();
    }
}

I risultati mostrano che il riutilizzo dell'oggetto è circa il 10% più veloce rispetto alla creazione di un buffer delle dimensioni previste.


1

LOL, la prima volta che ho visto persone hanno confrontato le prestazioni combinando stringhe in StringBuilder. A tal fine, se usi "+", potrebbe essere ancora più veloce; D. Lo scopo di utilizzare StringBuilder per accelerare il recupero dell'intera stringa come concetto di "località".

Nello scenario in cui si recupera frequentemente un valore String che non necessita di modifiche frequenti, Stringbuilder consente prestazioni più elevate di recupero delle stringhe. E questo è lo scopo dell'utilizzo di Stringbuilder .. per favore non mis-test lo scopo principale di quello ..

Alcune persone hanno detto, L'aereo vola più veloce. Pertanto, l'ho testato con la mia bici e ho scoperto che l'aereo si muove più lentamente. Sai come ho impostato le impostazioni dell'esperimento? D


1

Non significativamente più veloce, ma dai miei test risulta in media essere un paio di millis più veloce usando 1.6.0_45 64 bit: usa StringBuilder.setLength (0) invece di 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

Il modo più veloce è usare "setLength". Non coinvolgerà l'operazione di copia. Il modo per creare un nuovo StringBuilder dovrebbe essere completamente fuori . Il lento per StringBuilder.delete (int start, int end) è perché copierà di nuovo l'array per la parte di ridimensionamento.

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

Successivamente, StringBuilder.delete () aggiornerà StringBuilder.count alla nuova dimensione. Mentre StringBuilder.setLength () semplifica semplicemente, aggiorna StringBuilder.count alla nuova dimensione.


0

Il primo è meglio per gli esseri umani. Se il secondo è un po 'più veloce su alcune versioni di alcune JVM, allora cosa?

Se le prestazioni sono così critiche, ignora StringBuilder e scrivi il tuo. Se sei un buon programmatore e prendi in considerazione come la tua app utilizza questa funzione, dovresti essere in grado di renderlo ancora più veloce. Vale la pena? Probabilmente no.

Perché questa domanda è considerata "domanda preferita"? Perché l'ottimizzazione delle prestazioni è molto divertente, indipendentemente dal fatto che sia pratica o meno.


Non è solo una questione accademica. Mentre la maggior parte delle volte (leggi 95%) preferisco leggibilità e manutenibilità, ci sono davvero casi in cui piccoli miglioramenti fanno grandi differenze ...
Pier Luigi

OK, cambierò la mia risposta. Se un oggetto fornisce un metodo che consente di cancellarlo e riutilizzarlo, fallo. Esamina prima il codice se vuoi assicurarti che il clear sia efficiente; forse rilascia un array privato! Se efficiente, alloca l'oggetto al di fuori del ciclo e riutilizzalo all'interno.
Dongilmore

0

Non credo che abbia senso cercare di ottimizzare le prestazioni in questo modo. Oggi (2019) entrambe le dichiarazioni sono in esecuzione di circa 11 secondi per 100.000.000 di loop sul mio laptop I5:

    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 msec (dichiarazione all'interno del ciclo) e 8236 msec (dichiarazione all'esterno del ciclo)

Anche se sto eseguendo programmi per la pubblicazione degli indirizzi con alcuni miliardi di loop, una differenza di 2 sec. per 100 milioni di cicli non fa alcuna differenza perché quei programmi sono in esecuzione per ore. Inoltre, tieni presente che le cose sono diverse se hai solo un'istruzione append:

    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 msec (ciclo interno), 3555 msec (ciclo esterno) La prima istruzione che crea StringBuilder all'interno del ciclo è più veloce in questo caso. E se cambi l'ordine di esecuzione è molto più veloce:

    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 msec (loop esterno), 2908 msec (loop interno)

Saluti, Ulrich


-2

Dichiara una volta e assegna ogni volta. È un concetto più pragmatico e riutilizzabile di un'ottimizzazione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.