example - stringbuilder java 8
¿Es mejor reutilizar un StringBuilder en un bucle? (14)
Tengo una pregunta relacionada con el rendimiento con respecto al uso de StringBuilder. En un ciclo muy largo estoy manipulando un StringBuilder
y 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());
}
¿ StringBuilder
instancia de StringBuilder
en cada ciclo de ciclo 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());
}
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 las concatenaciones de String, el primer ejemplo en 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 tiene problemas con el rendimiento, entonces, optimícese. Empezaría especificando explícitamente el tamaño del buffer del StringBuilder, según Jon Skeet.
De acuerdo con mi experiencia con el desarrollo de software en Windows, diría que eliminar el StringBuilder durante el ciclo tiene un mejor rendimiento que crear una 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 contraria a lo que sugieren los demás. Mmm. Es hora de compararla).
De acuerdo, ahora entiendo lo que está pasando, y tiene sentido.
Tenía la impresión de que toString
acaba de pasar el char[]
subyacente char[]
en un constructor de cadenas que no tomó 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 de String
público que toma una copia.
Por lo tanto, en el caso de "reutilización de StringBuilder
" creamos genuinamente una copia de los datos por cadena, utilizando la misma matriz de caracteres en el búfer todo el tiempo. Obviamente, al crear un nuevo StringBuilder
cada vez se 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.
Declare una vez y asigne cada vez. Es un concepto más pragmático y reutilizable que una optimización.
Desgraciadamente, estoy en C # normalmente, creo que borrar un StringBuilder pesa más que la instanciación de uno nuevo.
El primero 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.
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
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 mejora en StringBuilder viene de darle un tamaño inicial para evitar que crezca más mientras corre el bucle
for (loop condition) {
StringBuilder sb = new StringBuilder(4096);
}
LOL, la primera vez que vi gente comparó 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 en 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. ¿Sabes cómo configuro la configuración del experimento? D
La JVM moderna es realmente inteligente sobre cosas como esta. No lo adivinaría en segundo lugar y haría algo hacky que es menos sostenible / legible ... a menos que haga benchmarks adecuados con datos de producción que validen una mejora de rendimiento no trivial (y documentarlo);
La forma 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, StringBuilder.delete () actualizará StringBuilder.count al nuevo tamaño. Mientras que StringBuilder.setLength () simplemente simplifica la actualización de StringBuilder.count al nuevo tamaño.
La razón por la cual hacer un ''setLength'' o ''delete'' 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, recomiendo dejar que el compilador realice las optimizaciones de cadenas . 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-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 buffer del tamaño esperado.
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 ocultarse a los objetos que usan 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, configurar el tamaño del búfer inicial de StringBuilder
dará una pequeña ganancia.
No es significativamente más rápido, pero según mis pruebas, en promedio muestra un par de millones de veces 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 );