Более быстрая альтернатива DecimalFormat.format()?
Чтобы повысить производительность, я профилировал одно из своих приложений с помощью пробоотборника VisualVM, используя минимальный период выборки 20 мс. Согласно профилировщику, основной поток затрачивает почти четверть своего процессорного времени в метод DecimalFormat.format()
.
Я использую DecimalFormat.format()
с шаблоном 0.000000
для "преобразования" double
чисел в строковое представление с ровно шестью десятичными цифрами. Я знаю, что этот метод относительно дорог, и его называют много раз, но я все еще несколько удивлялся этим результатам.
-
В какой степени результаты такого профилирования выборки точны? Как я буду проверять их - предпочтительнее, не прибегая к инструментальному профилировщику?
-
Есть ли более быстрая альтернатива 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,4 раза за звонок.
-
String.format()
был медленнее в два раза по 2,7 раза за звонок.
-
Formatter.format()
также был медленнее в два раза по 2,5 раза за звонок.
Сейчас кажется, что DecimalFormat.format()
по-прежнему самый быстрый среди этих альтернатив.
Ответы
Ответ 1
Вы можете написать свою собственную рутину, если вы точно знаете, что хотите.
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
Возможно, ваша программа не делает много интенсивной работы, и поэтому это, по-видимому, делает больше всего - хруст некоторых номеров.
Я хочу сказать, что ваши результаты по-прежнему относительно вашего приложения.
Поместите таймер вокруг каждого DecimalFormatter.format() и посмотрите, сколько миллинов вы используете, чтобы получить более четкое изображение.
Но если вы все еще беспокоитесь об этом, вот статья, которая вам может понравиться:
http://onjava.com/pub/a/onjava/2000/12/15/formatting_doubles.html
Ответ 3
Альтернативой может быть использование строки 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();
Ответ 4
Принятый ответ (напишите свой собственный форматтер) правильный, но желаемый формат 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 <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
* http://stackoverflow.com/info/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" );
}