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

Posted

技术标签:

【中文标题】DecimalFormat.format() 的更快替代方案?【英文标题】:A faster alternative to DecimalFormat.format()? 【发布时间】:2012-01-23 03:08:55 【问题描述】:

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

我正在使用DecimalFormat.format()0.000000 模式将double 数字“转换”为恰好包含六位十进制数字的字符串表示形式。我知道这种方法比较昂贵,而且它调用了很多次,但我仍然对这些结果感到有些惊讶。

    这种采样分析器的结果准确到什么程度?我将如何验证它们 - 最好不使用检测分析器?

    对于我的用例,有没有比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() 仍然是这些替代方案中最快的。

【问题讨论】:

【参考方案1】:

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

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

在每个 DecimalFormatter.format() 周围放置一个计时器,看看您使用了多少毫秒来获得更清晰的图片。

【讨论】:

【参考方案2】:

接受的答案(编写您自己的自定义格式化程序)是正确的,但 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();

这个微基准测试显示它比DecimalFormat 快 2 倍(当然 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://***.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" );

【讨论】:

【参考方案3】:

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

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

【讨论】:

+1 我正要自己写一些东西——这段代码可能是一个不错的起点。 我终于使用自己的算法编写了一个格式化程序,对于我需要的功能子集,它比DecimalFormat 快​​了大约四倍。我相信还有改进的余地,因为我实际上并没有下降到附加单个数字的水平。我会接受这个答案,因为它是唯一一个包含可用代码的答案。 我使用类似的形式写入直接 ByteBuffer,它可重用并可直接写入 NIO 通道。我反过来从这样的缓冲区读取数据,这意味着它可以在不创建任何对象(如字符串)的情况下从 double 读取/写入文本。 github.com/peter-lawrey/Java-Chronicle/blob/master/src/main/… @PeterLawrey 我试过了,它进入了一个无限循环——我错过了什么吗?我认为这是因为比较 factor * 10 &lt;= scaled 而发生的 - 对于足够大的值,乘以 10 会溢出,所以比较总是评估为真。 对于任何想要将其用于更高精度要求的人来说,最后一件事值得注意 - 此算法不是 DecimalFormat 的直接替代品。它不像 DecimalFormat 那样处理 -0.0、无穷大和 NaN,您可以在必要时添加它们。更重要的是,它可能会丢失一些精度并在边缘情况下打印出稍微不同的值(由于使用双精度时分辨率的损失)。对于大多数用例来说可能没问题 - 如果精度很重要,您可能应该使用 BigDecimal 或自定义定点值而不是双精度值。【参考方案4】:

另一种方法是使用字符串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();

【讨论】:

重用会很好,但格式化程序不是线程安全的,因此您必须检查此特定格式化程序是否可以处理多个线程(例如在 Web 应用程序中) @extraneon 感谢您的评论,我相应地编辑了我的答案。 String.format()Formatter.format() 似乎都比 DecimalFormat.format() 慢。我怀疑这是因为每次都必须解析模式字符串。 Formatter.format() 也更难重复使用 - 有关详细信息,请参阅我的编辑。

以上是关于DecimalFormat.format() 的更快替代方案?的主要内容,如果未能解决你的问题,请参考以下文章

Float精度格式化

格式化数字

java 计算百分数方法

Java 数字格式 - #.## - Windows 和 Linux

排序和过滤核心数据关系的更有效方法是啥?

列出组合的更有效方式