DecimalFormat.format()的更快替代方案?

为了提高性能,我使用VisualVM采样器分析了我的一个应用程序,使用的最小采样周期为20ms。 根据分析器,主线程在DecimalFormat.format()方法中花费了近四分之一的CPU时间。

我使用带有0.000000模式的DecimalFormat.format()double数字“转换”为具有正好六位小数的字符串表示。 我知道这种方法相对昂贵并且称为很多次,但我仍然对这些结果感到有些惊讶。

  1. 这种采样分析仪的结果在多大程度上准确? 我将如何validation它们 – 最好不要求助于仪器分析仪?

  2. 对于我的用例,是否有更快的替代DecimalFormat ? 推出自己的NumberFormat子类是否有意义?

更新:

我创建了一个微基准来比较以下三种方法的性能:

  • 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()仍然是这些替代品中最快的。

鉴于您确切知道自己想要什么,您可以编写自己的例程。

 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)如果这可能是一个问题,您可以添加特殊处理。

也许你的程序没有做太多密集的工作,所以这似乎做得最多 – 处理一些数字。

我的观点是,您的结果仍然与您的应用相关。

在每个DecimalFormatter.format()周围放一个计时器,看看你用了多少毫秒来获得更清晰的画面。

但是,如果你仍然担心它,你可能会喜欢这篇文章:
http://onjava.com/pub/a/onjava/2000/12/15/formatting_doubles.html

另一种方法是使用字符串Formatter ,尝试看它是否表现更好:

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

或者更好的是,创建一个格式化程序并重用它 – 只要没有multithreading问题,因为格式化程序对于multithreading访问不一定安全:

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

接受的答案(编写自己的自定义格式化程序)是正确的,但OP的所需格式有点不寻常,所以可能不会对其他人有帮助吗?

以下是数字的自定义实现:需要逗号分隔符; 最多有两位小数。 这对于诸如货币和百分比之类的企业非常有用。

 /** * Formats a decimal to either zero (if an integer) or two (even if 0.5) decimal places. Useful * for currency. Also adds commas. * 

* Note: Java's DecimalFormat 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(); }

这个微基准测试显示它比DecimalFormat快2倍(但当然YMMV取决于你的用例)。 欢迎改进!

 /** * Micro-benchmark for our custom DecimalFormat. When profiling, we spend a * surprising amount of time in DecimalFormat, as noted here * https://bugs.openjdk.java.net/browse/JDK-7050528. It is also not Thread-safe. * 

* 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. *

* Our code benchmarks around 2x as fast as DecimalFormat. 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" ); }