Compromisos de rendimiento y simplicidad entre String, StringBuffer y StringBuilder

¿Alguna vez has pensado en las implicaciones de este cambio en el lenguaje de progtwigción Java?

La clase String fue concebida como una clase inmutable (y esta decisión fue pensada intencionalmente). Pero la concatenación de cuerdas es muy lenta, la he evaluado yo mismo. Así nació el StringBuffer. Muy buena clase, sincronizada y realmente rápida. Pero algunas personas no estaban satisfechas con el costo de rendimiento de algunos bloques sincronizados, y se introdujo el StringBuilder.

Pero, cuando se usa la Cadena para concatenar no demasiados objetos, la inmutabilidad de la clase hace que sea una forma realmente natural de lograr la seguridad de la rosca. Puedo entender el uso de StringBuffer cuando queremos administrar varias cadenas. Pero, aquí está mi primera pregunta:

  1. Si tiene, digamos, 10 o menos cadenas que desea agregar, por ejemplo, ¿cambiaría la simplicidad por solo unos milisegundos en tiempo de ejecución?

    También he evaluado a StringBuilder. Es más eficiente que StringBuffer (solo una mejora del 10%). Pero, si en su progtwig de subproceso único está utilizando StringBuilder, ¿qué sucede si a veces desea cambiar el diseño para usar varios subprocesos? Tienes que cambiar cada instancia de StringBuilder, y si olvidas una, tendrás un efecto extraño (dado el estado de la carrera que puede surgir) que se puede producir.

  2. En esta situación, ¿cambiaría el rendimiento por horas de depuración?

Ok eso es todo Más allá de la pregunta simple (StringBuffer es más eficiente que “+” y es seguro para subprocesos, y StringBuilder es más rápido que StringBuffer pero no es seguro para subprocesos) Me gustaría saber cuándo usarlos.

(Importante: conozco las diferencias entre ellos; esta es una pregunta relacionada con la architecture de la plataforma y algunas decisiones de diseño).

Hoy en día, tanto StringBuffer como Builder son inútiles (desde el punto de vista del rendimiento). Explico por qué

Se suponía que StringBuilder era más rápido que StringBuffer pero cualquier JVM sensata puede optimizar la sincronización. Así que fue una gran falla (y un pequeño golpe) cuando se introdujo.

StringBuffer usó NO para copiar el char [] al crear el String (en variante no compartida); sin embargo, esa fue una de las principales fonts de problemas, incluida la filtración de grandes caracteres [] para cadenas pequeñas. En 1.5 decidieron que una copia del char [] debía aparecer cada vez y que prácticamente hacía que el StringBuffer fuera inútil (la sincronización estaba allí para asegurar que ningún juego de hilos pueda engañar al String). Sin embargo, eso conserva la memoria y, en última instancia, ayuda al GC (además de la huella obviamente reducida), por lo general, el carácter [] es el top3 de los objetos que consumen memoria.

String.concat fue y sigue siendo la forma más rápida de concatenar 2 cadenas (y 2 solo … o posiblemente 3). Tenlo en cuenta, no realiza una copia adicional del char [].

De vuelta a la parte inútil, ahora cualquier código de terceros puede lograr el mismo rendimiento que StringBuilder. Incluso en java1.1 solía tener un nombre de clase AsycnStringBuffer que hizo exactamente lo mismo que StringBuilder ahora, pero aún así asigna mayor char [] que StringBuilder. Ambos StrinBuffer / StringBuilder están optimizados para cadenas pequeñas por defecto, puedes ver el c-tor

StringBuilder(String str) { super(str.length() + 16); append(str); } 

Por lo tanto, si la segunda cadena es más larga que 16chars, obtiene otra copia del carácter subyacente []. Bastante uncool

Eso puede ser un efecto secundario del bash de ajustar tanto StringBuilder / Buffer como el char [] en la misma línea de caché (en x86) en el sistema operativo de 32 bits … pero no estoy seguro.

En cuanto a la observación de las horas de depuración, etc. Use su criterio, personalmente no recuerdo haber tenido ningún problema con las operaciones de las cadenas, aparte de la implícita. Estructura de cuerda similar para el generador de sql de JDO impl.


Edición: a continuación ilustro lo que los diseñadores de java no hicieron para acelerar las operaciones de String. Por favor, tenga en cuenta que la clase está destinada para el paquete java.lang y que se puede colocar solo agregándola a la ruta de clases de arranque. Sin embargo, incluso si no se pone ahí (¡la diferencia es una sola línea de código!), Sería aún más rápido que StringBuilder, ¿impactante? La clase hubiera hecho string1 + string2 + … mucho mejor que usar StringBuilder, pero bueno …

 package java.lang; public class FastConcat { public static String concat(String s1, String s2){ s1=String.valueOf(s1);//null checks s2=String.valueOf(s2); return s1.concat(s2); } public static String concat(String s1, String s2, String s3){ s1=String.valueOf(s1);//null checks s2=String.valueOf(s2); s3=String.valueOf(s3); int len = s1.length()+s2.length()+s3.length(); char[] c = new char[len]; int idx=0; idx = copy(s1, c, idx); idx = copy(s2, c, idx); idx = copy(s3, c, idx); return newString(c); } public static String concat(String s1, String s2, String s3, String s4){ s1=String.valueOf(s1);//null checks s2=String.valueOf(s2); s3=String.valueOf(s3); s4=String.valueOf(s4); int len = s1.length()+s2.length()+s3.length()+s4.length(); char[] c = new char[len]; int idx=0; idx = copy(s1, c, idx); idx = copy(s2, c, idx); idx = copy(s3, c, idx); idx = copy(s4, c, idx); return newString(c); } private static int copy(String s, char[] c, int idx){ s.getChars(c, idx); return idx+s.length(); } private static String newString(char[] c){ return new String(0, c.length, c); //return String.copyValueOf(c);//if not in java.lang } } 

Solo un comentario sobre el comentario de “StringBuilders and threads”: incluso en progtwigs de múltiples subprocesos, es muy raro querer construir una cadena a través de múltiples hilos. Normalmente, cada subproceso tendrá algún conjunto de datos y creará una cadena a partir de eso, a menudo concatenando varias cadenas juntas. Luego, convertirán ese StringBuilder en una cadena, y esa cadena se puede compartir de manera segura entre las hebras.

No creo que haya visto un error debido a un StringBuilder compartido entre subprocesos.

Personalmente desearía que StringBuffer no existiera, fue en la fase de “vamos a sincronizar todo” de Java, lo que lleva a Vector y Hashtable que han quedado casi obsoletos por las clases no sincronizadas de ArrayList y HashMap de Java 2. Solo demoró un poco. para que llegue el equivalente no sincronizado de StringBuffer .

Así que básicamente:

  • Use la cadena cuando no quiera realizar manipulaciones y asegúrese de que nada más lo hará
  • Utilice StringBuilder para realizar la manipulación, generalmente en un período corto
  • Evite StringBuffer menos que realmente lo necesite, y como digo, no recuerdo haber visto nunca una situación en la que usaría StringBuffer lugar de StringBuilder , cuando ambos están disponibles.

StringBuffer estaba en Java 1.0; No fue ningún tipo de reacción ante la lentitud o la inmutabilidad. Tampoco es en modo alguno más rápido o mejor que la concatenación de cadenas; De hecho, el comstackdor de Java se comstack.

 String s1 = s2 + s3; 

en algo como

 String s1 = new StringBuilder(s2).append(s3).toString(); 

Si no me crees, pruébalo con un desensamblador (javap -c, por ejemplo).

Lo que pasa con “StringBuffer es más rápido que la concatenación” se refiere a la concatenación repetida . En ese caso, crear explícitamente su propio StringBuffer y usarlo repetidamente funciona mejor que dejar que el comstackdor cree muchos de ellos.

StringBuilder se introdujo en Java 5 por razones de rendimiento, como usted dice. La razón por la que tiene sentido es que StringBuffer / Builder prácticamente nunca se comparten fuera del método que los crea: el 99% de su uso es algo como lo anterior, donde se crean, se utilizan para agregar algunas cadenas y se descartan.

Intenté lo mismo en una máquina XP. el StringBuilder ES algo más rápido, pero si invierte el orden de la ejecución, o realiza varias ejecuciones, notará que el “casi factor dos” en los resultados se cambiará en una ventaja del 10%:

 StringBuffer build & output duration= 4282,000000 µs StringBuilder build & output duration= 4226,000000 µs StringBuffer build & output duration= 4439,000000 µs StringBuilder build & output duration= 3961,000000 µs StringBuffer build & output duration= 4801,000000 µs StringBuilder build & output duration= 4210,000000 µs 

Para tu tipo de prueba, la JVM NO te ayudará. Tuve que limitar el número de ejecuciones y elementos solo para obtener CUALQUIER resultado de una prueba de “Sólo cadena”.

Decidió poner las opciones a prueba con una composición simple de ejercicio XML. Pruebas realizadas en un i5 de 2.7GHz con 16Gb de memoria RAM DDR3 para aquellos que desean replicar resultados.

Código:

  private int testcount = 1000; private int elementCount = 50000; public void testStringBuilder() { long total = 0; int counter = 0; while (counter++ < testcount) { total += doStringBuilder(); } float f = (total/testcount)/1000; System.out.printf("StringBuilder build & output duration= %f µs%n%n", f); } private long doStringBuilder(){ long start = System.nanoTime(); StringBuilder buffer = new StringBuilder("\n"); buffer.append(""); for (int i =0; i < elementCount; i++) { buffer.append(""); } buffer.append(""); //System.out.println(buffer.toString()); output = buffer.toString(); long end = System.nanoTime(); return end - start; } public void testStringBuffer(){ long total = 0; int counter = 0; while (counter++ < testcount) { total += doStringBuffer(); } float f = (total/testcount)/1000; System.out.printf("StringBuffer build & output duration= %f µs%n%n", f); } private long doStringBuffer(){ long start = System.nanoTime(); StringBuffer buffer = new StringBuffer("\n"); buffer.append(""); for (int i =0; i < elementCount; i++) { buffer.append(""); } buffer.append(""); //System.out.println(buffer.toString()); output = buffer.toString(); long end = System.nanoTime(); return end - start; } 

Resultados:

 En la máquina OSX:

 StringBuilder duración de comstackción y salida = 1047.000000 µs 

 StringBuffer duración de comstackción y salida = 1844.000000 µs 


 En la máquina Win7:
 StringBuilder duración de comstackción y salida = 1869.000000 µs 

 StringBuffer duración de comstackción y salida = 2122.000000 µs

Parece que la mejora del rendimiento puede ser específica de la plataforma, dependiendo de cómo JVM implementa la sincronización.

Referencias:

El uso de System.nanoTime () se ha cubierto aquí -> ¿System.nanoTime () es completamente inútil? y aquí -> ¿Cómo cronometro la ejecución de un método en Java? .

Fuente de StringBuilder y StringBuffer aquí -> http://www.java2s.com/Open-Source/Java-Document/6.0-JDK-Core/lang/java.lang.htm

Buena descripción general de la sincronización aquí -> http://www.javaworld.com/javaworld/jw-07-1997/jw-07-hood.html?page=1