¿Debo usar el String.format () de Java si el rendimiento es importante?

Tenemos que construir cadenas todo el tiempo para la salida de registro, etc. Sobre las versiones de JDK hemos aprendido cuándo usar StringBuffer (muchos anexos, seguridad de subprocesos) y StringBuilder (muchos anexos, no seguros para subprocesos).

¿Cuál es el consejo sobre el uso de String.format() ? ¿Es eficiente, o nos vemos obligados a seguir con la concatenación para frases one-line donde el rendimiento es importante?

por ejemplo, viejo estilo feo,

 String s = "What do you get if you multiply " + varSix + " by " + varNine + "?"); 

frente a un nuevo estilo ordenado (y posiblemente lento),

 String s = String.format("What do you get if you multiply %d by %d?", varSix, varNine); 

Nota: mi caso de uso específico son los cientos de cadenas de registro de ‘una sola línea’ a lo largo de mi código. No implican un bucle, por lo que StringBuilder es demasiado pesado. Estoy interesado en String.format() específicamente.

Escribí una clase pequeña para probar cuál tiene el mejor rendimiento de los dos y + viene antes del formato. por un factor de 5 a 6. Pruébalo tú mismo

 import java.io.*; import java.util.Date; public class StringTest{ public static void main( String[] args ){ int i = 0; long prev_time = System.currentTimeMillis(); long time; for( i = 0; i< 100000; i++){ String s = "Blah" + i + "Blah"; } time = System.currentTimeMillis() - prev_time; System.out.println("Time after for loop " + time); prev_time = System.currentTimeMillis(); for( i = 0; i<100000; i++){ String s = String.format("Blah %d Blah", i); } time = System.currentTimeMillis() - prev_time; System.out.println("Time after for loop " + time); } } 

Ejecutando lo anterior para diferentes N muestra que ambos se comportan linealmente, pero String.format es 5-30 veces más lento.

La razón es que en la implementación actual, String.format primero analiza la entrada con expresiones regulares y luego completa los parámetros. Concatenación con más, por otro lado, se optimiza con javac (no con el JIT) y utiliza StringBuilder.append directamente.

Comparación de tiempo de ejecución

Tomé el código hhafez y agregué una prueba de memoria :

 private static void test() { Runtime runtime = Runtime.getRuntime(); long memory; ... memory = runtime.freeMemory(); // for loop code memory = memory-runtime.freeMemory(); 

Ejecuto esto por separado para cada enfoque, el operador ‘+’, String.format y StringBuilder (llamando a String ()), por lo que la memoria utilizada no se verá afectada por otros enfoques. Agregué más concatenaciones, haciendo que la cadena sea “Blah” + i + “Blah” + i + “Blah” + i + “Blah”.

El resultado es el siguiente (promedio de 5 carreras cada uno):
Tiempo de aproximación (ms) Memoria asignada (larga)
operador ‘+’ 747 320,504
String.format 16484 373,312
StringBuilder 769 57,344

Podemos ver que String ‘+’ y StringBuilder son prácticamente idénticos en cuanto al tiempo, pero StringBuilder es mucho más eficiente en el uso de la memoria. Esto es muy importante cuando tenemos muchas llamadas de registro (o cualquier otra instrucción que involucre cadenas) en un intervalo de tiempo lo suficientemente corto para que Garbage Collector no pueda limpiar las muchas instancias de cadenas resultantes del operador ‘+’.

Y una nota, por cierto, no se olvide de verificar el nivel de registro antes de construir el mensaje.

Conclusiones

  1. Seguiré usando StringBuilder.
  2. Tengo demasiado tiempo o muy poca vida.

Tu antiguo y feo estilo es comstackdo automáticamente por JAVAC 1.6 como:

 StringBuilder sb = new StringBuilder("What do you get if you multiply "); sb.append(varSix); sb.append(" by "); sb.append(varNine); sb.append("?"); String s = sb.toString(); 

Por lo tanto, no hay absolutamente ninguna diferencia entre esto y el uso de un StringBuilder.

String.format es mucho más pesado ya que crea un nuevo formateador, analiza su cadena de formato de entrada, crea un StringBuilder, agrega todo a él y llama a String ().

Todos los puntos de referencia presentados aquí tienen algunos defectos , por lo que los resultados no son confiables.

Me sorprendió que nadie usara JMH para la evaluación comparativa, así que lo hice.

Resultados:

 Benchmark Mode Cnt Score Error Units MyBenchmark.testOld thrpt 20 9645.834 ± 238.165 ops/s // using + MyBenchmark.testNew thrpt 20 429.898 ± 10.551 ops/s // using String.format 

Las unidades son operaciones por segundo, cuanto más, mejor. Código fuente de referencia . Se utilizó OpenJDK IcedTea 2.5.4 Java Virtual Machine.

Entonces, el estilo antiguo (usando +) es mucho más rápido.

El String.format de Java funciona así:

  1. analiza la cadena de formato, explotando en una lista de fragmentos de formato
  2. itera los trozos de formato, convirtiéndose en un StringBuilder, que básicamente es una matriz que se redimensiona a sí misma según sea necesario, copiando en una nueva matriz. esto es necesario porque aún no sabemos cuán grande es asignar la cadena final
  3. StringBuilder.toString () copia su búfer interno en una nueva cadena

si el destino final para estos datos es una transmisión (por ejemplo, representación de una página web o escritura en un archivo), puede ensamblar los fragmentos de formato directamente en la transmisión:

 new PrintStream(outputStream, autoFlush, encoding).format("hello {0}", "world"); 

Yo especulo que el optimizador optimizará el procesamiento de cadenas de formato. Si es así, te queda un rendimiento amortizado equivalente para desenrollar manualmente tu String.format en un StringBuilder.

Para expandir / corregir en la primera respuesta anterior, no es la traducción con la que String.format ayudaría, en realidad.
Lo que String.format ayudará cuando imprima una fecha / hora (o un formato numérico, etc.), donde haya diferencias de localización (l10n) (es decir, algunos países imprimirán 04Feb2009 y otros imprimirán Feb042009).
Con la traducción, solo está hablando de mover cualquier cadena externalizable (como mensajes de error y otras cosas) en un paquete de propiedades para que pueda usar el paquete adecuado para el idioma correcto, utilizando ResourceBundle y MessageFormat.

Mirando todo lo anterior, diría que la concatenación String.format vs. plain se reduce a lo que prefiera. Si prefiere mirar las llamadas a .format sobre la concatenación, entonces por supuesto, vaya con eso.
Después de todo, el código se lee mucho más de lo que está escrito.

En su ejemplo, probalby de rendimiento no es demasiado diferente, pero hay otros problemas a considerar: a saber, la fragmentación de la memoria. Incluso la operación de concatenación está creando una nueva cadena, incluso si es temporal (lleva tiempo GC y es más trabajo). String.format () es más legible e implica menos fragmentación.

Además, si está utilizando mucho un formato particular, no olvide que puede usar la clase Formatter () directamente (todo String.format () crea una instancia de un solo uso de Formatter).

Además, debe tener en cuenta algo más: tenga cuidado al usar la subcadena (). Por ejemplo:

 String getSmallString() { String largeString = // load from file; say 2M in size return largeString.substring(100, 300); } 

Esa cadena grande todavía está en la memoria porque así es cómo funcionan las subcadenas de Java. Una mejor versión es:

  return new String(largeString.substring(100, 300)); 

o

  return String.format("%s", largeString.substring(100, 300)); 

La segunda forma es probablemente más útil si estás haciendo otras cosas al mismo tiempo.

En general, debe utilizar String.Format porque es relativamente rápido y admite la globalización (suponiendo que realmente está tratando de escribir algo leído por el usuario). También hace que sea más fácil globalizar si tratas de traducir una cadena en lugar de 3 o más por statement (especialmente para los idiomas que tienen estructuras gtwigticales drásticamente diferentes).

Ahora bien, si nunca planea traducir nada, confíe en la conversión de Java de los operadores + en StringBuilder . O use el StringBuilder de Java explícitamente.

Otra perspectiva desde el punto de vista Logging Only.

Veo mucha discusión relacionada con iniciar sesión en este hilo, así que pensé en agregar mi experiencia en respuesta. Puede ser que alguien lo encuentre útil.

Supongo que la motivación del registro mediante el formateador proviene de evitar la concatenación de cadenas. Básicamente, no desea tener una sobrecarga de string concat si no va a iniciar sesión.

Realmente no necesita concat / format a menos que quiera iniciar sesión. Digamos si defino un método como este

 public void logDebug(String... args, Throwable t) { if(debugOn) { // call concat methods for all args //log the final debug message } } 

En este enfoque el cancat / formateador realmente no se llama en absoluto si es un mensaje de depuración y debugOn = falso

Aunque todavía será mejor usar StringBuilder en lugar de formatear aquí. La motivación principal es evitar todo eso.

Al mismo tiempo, no me gusta agregar un bloque “if” para cada statement de registro desde

  • Afecta la legibilidad
  • Reduce la cobertura en las pruebas de mi unidad; eso es confuso cuando quieres asegurarte de que cada línea sea probada.

Por lo tanto, prefiero crear una clase de utilidad de registro con métodos como los anteriores y utilizarlo en todas partes sin preocuparme por el rendimiento alcanzado ni por otros problemas relacionados.

Acabo de modificar la prueba de hhafez para incluir StringBuilder. StringBuilder es 33 veces más rápido que String.format con el cliente jdk 1.6.0_10 en XP. El uso del interruptor -server reduce el factor a 20.

 public class StringTest { public static void main( String[] args ) { test(); test(); } private static void test() { int i = 0; long prev_time = System.currentTimeMillis(); long time; for ( i = 0; i < 1000000; i++ ) { String s = "Blah" + i + "Blah"; } time = System.currentTimeMillis() - prev_time; System.out.println("Time after for loop " + time); prev_time = System.currentTimeMillis(); for ( i = 0; i < 1000000; i++ ) { String s = String.format("Blah %d Blah", i); } time = System.currentTimeMillis() - prev_time; System.out.println("Time after for loop " + time); prev_time = System.currentTimeMillis(); for ( i = 0; i < 1000000; i++ ) { new StringBuilder("Blah").append(i).append("Blah"); } time = System.currentTimeMillis() - prev_time; System.out.println("Time after for loop " + time); } } 

Si bien esto puede sonar drástico, lo considero relevante sólo en casos excepcionales, porque los números absolutos son bastante bajos: 4 s por 1 millón de llamadas String.format simples está algo bien, siempre y cuando las use para el registro o la me gusta.

Actualización: como lo señala sjbotha en los comentarios, la prueba StringBuilder no es válida, ya que falta un .toString() final .toString() .

El factor de String.format(.) de String.format(.) StringBuilder es 23 en mi máquina (16 con el conmutador -server ).

Aquí está la versión modificada de la entrada hhafez. Incluye una opción de generador de cadenas.

 public class BLA { public static final String BLAH = "Blah "; public static final String BLAH2 = " Blah"; public static final String BLAH3 = "Blah %d Blah"; public static void main(String[] args) { int i = 0; long prev_time = System.currentTimeMillis(); long time; int numLoops = 1000000; for( i = 0; i< numLoops; i++){ String s = BLAH + i + BLAH2; } time = System.currentTimeMillis() - prev_time; System.out.println("Time after for loop " + time); prev_time = System.currentTimeMillis(); for( i = 0; i 

}

Tiempo después para el ciclo 391 Tiempo después para el ciclo 4163 Tiempo después para el ciclo 227

La respuesta a esto depende mucho de cómo su comstackdor Java específico optimiza el bytecode que genera. Las cadenas son inmutables y, teóricamente, cada operación “+” puede crear una nueva. Sin embargo, es casi seguro que el comstackdor optimice los pasos intermedios para crear cadenas largas. Es muy posible que ambas líneas de código generen exactamente el mismo código de bytes.

La única forma real de saber es probar el código de forma iterativa en su entorno actual. Escriba una aplicación QD que concatene cadenas en ambas direcciones de forma iterativa y vea cómo se interrumpen el tiempo el uno contra el otro.

Considere usar "hello".concat( "world!" ) Para un pequeño número de cadenas en la concatenación. Podría ser incluso mejor para el rendimiento que otros enfoques.

Si tiene más de 3 cadenas, considere utilizar StringBuilder o simplemente String, según el comstackdor que use.

Intereting Posts