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

215

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

¿Cuál es el consejo sobre el uso String.format()? ¿Es eficiente o nos vemos obligados a mantener la concatenación de líneas simples donde el rendimiento es importante?

por ejemplo, feo estilo antiguo,

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

vs. nuevo estilo ordenado (String.format, que posiblemente sea más 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 línea' en todo mi código. No implican un bucle, por lo que StringBuilderes demasiado pesado. Estoy interesado String.format()específicamente.

Aire
fuente
28
¿Por qué no lo pruebas?
Ed S.
1
Si está produciendo esta salida, supongo que debe ser legible por un humano como una tasa que un humano pueda leer. Digamos 10 líneas por segundo como máximo. Creo que encontrará que realmente no importa qué enfoque adopte, si es teóricamente más lento, el usuario podría apreciarlo. ;) Entonces no, StringBuilder no es pesado en la mayoría de las situaciones.
Peter Lawrey
99
@ Peter, ¡no, no es absolutamente para que los humanos lo lean en tiempo real! Está ahí para ayudar al análisis cuando las cosas salen mal. La salida del registro generalmente será de miles de líneas por segundo, por lo que debe ser eficiente.
Aire
55
si está produciendo muchos miles de líneas por segundo, sugeriría 1) usar texto más corto, incluso sin texto como CSV simple o binario 2) No usar String en absoluto, puede escribir los datos en un ByteBuffer sin crear cualquier objeto (como texto o binario) 3) en segundo plano la escritura de datos en el disco o un socket. Debería poder sostener alrededor de 1 millón de líneas por segundo. (Básicamente tanto como lo permita su subsistema de disco) Puede lograr ráfagas de 10 veces esto.
Peter Lawrey
77
Esto no es relevante para el caso general, pero para el registro en particular, LogBack (escrito por el autor original de Log4j) tiene una forma de registro parametrizado que aborda este problema exacto: logback.qos.ch/manual/architecture.html#ParametrizedLogging
Matt Passell el

Respuestas:

122

Escribí una clase pequeña para probar que tiene el mejor rendimiento de los dos y + viene antes del formato. por un factor de 5 a 6. Pruébelo usted 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);

    }
}

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

La razón es que en la implementación actual String.formatprimero analiza la entrada con expresiones regulares y luego completa los parámetros. La concatenación con más, por otro lado, se optimiza con javac (no con el JIT) y se usa StringBuilder.appenddirectamente.

Comparación de tiempo de ejecución

hhafez
fuente
12
Hay una falla con esta prueba en que no es del todo una buena representación de todo el formato de cadena. A menudo hay lógica involucrada en qué incluir y lógica para formatear valores específicos en cadenas. Cualquier prueba real debe mirar escenarios del mundo real.
Orion Adrian
99
Hubo otra pregunta sobre lo que alrededor de + versos StringBuffer, en las últimas versiones de Java + fue sustituido por StringBuffer cuando sea posible por lo que el rendimiento no sería diferente
hhafez
25
Esto se parece mucho al tipo de microbenchmark que se va a optimizar de una manera muy inútil.
David H. Clements
20
Otro micro-benchmark mal implementado. ¿Cómo se escalan ambos métodos por orden de magnitud? ¿Qué tal el uso de operaciones 100, 1000, 10000, 1000000? Si solo ejecuta una prueba, en un orden de magnitud, en una aplicación que no se ejecuta en un núcleo aislado; no hay forma de saber cuánta diferencia se puede descartar como "efectos secundarios" debido al cambio de contexto, procesos en segundo plano, etc.
Evan Plaice
8
Además, como nunca sales del JIT principal, no puedes
entrar en acción
241

Tomé hhafez código y añadido 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 ToString ()), 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".

Los resultados son los siguientes (promedio de 5 ejecuciones 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 el 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 declaración que involucre cadenas) en un intervalo de tiempo lo suficientemente corto como para que el recolector de basura no pueda limpiar las muchas instancias de cadenas que resultan del operador '+'.

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

Conclusiones:

  1. Seguiré usando StringBuilder.
  2. Tengo demasiado tiempo o muy poca vida.
Itamar
fuente
8
"No olvide verificar el nivel de registro antes de construir el mensaje", es un buen consejo, esto debe hacerse al menos para los mensajes de depuración, porque podría haber muchos de ellos y no deberían habilitarse en la producción.
stivlo
39
No, esto no está bien. Lamento ser franco, pero la cantidad de votos positivos que ha atraído es alarmante. El uso del +operador compila al StringBuildercódigo equivalente . Los microbenchmarks como este no son una buena forma de medir el rendimiento; por qué no usar jvisualvm, está en el jdk por una razón. String.format() será más lento, pero debido al tiempo para analizar la cadena de formato en lugar de las asignaciones de objetos. Aplazar la creación de artefactos de registro hasta que esté seguro de que son necesarios es un buen consejo, pero si tendría un impacto en el rendimiento, está en el lugar equivocado.
CurtainDog
1
@CurtainDog, su comentario fue hecho en una publicación de cuatro años, ¿puede señalar documentación o crear una respuesta separada para abordar la diferencia?
kurtzbot
1
Referencia en apoyo del comentario de @ CurtainDog: stackoverflow.com/a/1532499/2872712 . Es decir, se prefiere + a menos que se haga en un bucle.
albaricoque
And a note, BTW, don't forget to check the logging level before constructing the message.No es un buen consejo. Suponiendo que estamos hablando java.util.logging.*específicamente, verificar el nivel de registro es cuando se habla de hacer un procesamiento avanzado que causaría efectos adversos en un programa que no desearía cuando un programa no tiene el registro activado en el nivel apropiado. El formato de cadena no es ese tipo de procesamiento en absoluto. El formateo es parte del java.util.loggingmarco, y el registrador mismo verifica el nivel de registro antes de que se invoque el formateador.
searchengine27
30

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ó la máquina virtual Java OpenJDK IcedTea 2.5.4.

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

Adam Stelmaszczyk
fuente
55
Esto sería mucho más fácil de interpretar si anotara cuál fue "+" y cuál fue "formato".
AjahnCharles
21

JAVAC 1.6 compila automáticamente su viejo estilo feo 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 usar 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, lo agrega todo y llama aString ().

Raphaël
fuente
En términos de legibilidad, el código que publicaste es mucho más ... engorroso que String.format ("¿Qué obtienes si multiplicas% d por% d?", VarSix, varNine);
dusktreader
12
No hay diferencia entre +y de StringBuilderhecho. Lamentablemente, hay mucha información errónea en otras respuestas en este hilo. Estoy casi tentado a cambiar la pregunta a how should I not be measuring performance.
CurtainDog
12

El String.format de Java funciona así:

  1. analiza la cadena de formato, explotando en una lista de fragmentos de formato
  2. itera los fragmentos de formato, renderizándolos en un StringBuilder, que es básicamente una matriz que cambia de tamaño según sea necesario, copiando en una nueva matriz. esto es necesario porque aún no sabemos qué tan grande asignar la Cadena final
  3. StringBuilder.toString () copia su búfer interno en una nueva cadena

Si el destino final de estos datos es una secuencia (por ejemplo, renderizar una página web o escribir en un archivo), puede ensamblar los fragmentos de formato directamente en su secuencia:

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

Especulo que el optimizador optimizará el procesamiento de cadenas de formato. Si es así, tiene un rendimiento amortizado equivalente para desenrollar manualmente su String.format en un StringBuilder.

Dustin Getz
fuente
55
No creo que su especulación sobre la optimización del procesamiento de cadenas de formato sea correcta. En algunas pruebas del mundo real con Java 7, descubrí que usarlo String.formaten bucles internos (que se ejecuta millones de veces) resultó en más del 10% de mi tiempo de ejecución invertido java.util.Formatter.parse(String). Esto parece indicar que en los bucles internos, debe evitar llamar Formatter.formato cualquier cosa que lo llame, incluyendo PrintStream.format(una falla en la lib estándar de Java, IMO, especialmente porque no puede almacenar en caché la cadena de formato analizada).
Andy MacKinlay
8

Para expandir / corregir la primera respuesta anterior, no es la traducción con la que String.format ayudaría, en realidad.
Lo que String.format ayudará será 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 externa (como mensajes de error y demás) a un paquete de propiedades para que pueda usar el paquete correcto para el idioma correcto, utilizando ResourceBundle y MessageFormat.

Mirando todo lo anterior, diría que, en cuanto al rendimiento, String.format vs.concatenación simple se reduce a lo que prefieres. 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 se escribe.

dw.mackie
fuente
1
Diría que, en términos de rendimiento, String.format vs.concatenación simple se reduce a lo que prefieres. Creo que esto es incorrecto. En cuanto al rendimiento, la concatenación es mucho mejor. Para obtener más detalles, consulte mi respuesta.
Adam Stelmaszczyk
6

En su ejemplo, el rendimiento probalby no es muy diferente, pero hay otros problemas a considerar: 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 simplemente más legible e implica menos fragmentación.

Además, si usa mucho un formato particular, no olvide que puede usar la clase Formatter () directamente (todo lo que String.format () hace es instanciar una instancia de Formatter de un solo uso).

Además, debe tener en cuenta algo más: tenga cuidado de usar substring (). 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 como 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á haciendo otras cosas al mismo tiempo.

cletus
fuente
8
Vale la pena señalar que la "pregunta relacionada" es en realidad C # y, por lo tanto, no es aplicable.
Aire
¿Qué herramienta usaste para medir la fragmentación de la memoria y la fragmentación incluso hace una diferencia de velocidad para ram?
kritzikratzi
Vale la pena señalar que el método de subcadena se cambió de Java 7+. Ahora debería devolver una nueva representación de Cadena que contenga solo los caracteres subcadenados. Eso significa que no hay necesidad de devolver una llamada String :: new
João Rebelo
5

En general, debe usar String.Format porque es relativamente rápido y admite la globalización (suponiendo que realmente esté tratando de escribir algo que lea el usuario). También facilita la globalización si está tratando de traducir una cadena versus 3 o más por declaración (especialmente para lenguajes que tienen estructuras gramaticales drásticamente diferentes).

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

Orion Adrian
fuente
3

Otra perspectiva desde el punto de vista de registro solamente.

Veo mucha discusión relacionada con el inicio de 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 de iniciar sesión con formateador proviene de evitar la concatenación de cadenas. Básicamente, no desea tener una sobrecarga de string concat si no va a registrarlo.

Realmente no necesita concat / formato a menos que desee 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 no se llama realmente si es un mensaje de depuración y debugOn = false

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

Al mismo tiempo, no me gusta agregar el bloque "if" para cada instrucción de registro desde

  • Afecta la legibilidad
  • Reduce la cobertura en mis pruebas unitarias, eso es confuso cuando quieres asegurarte de que cada línea se pruebe.

Por lo tanto, prefiero crear una clase de utilidad de registro con métodos como el anterior y usarla en todas partes sin preocuparme por el impacto en el rendimiento y cualquier otro problema relacionado con él.

software.wikipedia
fuente
¿Podría aprovechar una biblioteca existente como slf4j-api que pretende abordar este caso de uso con su función de registro parametrizado? slf4j.org/faq.html#logging_performance
ammianus
2

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, considero que es relevante solo en casos raros, porque los números absolutos son bastante bajos: 4 s por 1 millón de llamadas simples de String.format es algo correcto, siempre que los use para iniciar sesión o me gusta.

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

El factor de aceleración correcto de String.format(.)a StringBuilderes 23 en mi máquina (16 con el -serverinterruptor).

the.duckman
fuente
1
Su prueba no es válida porque no tiene en cuenta el tiempo consumido simplemente con un bucle. Debe incluir eso y restarlo de todos los demás resultados, como mínimo (sí, puede ser un porcentaje significativo).
cletus
Lo hice, el bucle for tarda 0 ms. Pero incluso si tomara tiempo, esto solo aumentaría el factor.
the.duckman
3
La prueba de StringBuilder no es válida porque no llama a toString () al final para darle una cadena que puede usar. Agregué esto y el resultado es que StringBuilder lleva aproximadamente la misma cantidad de tiempo que +. Estoy seguro de que a medida que aumente el número de anexos, eventualmente se volverá más barato.
Sarel Botha
1

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<numLoops; i++){
        String s = String.format(BLAH3, i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        StringBuilder sb = new StringBuilder();
        sb.append(BLAH);
        sb.append(i);
        sb.append(BLAH2);
        String s = sb.toString();
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

}

}

Tiempo posterior para el ciclo 391 Tiempo posterior para el ciclo 4163 Tiempo posterior para el ciclo 227

LUEGO
fuente
0

La respuesta a esto depende mucho de cómo su compilador Java específico optimiza el código de bytes que genera. Las cadenas son inmutables y, en teoría, cada operación "+" puede crear una nueva. Pero, su compilador seguramente optimiza los pasos intermedios en la construcción de cadenas largas. Es completamente posible que ambas líneas de código anteriores generen exactamente el mismo bytecode.

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 de forma iterativa y vea cómo se agota el tiempo una contra la otra.

Sí, ese Jake.
fuente
1
El bytecode para el segundo ejemplo seguramente llama a String.format, pero me horrorizaría si lo hiciera una simple concatenación. ¿Por qué usaría el compilador una cadena de formato que luego tendría que analizarse?
Jon Skeet
Usé "bytecode" donde debería haber dicho "código binario". Cuando todo se reduce a jmps y mov, puede ser exactamente el mismo código.
Sí, ese Jake.
0

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

Si tiene más de 3 cadenas, considere usar StringBuilder, o simplemente String, dependiendo del compilador que use.

Sasa
fuente