web-dev-qa-db-fra.com

Est-il préférable de réutiliser un StringBuilder en boucle?

J'ai une question liée aux performances concernant l'utilisation de StringBuilder. Dans une très longue boucle, je manipule un StringBuilder et je le passe à une autre méthode comme celle-ci:

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

L'instanciation de StringBuilder à chaque cycle de boucle est-elle une bonne solution? Et est-il préférable d'appeler une suppression, comme suit?

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

Le second est environ 25% plus rapide dans mon 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 );
    }
}

Résultats:

25265
17969

Notez que c'est avec JRE 1.6.0_07.


Basé sur les idées de Jon Skeet dans l'édition, voici la version 2. Les mêmes résultats cependant.

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

Résultats:

5016
7516
67
Epaga

Dans la philosophie de l'écriture de code solide, il est toujours préférable de placer votre StringBuilder dans votre boucle. De cette façon, il ne sort pas du code auquel il est destiné.

Deuxièmement, la plus grande amélioration de StringBuilder vient de lui donner une taille initiale pour éviter qu'elle ne grossisse pendant que la boucle fonctionne

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

Toujours plus vite:

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

Dans la philosophie de l'écriture de code solide, le fonctionnement interne de la méthode doit être caché aux objets qui utilisent la méthode. Ainsi, cela ne fait aucune différence du point de vue du système, que vous redéclariez le StringBuilder dans la boucle ou à l'extérieur de la boucle. Comme le déclarer en dehors de la boucle est plus rapide et ne rend pas le code plus compliqué à lire, réutilisez l'objet plutôt que de le réinstancier.

Même si le code était plus compliqué et que vous saviez avec certitude que l'instanciation d'objet était le goulot d'étranglement, commentez-le.

Trois passages avec cette réponse:

$ Java ScratchPad
1567
$ Java ScratchPad
1569
$ Java ScratchPad
1570

Trois runs avec l'autre réponse:

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

Bien que non significatif, la définition de la taille de mémoire tampon initiale de StringBuilder donnera un petit gain.

23
Dave Jarvis

D'accord, je comprends maintenant ce qui se passe, et cela a du sens.

J'avais l'impression que toString venait de passer le sous-jacent char[] dans un constructeur String qui ne l'a pas en prend une copie. Une copie serait alors effectuée lors de la prochaine opération "d'écriture" (par exemple delete). Je crois que cela était le cas avec StringBuffer dans une version précédente. (Ce n'est pas le cas maintenant.) Mais non - toString passe simplement le tableau (et l'index et la longueur) au constructeur public String qui en prend une copie.

Donc, dans le cas "réutiliser le StringBuilder", nous créons véritablement une copie des données par chaîne, en utilisant le même tableau de caractères dans le tampon tout le temps. Évidemment, la création d'un nouveau StringBuilder à chaque fois crée un nouveau tampon sous-jacent - puis ce tampon est copié (un peu inutilement, dans notre cas particulier, mais pour des raisons de sécurité) lors de la création d'une nouvelle chaîne.

Tout cela conduit à la deuxième version certainement plus efficace - mais en même temps, je dirais toujours que c'est du code plus laid.

12
Jon Skeet

Étant donné que je ne pense pas que cela ait été souligné, en raison des optimisations intégrées dans le compilateur Sun Java, qui crée automatiquement StringBuilders (StringBuffers pré-J2SE 5.0) lorsqu'il voit les concaténations de chaînes, la première exemple dans la question est équivalent à:

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

Ce qui est plus lisible, l'OMI, la meilleure approche. Vos tentatives d'optimisation peuvent entraîner des gains sur certaines plateformes, mais potentiellement des pertes sur d'autres.

Mais si vous rencontrez vraiment des problèmes de performances, assurez-vous d'optimiser. Je commencerais par spécifier explicitement la taille de la mémoire tampon du StringBuilder, par Jon Skeet.

9
Jack Leow

La JVM moderne est vraiment intelligente sur ce genre de choses. Je ne voudrais pas le deviner et faire quelque chose de hacky qui est moins maintenable/lisible ... sauf si vous faites des repères appropriés avec des données de production qui valident une amélioration des performances non triviale (et le documentez;)

4
Stu Thompson

Sur la base de mon expérience avec le développement de logiciels sous Windows, je dirais que la suppression du StringBuilder pendant votre boucle a de meilleures performances que l'instanciation d'un StringBuilder à chaque itération. L'effacer libère cette mémoire à écraser immédiatement sans aucune allocation supplémentaire requise. Je ne connais pas assez le Java garbage collector, mais je pense que la libération et aucune réallocation (à moins que votre chaîne suivante ne développe le StringBuilder) est plus bénéfique que l'instanciation.

(Mon opinion est contraire à ce que tout le monde suggère. Hmm. Il est temps de le comparer.)

4
cfeduke

Le moyen le plus rapide consiste à utiliser "setLength". Cela n'implique pas l'opération de copie. La façon de créer un nouveau StringBuilder devrait être complètement sortie. Le ralentissement de StringBuilder.delete (int start, int end) est parce qu'il copiera à nouveau le tableau pour la partie de redimensionnement.

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

Après cela, StringBuilder.delete () mettra à jour StringBuilder.count à la nouvelle taille. Alors que StringBuilder.setLength () simplifie simplement la mise à jour du StringBuilder.count à la nouvelle taille.

1
Shen liang

La raison pour laquelle faire un "setLength" ou "supprimer" améliore les performances est principalement le code "apprenant" la bonne taille du tampon, et moins pour faire l'allocation de mémoire. Généralement, je recommande de laisser le compilateur faire les optimisations de chaîne . Cependant, si les performances sont critiques, je pré-calcule souvent la taille attendue du tampon. La taille par défaut de StringBuilder est de 16 caractères. Si vous vous développez au-delà de cela, alors il doit redimensionner. Le redimensionnement est l'endroit où les performances se perdent. Voici un autre mini-benchmark qui illustre cela:

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

Les résultats montrent que la réutilisation de l'objet est environ 10% plus rapide que la création d'un tampon de la taille attendue.

1
brianegge

LOL, la première fois que j'ai vu des gens comparer les performances en combinant des chaînes dans StringBuilder. À cet effet, si vous utilisez "+", cela pourrait être encore plus rapide; D. Le but de l'utilisation de StringBuilder pour accélérer la récupération de la chaîne entière comme concept de "localité".

Dans le scénario où vous récupérez fréquemment une valeur de chaîne qui ne nécessite pas de modifications fréquentes, Stringbuilder permet des performances supérieures de récupération de chaîne. Et c'est le but d'utiliser Stringbuilder .. s'il vous plaît ne testez pas MIS le but principal de cela ..

Certaines personnes ont dit que l'avion vole plus vite. Par conséquent, je l'ai testé avec mon vélo et j'ai constaté que l'avion se déplaçait plus lentement. Savez-vous comment j'ai défini les paramètres de l'expérience; D

1
Ting Choo Chiaw

Pas beaucoup plus rapide, mais d'après mes tests, il s'avère en moyenne être quelques millis plus rapide en utilisant 1.6.0_45 64 bits: utilisez StringBuilder.setLength (0) au lieu de 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
johnmartel

Hélas, je suis sur C # normalement, je pense que l'effacement d'un StringBuilder pèse plus que l'instanciation d'un nouveau.

0
Florian Greinacher

Le premier est meilleur pour les humains. Si la seconde est un peu plus rapide sur certaines versions de certaines JVM, alors quoi?

Si les performances sont si critiques, contournez StringBuilder et écrivez les vôtres. Si vous êtes un bon programmeur et tenez compte de la façon dont votre application utilise cette fonction, vous devriez pouvoir la rendre encore plus rapide. Digne d'intérêt? Probablement pas.

Pourquoi cette question est-elle considérée comme "question préférée"? Parce que l'optimisation des performances est tellement amusante, qu'elle soit pratique ou non.

0
dongilmore