Более быстрая альтернатива DecimalFormat.format ()? - PullRequest
16 голосов
/ 18 декабря 2011

Чтобы улучшить его производительность, я профилировал одно из моих приложений с помощью сэмплера VisualVM, используя минимальный период выборки 20 мс. Согласно профилировщику, основной поток тратит почти четверть своего процессорного времени в методе DecimalFormat.format().

Я использую DecimalFormat.format() с шаблоном 0.000000 для "преобразования" double чисел в строковое представление с ровно шестью десятичными цифрами. Я знаю, что этот метод относительно дорог и его называют много раз, но я все равно был несколько удивлен этими результатами.

  1. Насколько точны результаты такого профилировщика выборки? Как мне их проверить - желательно, не прибегая к инструментальному профилированию?

  2. Есть ли более быстрая альтернатива DecimalFormat для моего варианта использования? Имеет ли смысл развернуть мой собственный NumberFormat подкласс?

UPDATE:

Я создал микро-бенчмарк для сравнения производительности следующих трех методов:

  • DecimalFormat.format(): один DecimalFormat объект используется повторно несколько раз.

  • String.format(): несколько независимых вызовов. Внутренне этот метод сводится к

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

    Поэтому я ожидал, что его производительность будет очень похожа на Formatter.format().

  • Formatter.format(): один Formatter объект используется повторно несколько раз.

    Этот метод немного неудобен - объекты Formatter, созданные с помощью конструктора по умолчанию, добавляют все строки, созданные методом format(), к внутреннему объекту StringBuilder, который недоступен должным образом и поэтому не может быть очищен. Как следствие, несколько вызовов format() создадут объединение всех результирующих строк.

    Чтобы обойти эту проблему, я предоставил свой собственный экземпляр StringBuilder, который я очистил перед использованием с вызовом setLength(0).

Результаты, где интересно:

  • DecimalFormat.format() был базовый уровень в 1.4us за звонок.
  • String.format() был медленнее в два раза - 2,7us за звонок.
  • Formatter.format() также был медленнее в два раза при 2,5us за звонок.

Сейчас кажется, что DecimalFormat.format() по-прежнему самый быстрый среди этих альтернатив.

Ответы [ 4 ]

10 голосов
/ 19 декабря 2011

Вы можете написать свою собственную программу, если точно знаете, чего хотите.

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);
}

печать

Took 128 ns per append double

Если вы хотите еще большей производительности, вы можете записать в прямой ByteBuffer (при условии, что вы хотите записать данные куда-нибудь), поэтому создаваемые вами данные необходимо скопировать или закодировать. (Предполагая, что это нормально)

ПРИМЕЧАНИЕ: это ограничено положительными / отрицательными значениями менее 9 триллионов (Long.MAX_VALUE / 1e6). Вы можете добавить специальную обработку, если это может быть проблемой.

2 голосов
/ 18 декабря 2011

Может быть, ваша программа не выполняет много интенсивной работы, и поэтому она, похоже, делает больше всего - сокращая некоторые числа.

Моя точка зрения заключается в том, что ваши результаты по-прежнему относительно вашего приложения.* Установите таймер вокруг каждого DecimalFormatter.format () и посмотрите, сколько миллис используется для получения более четкой картинки.

Но если вы все еще беспокоитесь об этом, вот статья, которая вам может понравиться:1007 * http://onjava.com/pub/a/onjava/2000/12/15/formatting_doubles.html

0 голосов
/ 11 апреля 2017

Принятый ответ (напишите свой собственный пользовательский форматер) правильный, но желаемый формат OP несколько необычен, поэтому, вероятно, не будет таким полезным для других?

Вот пользовательская реализация для чисел, которые: требуют запятых; иметь до двух знаков после запятой. Это полезно для корпоративных вещей, таких как валюты и проценты.

/**
 * 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();
}

Этот микро-тест показывает, что он в 2 раза быстрее, чем DecimalFormat (но, конечно, YMMV в зависимости от вашего варианта использования). Улучшения приветствуются!

/**
 * 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
 * /6392805/bolee-bystraya-alternativa-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" );
}
0 голосов
/ 18 декабря 2011

В качестве альтернативы можно использовать строку Formatter , попробуйте проверить, работает ли она лучше:

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

Или еще лучше создать отдельный форматтер и использовать его повторно.- пока нет проблем с многопоточностью, поскольку средства форматирования не обязательно безопасны для многопоточного доступа:

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