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

101

J'ai une question relative aux performances concernant l'utilisation de StringBuilder. Dans une très longue boucle, je manipule un StringBuilderet 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 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());
}
Pier Luigi
la source

Réponses:

69

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. Même résultats.

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
Epaga
la source
4
J'ai ajouté une modification dans ma réponse pour expliquer pourquoi cela pourrait se produire. Je regarderai plus attentivement dans un moment (45 minutes). Notez que faire de la concaténation dans les appels d'ajout réduit quelque peu l'intérêt d'utiliser StringBuilder en premier lieu :)
Jon Skeet
3
Il serait également intéressant de voir ce qui se passe si vous inversez les deux blocs - le JIT "préchauffe" encore StringBuilder lors du premier test. Cela peut bien être hors de propos, mais intéressant à essayer.
Jon Skeet
1
J'irais toujours avec la première version car elle est plus propre . Mais c'est bien que vous ayez fait le benchmark :) Prochain changement suggéré: essayez le n ° 1 avec une capacité appropriée transmise au constructeur.
Jon Skeet
25
Utilisez sb.setLength (0); à la place, c'est le moyen le plus rapide de vider le contenu de StringBuilder contre la recréation d'un objet ou l'utilisation de .delete (). Notez que cela ne s'applique pas à StringBuffer, ses vérifications de concurrence annulent l'avantage de vitesse.
P Arrayah
1
Réponse inefficace. P Arrayah et Dave Jarvis ont raison. setLength (0) est de loin la réponse la plus efficace. StringBuilder est soutenu par un tableau de caractères et est modifiable. Au moment où .toString () est appelé, le tableau char est copié et est utilisé pour sauvegarder une chaîne immuable. À ce stade, le tampon mutable de StringBuilder peut être réutilisé, simplement en ramenant le pointeur d'insertion à zéro (via .setLength (0)). sb.toString crée encore une autre copie (le tableau de caractères immuable), donc chaque itération nécessite deux tampons par opposition à la méthode .setLength (0) qui ne nécessite qu'un nouveau tampon par boucle.
Chris
25

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

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

for (loop condition) {
  StringBuilder sb = new StringBuilder(4096);
}
Peter
la source
1
Vous pouvez toujours définir le tout avec des accolades, de cette façon, vous n'avez pas le Stringbuilder à l'extérieur.
Epaga
@Epaga: C'est toujours en dehors de la boucle elle-même. Oui, cela ne pollue pas la portée externe, mais c'est une façon non naturelle d'écrire le code pour une amélioration des performances qui n'a pas été vérifiée dans le contexte .
Jon Skeet
Ou mieux encore, mettez le tout dans sa propre méthode. ;-) Mais je t'entends re: contexte.
Epaga
Mieux encore initialiser avec la taille attendue au lieu du nombre arbitraire de somme (4096) Votre code peut renvoyer une chaîne qui fait référence à un char [] de taille 4096 (dépend du JDK; pour autant que je me souvienne, c'était le cas pour 1.4)
kohlerm
24

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é des objets qui utilisent la méthode. Ainsi, il ne fait aucune différence du point de vue du système si vous redéclarez le StringBuilder dans la boucle ou en dehors de la boucle. Comme le déclarer en dehors de la boucle est plus rapide et que cela ne complique pas la lecture du code, réutilisez l'objet plutôt que de le réinstituer.

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 courses avec cette réponse:

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

Trois courses avec l'autre réponse:

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

Bien que cela ne soit pas significatif, le réglage de la StringBuildertaille initiale du tampon donnera un petit gain.

Dave Jarvis
la source
3
C'est de loin la meilleure réponse. StringBuilder est soutenu par un tableau de caractères et est modifiable. Au moment où .toString () est appelé, le tableau char est copié et est utilisé pour sauvegarder une chaîne immuable. À ce stade, le tampon mutable de StringBuilder peut être réutilisé, simplement en ramenant le pointeur d'insertion à zéro (via .setLength (0)). Ces réponses suggérant d'allouer un tout nouveau StringBuilder par boucle ne semblent pas se rendre compte que .toString crée encore une autre copie, donc chaque itération nécessite deux tampons par opposition à la méthode .setLength (0) qui ne nécessite qu'un nouveau tampon par boucle.
Chris
12

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

J'avais l'impression que toStringje venais de passer le sous-jacent char[]dans un constructeur String qui ne prenait pas de copie. Une copie serait alors faite lors de la prochaine opération "d'écriture" (par exemple delete). Je pense que c'était le cas StringBufferdans certaines versions précédentes. (Ce n'est pas le cas maintenant.) Mais non - toStringpasse simplement le tableau (ainsi que l'index et la longueur) au Stringconstructeur public qui 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, créer un nouveau à StringBuilderchaque fois crée un nouveau tampon sous-jacent - puis ce tampon est copié (quelque peu inutile, dans notre cas particulier, mais fait pour des raisons de sécurité) lors de la création d'une nouvelle chaîne.

Tout cela conduit à ce que la deuxième version soit définitivement plus efficace - mais en même temps, je dirais toujours que c'est un code plus laid.

Jon Skeet
la source
Juste quelques informations amusantes sur le .NET, la situation est différente. Le .NET StringBuilder modifie en interne l'objet "string" normal et la méthode toString le renvoie simplement (le marquant comme non modifiable, donc les manipulations StringBuilder conséquentes le recréeront). Ainsi, la séquence typique "nouveau StringBuilder-> le modifier-> en chaîne" ne fera pas de copie supplémentaire (uniquement pour étendre le stockage ou le réduire, si la longueur de chaîne résultante est beaucoup plus courte que sa capacité). En Java, ce cycle effectue toujours au moins une copie (dans StringBuilder.toString ()).
Ivan Dubrovnik
Le Sun JDK pré-1.5 avait l'optimisation que vous supposiez
Dan Berindei
9

Comme je ne pense pas que cela ait encore été souligné, en raison des optimisations intégrées au compilateur Sun Java, qui crée automatiquement StringBuilders (StringBuffers pré-J2SE 5.0) lorsqu'il voit des concaténations de chaînes, le premier exemple de 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 plates-formes, mais potentiellement en perdre d'autres.

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

Jack Leow
la source
4

La JVM moderne est vraiment intelligente pour ce genre de choses. Je ne le devinerais pas et ne ferais pas quelque chose de piraté qui soit moins maintenable / lisible ... à moins que vous ne fassiez de bons repères avec des données de production qui valident une amélioration de performance non triviale (et la documentent;)

Stu Thompson
la source
Où «non trivial» est la clé - les benchmarks peuvent montrer qu'une forme est proportionnellement plus rapide, mais sans aucune indication sur le temps que cela prend dans la vraie application :)
Jon Skeet
Voir le repère dans ma réponse ci-dessous. Le deuxième moyen est plus rapide.
Epaga
1
@Epaga: Votre benchmark en dit peu sur l'amélioration des performances de l'application réelle, où le temps nécessaire pour faire l'allocation StringBuilder peut être insignifiant par rapport au reste de la boucle. C'est pourquoi le contexte est important dans l'analyse comparative.
Jon Skeet
1
@Epaga: Tant qu'il ne l'a pas mesuré avec son vrai code, nous n'aurons aucune idée de son importance réelle. S'il y a beaucoup de code pour chaque itération de la boucle, je soupçonne fortement que cela ne sera toujours pas pertinent. Nous ne savons pas ce qu'il y a dans le "..."
Jon Skeet
1
(Ne vous méprenez pas, btw - vos résultats de référence sont toujours très intéressants en eux-mêmes. Je suis fasciné par les microbenchmarks. Je n'aime tout simplement pas déformer mon code avant d'effectuer des tests réels.)
Jon Skeet
4

Sur la base de mon expérience avec le développement de logiciels sur Windows, je dirais que l'effacement de StringBuilder pendant votre boucle a de meilleures performances que l'instanciation d'un StringBuilder à chaque itération. L'effacer libère cette mémoire pour être écrasée immédiatement sans allocation supplémentaire requise. Je ne suis pas assez familier avec le ramasse-miettes Java, mais je pense que la libération et aucune réallocation (à moins que votre prochaine chaîne 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.)

cfeduke
la source
Le fait est que plus de mémoire doit être réallouée de toute façon, car les données existantes sont utilisées par la chaîne nouvellement créée à la fin de l'itération de boucle précédente.
Jon Skeet
Oh, cela a du sens, j'avais pensé que toString allouait et renvoyait une nouvelle instance de chaîne et que le tampon d'octets pour le constructeur s'effaçait au lieu de réallouer.
cfeduke
La référence d'Epaga montre que l'effacement et la réutilisation sont un gain par rapport à l'instanciation à chaque passage.
cfeduke
1

La raison pour laquelle faire un 'setLength' ou 'delete' améliore les performances est principalement le code 'apprenant' la bonne taille du tampon, et moins pour faire l'allocation de mémoire. En général, 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 être redimensionné. 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.

Brianegge
la source
1

LOL, la première fois que j'ai vu des gens comparer les performances en combinant une chaîne dans StringBuilder. Pour cela, si vous utilisez "+", cela pourrait être encore plus rapide; D. Le but d'utiliser StringBuilder pour accélérer la récupération de la chaîne entière en tant que 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 modification fréquente, Stringbuilder permet de meilleures performances de récupération de chaîne. Et c'est le but de l'utilisation de Stringbuilder .. s'il vous plaît ne pas MIS-Test le but principal de cela.

Certaines personnes ont dit, 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 je règle les paramètres de l'expérience; D

Ting Choo Chiaw
la source
1

Pas beaucoup plus rapide, mais d'après mes tests, il montre en moyenne quelques millis de temps 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 );
johnmartel
la source
1

Le moyen le plus rapide est d'utiliser "setLength". Cela n'impliquera pas l'opération de copie. La façon de créer un nouveau StringBuilder devrait être complètement abandonnée . La lenteur de StringBuilder.delete (int start, int end) est due au fait qu'il recopiera le tableau pour la partie redimensionnée.

 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 de StringBuilder.count à la nouvelle taille.

Shen liang
la source
0

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

Si les performances sont si critiques, contournez StringBuilder et écrivez les vôtres. Si vous êtes un bon programmeur et que vous 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.

Dongilmore
la source
Ce n'est pas seulement une question académique. Alors que la plupart du temps (lire 95%) je préfère la lisibilité et la maintenabilité, il y a vraiment des cas où de petites améliorations font de grandes différences ...
Pier Luigi
OK, je vais changer ma réponse. Si un objet fournit une méthode qui lui permet d'être effacé et réutilisé, faites-le. Examinez d'abord le code si vous voulez vous assurer que le clear est efficace; peut-être qu'il libère un tableau privé! Si efficace, allouez l'objet en dehors de la boucle et réutilisez-le à l'intérieur.
dongilmore
0

Je ne pense pas qu'il soit logique d'essayer d'optimiser les performances comme ça. Aujourd'hui (2019), les deux états fonctionnent environ 11sec pour 100.000.000 de boucles sur mon ordinateur portable 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 ms (déclaration à l'intérieur de la boucle) et 8236 ms (déclaration à l'extérieur de la boucle)

Même si je suis en train d'exécuter des programmes de dédoublement d'adresses avec quelques milliards de boucles, une différence de 2 sec. pour 100 millions de boucles ne fait aucune différence car ces programmes fonctionnent pendant des heures. Sachez également que les choses sont différentes si vous n'avez qu'une seule instruction d'ajout:

    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 (boucle intérieure), 3555 ms (boucle extérieure) La première instruction qui crée le StringBuilder dans la boucle est plus rapide dans ce cas. Et, si vous changez l'ordre d'exécution, c'est beaucoup plus rapide:

    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 (boucle extérieure), 2908 msec (boucle intérieure)

Cordialement, Ulrich

Ulrich K.
la source
-2

Déclarez une fois et attribuez-les à chaque fois. C'est un concept plus pragmatique et réutilisable qu'une optimisation.

Peter Mortensen
la source