2011-12-18 20 views
12

Para mejorar su rendimiento, he estado perfilando una de mis aplicaciones con la muestra VisualVM, utilizando un período de muestreo mínimo de 20 ms. Según el generador de perfiles, el hilo principal gasta casi un cuarto de su tiempo de CPU en el método DecimalFormat.format().¿Una alternativa más rápida a DecimalFormat.format()?

estoy usando DecimalFormat.format() con el patrón 0.000000 "convertir" double números a una representación de cadena con exactamente seis dígitos decimales. Sé que este método es relativamente caro y es llamado muchas veces, pero todavía estaba algo sorprendido por estos resultados.

  1. ¿Hasta qué punto son precisos los resultados de dicho generador de perfiles de muestreo? ¿Cómo podría verificarlos? ¿Preferiblemente sin recurrir a un perfilador de instrumentos?

  2. ¿Existe una alternativa más rápida a DecimalFormat para mi caso de uso? ¿Tendría sentido implementar mi propia subclase NumberFormat?

UPDATE:

creé una micro-punto de referencia para comparar el rendimiento de los tres métodos siguientes:

  • DecimalFormat.format(): Single DecimalFormat objeto reutilizados varias veces.

  • String.format(): Múltiples llamadas independientes. Internamente este método se reduce a

    public static String format(String format, Object ... args) { 
        return new Formatter().format(format, args).toString(); 
    } 
    

    tanto que esperaba su rendimiento sea muy similar a Formatter.format().

  • Formatter.format(): Solo Formatter objeto reutilizado varias veces.

    Este método es un poco incómoda - Formatter objetos creados con el constructor por defecto anexar todas las cadenas creadas por el método format() a un StringBuilder objeto interno, que no es adecuadamente accesible y por lo tanto no se pueden borrar. Como consecuencia, múltiples llamadas a format() crearán una concatenación de todas las cadenas resultantes.

    Para solucionar este problema, proporcioné mi propia instancia de StringBuilder que borré antes de su uso con una llamada setLength(0).

Los resultados donde interesante:

  • DecimalFormat.format() era la línea de base en 1.4us por llamada.
  • String.format() fue más lento en un factor de dos a 2.7us por llamada.
  • Formatter.format() también fue más lento en un factor de dos a 2.5us por llamada.

En este momento parece que DecimalFormat.format() sigue siendo la más rápida entre estas alternativas.

Respuesta

9

Puede escribir su propia rutina dado que sabe exactamente lo que quiere.

public static void appendTo6(StringBuilder builder, double d) { 
    if (d < 0) { 
     builder.append('-'); 
     d = -d; 
    } 
    if (d * 1e6 + 0.5 > Long.MAX_VALUE) { 
     // TODO write a fall back. 
     throw new IllegalArgumentException("number too large"); 
    } 
    long scaled = (long) (d * 1e6 + 0.5); 
    long factor = 1000000; 
    int scale = 7; 
    long scaled2 = scaled/10; 
    while (factor <= scaled2) { 
     factor *= 10; 
     scale++; 
    } 
    while (scale > 0) { 
     if (scale == 6) 
      builder.append('.'); 
     long c = scaled/factor % 10; 
     factor /= 10; 
     builder.append((char) ('0' + c)); 
     scale--; 
    } 
} 

@Test 
public void testCases() { 
    for (String s : "-0.000001,0.000009,-0.000010,0.100000,1.100000,10.100000".split(",")) { 
     double d = Double.parseDouble(s); 
     StringBuilder sb = new StringBuilder(); 
     appendTo6(sb, d); 
     assertEquals(s, sb.toString()); 
    } 
} 

public static void main(String[] args) { 
    StringBuilder sb = new StringBuilder(); 
    long start = System.nanoTime(); 
    final int runs = 20000000; 
    for (int i = 0; i < runs; i++) { 
     appendTo6(sb, i * 1e-6); 
     sb.setLength(0); 
    } 
    long time = System.nanoTime() - start; 
    System.out.printf("Took %,d ns per append double%n", time/runs); 
} 

impresiones

Took 128 ns per append double 

Si desea aún más el rendimiento que puede escribir en un directo ByteBuffer (suponiendo que desea escribir los datos en alguna parte) lo que los datos que usted produce no necesitan ser copiados o codificada . (Asumiendo que está bien)

NOTA: esto está limitado a valores positivos/negativos de menos de 9 billones (Long.MAX_VALUE/1e6) Puede agregar un manejo especial si esto pudiera ser un problema.

+2

+1 Estaba a punto de escribir algo yo mismo: este fragmento de código podría ser un buen punto de partida. – thkala

+1

Finalmente escribí un formateador usando mi propio algoritmo y es aproximadamente cuatro veces más rápido que 'DecimalFormat' para el subconjunto de funcionalidad que necesito. Creo que todavía hay margen de mejora, ya que en realidad no bajé al nivel de agregar dígitos individuales. Aceptaré esta respuesta ya que es la única que contiene código utilizable. – thkala

+0

Gran trabajo Peter. He reutilizado tu código para crear el formateador rápido 'double' e incluso twiece faster' String' (contiene valor doble). – dantuch

2

Tal vez su programa no hace mucho trabajo intensivo y por lo tanto, parece que este hace lo mejor, al machacar algunos números.

Mi punto es que los resultados siguen siendo relativos a su aplicación.

Ponga un temporizador alrededor de cada DecimalFormatter.format() y vea cuántos millis está utilizando para obtener una imagen más clara.

Pero si usted todavía está preocupado por eso, aquí es un artículo que te pueden gustar:
http://onjava.com/pub/a/onjava/2000/12/15/formatting_doubles.html

+0

+1 para el enlace del artículo - Probablemente tenga que escribir mi propia implementación de formateador. – thkala

0

Una alternativa sería el uso de la cadena Formatter, darle una oportunidad para ver si tiene un mejor rendimiento:

String.format("%.6f", 1.23456789) 

O mejor aún, crear una sola formateador y reutilizarla - siempre y cuando no hay problemas de multiproceso, ya formateadores no son necesariamente seguros para el acceso multiproceso:

Formatter formatter = new Formatter(); 
// presumably, the formatter would be called multiple times 
System.out.println(formatter.format("%.6f", 1.23456789)); 
formatter.close(); 
+1

reutilizar sería bueno, pero los formateadores no son seguros para subprocesos, por lo que tendría que comprobar si este formateador en particular puede manejar varios subprocesos (como por ejemplo en una aplicación web) – extraneon

+0

@extraneon gracias por el comentario, edité mi respuesta en consecuencia . –

+1

Ambos 'String.format()' y 'Formatter.format()' parecen ser más lentos que 'DecimalFormat.format()'. Sospecho que es porque la cadena de patrones debe analizarse todas y cada una de las veces. 'Formatter.format()' también es más difícil de reutilizar: vea mi edición para más detalles. – thkala

0

La respuesta aceptada (escriba su propio formateador personalizado) es correcta, pero el formato deseado de OP es un tanto inusual, ¿por qué no será tan útil para otros?

Aquí hay una implementación personalizada para números que: requieren separadores de coma; tener hasta dos lugares decimales Esto es útil para empresas, cosas como monedas y porcentajes.

/** 
* Formats a decimal to either zero (if an integer) or two (even if 0.5) decimal places. Useful 
* for currency. Also adds commas. 
* <p> 
* Note: Java's <code>DecimalFormat</code> is neither Thread-safe nor particularly fast. This is our attempt to improve it. Basically we pre-render a bunch of numbers including their 
* commas, then concatenate them. 
*/ 

private final static String[] PRE_FORMATTED_INTEGERS = new String[500_000]; 

static { 
    for (int loop = 0, length = PRE_FORMATTED_INTEGERS.length; loop < length; loop++) { 

     StringBuilder builder = new StringBuilder(Integer.toString(loop)); 

     for (int loop2 = builder.length() - 3; loop2 > 0; loop2 -= 3) { 
      builder.insert(loop2, ','); 
     } 

     PRE_FORMATTED_INTEGERS[loop] = builder.toString(); 
    } 
} 

public static String formatShortDecimal(Number decimal, boolean removeTrailingZeroes) { 

    if (decimal == null) { 
     return "0"; 
    } 

    // Use PRE_FORMATTED_INTEGERS directly for short integers (fast case) 

    boolean isNegative = false; 

    int intValue = decimal.intValue(); 
    double remainingDouble; 

    if (intValue < 0) { 
     intValue = -intValue; 
     remainingDouble = -decimal.doubleValue() - intValue; 
     isNegative = true; 
    } else { 
     remainingDouble = decimal.doubleValue() - intValue; 
    } 

    if (remainingDouble > 0.99) { 
     intValue++; 
     remainingDouble = 0; 
    } 

    if (intValue < PRE_FORMATTED_INTEGERS.length && remainingDouble < 0.01 && !isNegative) { 
     return PRE_FORMATTED_INTEGERS[intValue]; 
    } 

    // Concatenate our pre-formatted numbers for longer integers 

    StringBuilder builder = new StringBuilder(); 

    while (true) { 
     if (intValue < PRE_FORMATTED_INTEGERS.length) { 
      String chunk = PRE_FORMATTED_INTEGERS[intValue]; 
      builder.insert(0, chunk); 
      break; 
     } 
     int nextChunk = intValue/1_000; 
     String chunk = PRE_FORMATTED_INTEGERS[intValue - (nextChunk * 1_000) + 1_000]; 
     builder.insert(0, chunk, 1, chunk.length()); 
     intValue = nextChunk; 
    } 

    // Add two decimal places (if any) 

    if (remainingDouble >= 0.01) { 
     builder.append('.'); 
     intValue = (int) Math.round((remainingDouble + 1) * 100); 
     builder.append(PRE_FORMATTED_INTEGERS[intValue], 1, PRE_FORMATTED_INTEGERS[intValue].length()); 

     if (removeTrailingZeroes && builder.charAt(builder.length() - 1) == '0') { 
      builder.deleteCharAt(builder.length() - 1); 
     } 
    } 

    if (isNegative) { 
     builder.insert(0, '-'); 
    } 

    return builder.toString(); 
} 

Este micro-punto de referencia muestra que es 2 veces más rápido que DecimalFormat (pero por supuesto tu caso es distinto dependiendo de su caso de uso). Mejoras bienvenidas!

/** 
* Micro-benchmark for our custom <code>DecimalFormat</code>. When profiling, we spend a 
* surprising amount of time in <code>DecimalFormat</code>, as noted here 
* https://bugs.openjdk.java.net/browse/JDK-7050528. It is also not Thread-safe. 
* <p> 
* As recommended here 
* http://stackoverflow.com/questions/8553672/a-faster-alternative-to-decimalformat-format 
* we can write a custom format given we know exactly what output we want. 
* <p> 
* Our code benchmarks around 2x as fast as <code>DecimalFormat</code>. See micro-benchmark 
* below. 
*/ 

public static void main(String[] args) { 

    Random random = new Random(); 
    DecimalFormat format = new DecimalFormat("###,###,##0.##"); 

    for (int warmup = 0; warmup < 100_000_000; warmup++) { 
     MathUtils.formatShortDecimal(random.nextFloat() * 100_000_000); 
     format.format(random.nextFloat() * 100_000_000); 
    } 

    // DecimalFormat 

    long start = System.currentTimeMillis(); 

    for (int test = 0; test < 100_000_000; test++) { 
     format.format(random.nextFloat() * 100_000_000); 
    } 

    long end = System.currentTimeMillis(); 
    System.out.println("DecimalFormat: " + (end - start) + "ms"); 

    // Custom 

    start = System.currentTimeMillis(); 

    for (int test = 0; test < 100_000_000; test++) { 
     MathUtils.formatShortDecimal(random.nextFloat() * 100_000_000); 
    } 

    end = System.currentTimeMillis(); 
    System.out.println("formatShortDecimal: " + (end - start) + "ms"); 
} 
Cuestiones relacionadas