2008-10-28 6 views
91

Tengo una pregunta relacionada con el rendimiento con respecto al uso de StringBuilder. En un bucle muy largo estoy manipulando una StringBuilder y pasándolo a otro método como este:¿Es mejor reutilizar un StringBuilder en un bucle?

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

Está crear instancias de StringBuilder en cada ciclo del bucle es una buena solución? ¿Y está llamando a una eliminación mejor, 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()); 
} 

Respuesta

4

La JVM moderna es realmente inteligente sobre cosas como esta. No lo adivinaría y haría algo raro que es menos fácil de mantener/legible ... a menos que haga marcas de referencia adecuadas con datos de producción que validen una mejora de rendimiento no trivial (y documentarlo)

+0

Donde "no trivial" es la clave - los puntos de referencia pueden mostrar que una forma es * proporcionalmente * más rápida, pero sin ninguna pista sobre cuánto tiempo está tomando la aplicación real :) –

+0

Consulte la prueba comparativa 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. Es por eso que el contexto es importante en la evaluación comparativa. –

0

Alas estoy en C# normalmente, creo que borrar un StringBuilder pesa más que la instanciación de uno nuevo.

25

En la filosofía de escribir código sólido, siempre es mejor poner su StringBuilder dentro de su ciclo. De esta forma no sale del código para el que fue diseñado.

En segundo lugar la mayor Improvment en StringBuilder proviene de lo que le da un tamaño inicial para evitar que cada vez más grande, mientras que el bucle se ejecuta

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

Siempre puede delimitar todo el conjunto con corchetes, de esta forma no tendrá el Stringbuilder afuera. – Epaga

+0

@Epaga: Aún está fuera del bucle.Sí, no contamina el alcance externo, pero es una forma antinatural de escribir el código para una mejora del rendimiento que no se ha verificado * en contexto *. –

+0

O mejor aún, ponga todo en su propio método. ;-) Pero te escucho: contexto. – Epaga

4

Con base en mi experiencia con el desarrollo de software en Windows yo diría despejar el StringBuilder a cabo durante su ciclo tiene un mejor rendimiento que la instancia de un StringBuilder con cada iteración. Al borrarlo, se libera la memoria que se sobrescribirá inmediatamente sin necesidad de una asignación adicional. No estoy lo suficientemente familiarizado con el recolector de basura de Java, pero creo que la liberación y la no reasignación (a menos que su próxima cadena crezca StringBuilder) es más beneficiosa que la creación de instancias.

(Mi opinión es contrario a lo que todos los demás están sugiriendo. Hmm. Tiempo establecer criterios de referencia.)

+0

La cuestión es que hay que reasignar más memoria de todos modos, ya que los datos existentes están siendo utilizados por el String recién creado al final de la iteración del bucle anterior. –

+0

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 estaba borrando en lugar de reasignar. – cfeduke

+0

El punto de referencia de Epaga muestra que borrar y reutilizar es una ganancia sobre la instanciación en cada pasada. – cfeduke

65

El segundo es aproximadamente un 25% más rápido en mi mini-referencia.

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 

en cuenta que este es con JRE 1.6.0_07.


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

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 
+4

He agregado una edición en mi respuesta para explicar por qué esto podría estar pasando. Voy a mirar con más cuidado en un momento (45 minutos). Tenga en cuenta que hacer concatenación en las llamadas de adición reduce el punto de utilizar StringBuilder en primer lugar algo :) –

+3

También sería interesante ver qué pasa si invierte los dos bloques: el JIT todavía está "calentando" StringBuilder durante la primera prueba. Puede ser irrelevante, pero interesante de probar. –

+1

Todavía iría con la primera versión porque es * cleaner *. Pero está bien que hayas hecho el benchmark :) Siguiente cambio sugerido: intenta # 1 con una capacidad apropiada pasada al constructor. –

12

Está bien, ahora entienden lo que está pasando, y que tiene sentido.

Yo tenía la impresión de que acaba de pasar el toStringchar[] que subyace en un constructor de cadena que no lo hicieron lleve una copia. Luego se realizaría una copia en la siguiente operación de "escritura" (por ejemplo, delete). Creo que este fue el caso con StringBuffer en alguna versión anterior. (No es ahora.) Pero no - toString simplemente pasa la matriz (y el índice y la longitud) al constructor público String que toma una copia.

Por lo tanto, en el caso de "reutilizar el StringBuilder" creamos genuinamente una copia de los datos por cadena, utilizando la misma matriz de caracteres en el búfer todo el tiempo. Obviamente, la creación de un nuevo StringBuilder crea un nuevo búfer subyacente, y luego ese búfer se copia (algo sin sentido, 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 aún diría que es un código más feo.

+0

Solo alguna información divertida sobre .NET, la situación allí es diferente. .NET StringBuilder modifica internamente el objeto "cadena" regular y el método toString simplemente lo devuelve (marcándolo como no modificable, por lo que las manipulaciones posteriores de StringBuilder lo volverán a crear). Por lo tanto, la secuencia típica de "nuevo StringBuilder-> modificarlo-> a Cadena" no realizará 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 realiza al menos una copia (en StringBuilder.toString()). –

+0

Sun JDK pre-1.5 tenía la optimización que estaba asumiendo: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6219959 –

-2

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

0

La primera es mejor para los humanos. Si el segundo es un poco más rápido en algunas versiones de algunas JVM, ¿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 está usando tu aplicación esta función, deberías poder hacerlo aún más rápido. ¿Vale la pena? Probablemente no.

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

+0

No es una cuestión académica solamente. Aunque la mayoría de las veces (lea el 95%) prefiero la legibilidad y el mantenimiento, en realidad hay casos en que las pequeñas mejoras hacen grandes diferencias ... –

+0

OK, cambiaré mi respuesta. Si un objeto proporciona un método que le permite ser borrado y reutilizado, entonces hágalo. Examine primero el código si quiere asegurarse de que el borrado sea eficiente; tal vez libera una matriz privada! Si es eficiente, asigne el objeto fuera del ciclo y vuelva a usarlo dentro. – dongilmore

9

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

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

Que es más legible, IMO, el mejor enfoque. Sus intentos de optimizar pueden generar ganancias en alguna plataforma, pero potencialmente pueden afectar a otros.

Pero si realmente está teniendo problemas de rendimiento, entonces, optimícese. Empezaría especificando explícitamente el tamaño del buffer del StringBuilder, según Jon Skeet.

23

todavía más rápido:

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 la escritura de código sólido, el funcionamiento interno del método deben ocultarse a los objetos que utilizan el método. Por lo tanto, no importa desde el punto de vista del sistema si redeclara 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, entonces reutilice el objeto en lugar de reinstalarlo.

Incluso si el código era más complicado, y usted sabía con certeza que la instanciación del objeto 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, ajuste de tamaño de búfer inicial del StringBuilder 's va a dar una pequeña ganancia.

+2

Esta es de lejos la mejor respuesta. StringBuilder está respaldado por una matriz char y es mutable. En el momento en que se llama a .toString(), la matriz char se copia y se utiliza para respaldar una cadena inmutable. En este punto, el buffer mutable de StringBuilder puede ser reutilizado, simplemente moviendo el puntero de inserción a cero (a través de .setLength (0)). Las 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 almacenamientos intermedios en comparación con el método .setLength (0) que solo requiere un nuevo almacenamiento intermedio por ciclo. – Chris

1

La razón por la que hacer un 'setLength' o 'eliminar' mejora el rendimiento es principalmente el código 'learning' del tamaño correcto del buffer, y menos para hacer la asignación de memoria. Generalmente, I recommend letting the compiler do the string optimizations. Sin embargo, si el rendimiento es crítico, a menudo pre-calcularé el tamaño esperado del buffer. El tamaño predeterminado de StringBuilder es de 16 caracteres. Si creces más allá de eso, entonces tiene que cambiar el tamaño. Cambiar el tamaño es donde se pierde el rendimiento. Aquí hay otro mini-índice de referencia 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 reutilizar el objeto es un 10% más rápido que la creación de un búfer del tamaño esperado.

1

LOL, primera vez que he visto gente ha comparado el rendimiento combinando cadenas en StringBuilder. Para ese propósito, si usa "+", podría ser aún más rápido; D. El propósito de usar StringBuilder para acelerar la recuperación de toda la cadena como concepto de "localidad".

En el caso de que recupere un valor String con frecuencia que no necesita 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 haga MIS-Test el propósito principal de eso ...

Algunas personas dijeron, Plane vuela más rápido. Por lo tanto, lo pruebo con mi bicicleta y descubrí que el avión se mueve más despacio. ¿Sabe cómo configuro la configuración del experimento? D

1

No es significativamente más rápido, pero según mis pruebas muestra un promedio de un par de millones más 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); 
1

La manera más rápida es usar "setLength". No implicará la operación de copiado. La forma de crear un nuevo StringBuilder debería estar completamente fuera. La lentitud de StringBuilder.delete (int start, int end) se debe a que copiará la matriz de nuevo para la parte de cambio de tamaño.

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

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