¿Es mejor reutilizar un StringBuilder en un bucle?

101

Tengo una pregunta relacionada con el rendimiento sobre el uso de StringBuilder. En un bucle muy largo, estoy manipulando un StringBuildery pasándolo a otro método como este:

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

¿La instanciación StringBuilderen cada ciclo de bucle es una buena solución? ¿Y es mejor llamar a una eliminación, como la siguiente?

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

Respuestas:

69

El segundo es aproximadamente un 25% más rápido en mi 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 );
    }
}

Resultados:

25265
17969

Tenga en cuenta que esto es con JRE 1.6.0_07.


Basado en las ideas de Jon Skeet en la edición, aquí está la versión 2. Sin embargo, los mismos resultados.

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

Resultados:

5016
7516
Epaga
fuente
4
Agregué una edición en mi respuesta para explicar por qué podría estar sucediendo esto. Lo miraré con más atención en un rato (45 minutos). Tenga en cuenta que hacer concatenación en las llamadas anexas reduce un poco el punto de usar StringBuilder en primer lugar :)
Jon Skeet
3
También sería interesante ver qué sucede si invierte los dos bloques: el JIT todavía está "calentando" StringBuilder durante la primera prueba. Puede que sea irrelevante, pero es interesante intentarlo.
Jon Skeet
1
Seguiría eligiendo la primera versión porque es más limpia . Pero es bueno que haya hecho el punto de referencia :) Siguiente cambio sugerido: intente el n. ° 1 con una capacidad adecuada pasada al constructor.
Jon Skeet
25
Utilice sb.setLength (0); en cambio, es la forma más rápida de vaciar el contenido de StringBuilder contra la recreación de objetos o el uso de .delete (). Tenga en cuenta que esto no se aplica a StringBuffer, sus comprobaciones de concurrencia anulan la ventaja de velocidad.
P Arrayah
1
Respuesta ineficaz. P Arrayah y Dave Jarvis tienen razón. setLength (0) es de lejos la respuesta más eficiente. StringBuilder está respaldado por una matriz de caracteres y es mutable. En el punto en que se llama a .toString (), la matriz de caracteres se copia y se usa para respaldar una cadena inmutable. En este punto, el búfer mutable de StringBuilder se puede reutilizar, simplemente moviendo el puntero de inserción de nuevo a cero (a través de .setLength (0)). sb.toString crea otra copia (la matriz de caracteres inmutable), por lo que cada iteración requiere dos búferes en lugar del método .setLength (0) que solo requiere un búfer nuevo por ciclo.
Chris
25

En la filosofía de escribir código sólido, siempre es mejor poner tu StringBuilder dentro de tu bucle. De esta manera no sale del código para el que está destinado.

En segundo lugar, la mayor mejora en StringBuilder proviene de darle un tamaño inicial para evitar que crezca mientras se ejecuta el ciclo.

for (loop condition) {
  StringBuilder sb = new StringBuilder(4096);
}
Pedro
fuente
1
Siempre puede medir todo el asunto con llaves, de esa manera no tiene el Stringbuilder afuera.
Epaga
@Epaga: Todavía está fuera del circuito en sí. Sí, no contamina el alcance externo, pero es una forma poco natural de escribir el código para una mejora del rendimiento que no se ha verificado en contexto .
Jon Skeet
O mejor aún, pon todo en su propio método. ;-) Pero te escucho re: contexto.
Epaga
Mejor aún, inicialice con el tamaño esperado en lugar de la suma de un número arbitrario (4096) Su código puede devolver una Cadena que hace referencia a un carácter [] de tamaño 4096 (depende del JDK; por lo que recuerdo, ese fue el caso de 1.4)
kohlerm
24

Más rápido todavía:

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

En la filosofía de escribir código sólido, el funcionamiento interno del método debe estar oculto a los objetos que usan el método. Por lo tanto, desde la perspectiva del sistema, no hay diferencia si vuelve a declarar el StringBuilder dentro del bucle o fuera del bucle. Dado que declararlo fuera del ciclo es más rápido y no hace que el código sea más complicado de leer, reutilice el objeto en lugar de reinstalarlo.

Incluso si el código era más complicado y sabía con certeza que la instanciación de objetos era el cuello de botella, coméntelo.

Tres carreras con esta respuesta:

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

Tres carreras con la otra respuesta:

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

Aunque no es significativo, configurar el StringBuildertamaño de búfer inicial de la le dará una pequeña ganancia.

Dave Jarvis
fuente
3
Esta es, con mucho, la mejor respuesta. StringBuilder está respaldado por una matriz de caracteres y es mutable. En el punto en que se llama a .toString (), la matriz de caracteres se copia y se usa para respaldar una cadena inmutable. En este punto, el búfer mutable de StringBuilder se puede reutilizar, simplemente moviendo el puntero de inserción de nuevo a cero (a través de .setLength (0)). Esas respuestas que sugieren asignar un nuevo StringBuilder por ciclo no parecen darse cuenta de que .toString crea otra copia, por lo que cada iteración requiere dos búferes en lugar del método .setLength (0) que solo requiere un nuevo búfer por ciclo.
Chris
12

Bien, ahora entiendo lo que está pasando y tiene sentido.

Tenía la impresión de que toStringacababa de pasar el subyacente char[]a un constructor de cadenas que no tomó una copia. Entonces se haría una copia en la siguiente operación de "escritura" (por ejemplo delete). Creo que este fue el caso StringBufferde alguna versión anterior. (No lo es ahora). Pero no, toStringsimplemente pasa la matriz (y el índice y la longitud) al Stringconstructor público que toma una copia.

Entonces, en el caso de "reutilizar el StringBuilder", realmente creamos una copia de los datos por cadena, usando la misma matriz de caracteres en el búfer todo el tiempo. Obviamente, crear uno nuevo StringBuildercada vez crea un nuevo búfer subyacente, y luego ese búfer se copia (algo inútil, en nuestro caso particular, pero se hace por razones de seguridad) al crear una nueva cadena.

Todo esto lleva a que la segunda versión sea definitivamente más eficiente, pero al mismo tiempo, diría que es un código más feo.

Jon Skeet
fuente
Solo una información divertida sobre .NET, la situación es diferente. El .NET StringBuilder modifica internamente el objeto "string" regular y el método toString simplemente lo devuelve (marcándolo como no modificable, por lo que las consiguientes manipulaciones de StringBuilder lo volverán a crear). Por lo tanto, la secuencia típica "new StringBuilder-> modificarlo-> a String" no hará ninguna copia adicional (solo para expandir el almacenamiento o reducirlo, si la longitud de la cadena resultante es mucho más corta que su capacidad). En Java, este ciclo siempre hace al menos una copia (en StringBuilder.toString ()).
Ivan Dubrov
El Sun JDK anterior a 1.5 tenía la optimización que suponía
Dan Berindei
9

Como no creo que se haya señalado todavía, debido a las optimizaciones integradas en el compilador Sun Java, que crea automáticamente StringBuilders (StringBuffers pre-J2SE 5.0) cuando ve concatenaciones de cadenas, el primer ejemplo de la pregunta es equivalente a:

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

Cuál es más legible, en mi opinión, el mejor enfoque. Sus intentos de optimizar pueden resultar en ganancias en algunas plataformas, pero potencialmente en pérdidas en otras.

Pero si realmente tiene problemas con el rendimiento, entonces optimice. Sin embargo, comenzaría especificando explícitamente el tamaño del búfer del StringBuilder, según Jon Skeet.

Jack Leow
fuente
4

La JVM moderna es realmente inteligente para cosas como esta. No lo adivinaría y haría algo hacky que sea menos fácil de mantener / legible ... a menos que haga puntos de referencia adecuados con datos de producción que validen una mejora de rendimiento no trivial (y la documente;)

Stu Thompson
fuente
Donde lo "no trivial" es clave: los puntos de referencia pueden mostrar que una forma es proporcionalmente más rápida, pero sin ningún indicio de cuánto tiempo lleva la aplicación real :)
Jon Skeet
Vea el punto de referencia en mi respuesta a continuación. La segunda forma es más rápida.
Epaga
1
@Epaga: Su punto de referencia dice poco sobre la mejora del rendimiento en la aplicación real, donde el tiempo necesario para realizar la asignación de StringBuilder puede ser trivial en comparación con el resto del ciclo. Por eso el contexto es importante en la evaluación comparativa.
Jon Skeet
1
@Epaga: Hasta que lo mida con su código real, no tendremos idea de lo significativo que es realmente. Si hay mucho código para cada iteración del bucle, sospecho que seguirá siendo irrelevante. No sabemos qué hay en el "..."
Jon Skeet
1
(No me malinterpretes, por cierto, los resultados de tus comparativas siguen siendo muy interesantes en sí mismos. Me fascinan los microbenchmarks. Simplemente no me gusta deformar mi código antes de realizar pruebas de la vida real)
Jon Skeet
4

Según mi experiencia con el desarrollo de software en Windows, diría que limpiar StringBuilder durante su ciclo tiene un mejor rendimiento que crear una instancia de StringBuilder con cada iteración. Borrarlo libera esa memoria para que se sobrescriba inmediatamente sin necesidad de asignación adicional. No estoy lo suficientemente familiarizado con el recolector de basura de Java, pero creo que liberar y no reasignar (a menos que su próxima cadena haga crecer StringBuilder) es más beneficioso que la instanciación.

(Mi opinión es contraria a lo que todos los demás están sugiriendo. Hmm. Es hora de compararlo).

cfeduke
fuente
La cuestión es que de todos modos se debe reasignar más memoria, ya que la cadena recién creada está utilizando los datos existentes al final de la iteración del ciclo anterior.
Jon Skeet
Oh, eso tiene sentido, pensé que toString estaba asignando y devolviendo una nueva instancia de cadena y el búfer de bytes para el constructor se estaba limpiando en lugar de reasignar.
cfeduke
El punto de referencia de Epaga muestra que borrar y reutilizar es una ganancia sobre la creación de instancias en cada paso.
cfeduke
1

La razón por la que hacer 'setLength' o 'delete' mejora el rendimiento es principalmente porque el código 'aprende' el tamaño correcto del búfer y menos para hacer la asignación de memoria. Generalmente, recomiendo dejar que el compilador haga las optimizaciones de cadenas . Sin embargo, si el rendimiento es crítico, a menudo calcularé previamente el tamaño esperado del búfer. El tamaño predeterminado de StringBuilder es de 16 caracteres. Si crece más allá de eso, entonces tiene que cambiar de tamaño. Cambiar el tamaño es donde se pierde el rendimiento. Aquí hay otro mini-benchmark que ilustra esto:

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

Los resultados muestran que reutilizar el objeto es aproximadamente un 10% más rápido que crear un búfer del tamaño esperado.

Brianegge
fuente
1

LOL, la primera vez que veo a personas comparan el rendimiento combinando cadenas en StringBuilder. Para ese propósito, si usa "+", podría ser incluso más rápido; D. El propósito de utilizar StringBuilder para acelerar la recuperación de toda la cadena como concepto de "localidad".

En el caso de que recupere un valor de cadena con frecuencia que no necesite cambios frecuentes, Stringbuilder permite un mayor rendimiento de la recuperación de cadenas. Y ese es el propósito de usar Stringbuilder ... por favor, no pruebe MIS el propósito principal de eso ...

Algunas personas dijeron: El avión vuela más rápido. Por lo tanto, lo probé con mi bicicleta y descubrí que el avión se mueve más lento. ¿Sabes cómo configuro la configuración del experimento? D

Ting Choo Chiaw
fuente
1

No significativamente más rápido, pero de mis pruebas muestra que, en promedio, es un par de milisegundos más rápido usando 1.6.0_45 64 bits: use StringBuilder.setLength (0) en lugar 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
fuente
1

La forma más rápida es utilizar "setLength". No implicará la operación de copia. La forma de crear un nuevo StringBuilder debería estar completamente descartada . La lentitud para StringBuilder.delete (int start, int end) es porque copiará la matriz nuevamente para la parte de cambio de tamaño.

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

Después de eso, StringBuilder.delete () actualizará StringBuilder.count al nuevo tamaño. Mientras que StringBuilder.setLength () simplemente simplifica, actualice StringBuilder.count al nuevo tamaño.

Shen liang
fuente
0

El primero es mejor para los humanos. Si el segundo es un poco más rápido en algunas versiones de algunas JVM, ¿entonces qué?

Si el rendimiento es tan crítico, omita StringBuilder y escriba el suyo. Si eres un buen programador y tienes en cuenta cómo tu aplicación utiliza esta función, deberías poder hacerlo aún más rápido. ¿Vale la pena? Probablemente no.

¿Por qué esta pregunta se considera "pregunta favorita"? Porque la optimización del rendimiento es muy divertida, no importa si es práctica o no.

dongilmore
fuente
No es solo una cuestión académica. Si bien la mayoría de las veces (lea 95%) prefiero la legibilidad y la capacidad de mantenimiento, realmente hay casos en que las pequeñas mejoras hacen grandes diferencias ...
Pier Luigi
OK, cambiaré mi respuesta. Si un objeto proporciona un método que permite borrarlo y reutilizarlo, hágalo. Examine el código primero si desea asegurarse de que el borrado sea eficaz; ¡tal vez libera una matriz privada! Si es eficiente, asigne el objeto fuera del bucle y reutilícelo dentro.
Dongilmore
0

No creo que tenga sentido tratar de optimizar el rendimiento de esa manera. Hoy (2019) las dos declaraciones se ejecutan durante aproximadamente 11 segundos para 100.000.000 de bucles en mi computadora portátil 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 mseg (declaración dentro del bucle) y 8236 mseg (declaración fuera del bucle)

Incluso si estoy ejecutando programas para la deduplicación de direcciones con mil millones de bucles, una diferencia de 2 segundos. para 100 millones de bucles no hace ninguna diferencia porque los programas se ejecutan durante horas. También tenga en cuenta que las cosas son diferentes si solo tiene una declaración adjunta:

    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 mseg (bucle interior), 3555 mseg (bucle exterior) La primera declaración que está creando el StringBuilder dentro del bucle es más rápida en ese caso. Y, si cambia el orden de ejecución, es mucho más rápido:

    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 mseg (bucle exterior), 2908 mseg (bucle interior)

Saludos, Ulrich

Ulrich K.
fuente
-2

Declare una vez y asigne cada vez. Es un concepto más pragmático y reutilizable que una optimización.

Peter Mortensen
fuente