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

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

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

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

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

【问题讨论】:

    标签: java performance profiling string-formatting


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

    【讨论】:

    • +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、infinity 和 NaN,如有必要,您可以添加它们。更重要的是,它可能会丢失一些精度并在边缘情况下打印出略有不同的值(由于使用双精度时会丢失分辨率)。对于大多数用例来说可能没问题 - 如果精度很重要,您可能应该使用 BigDecimal 或自定义定点值,而不是双精度值。
    【解决方案2】:

    另一种方法是使用字符串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() 也更难重复使用 - 有关详细信息,请参阅我的编辑。
    【解决方案3】:

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

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

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

    【讨论】:

      【解决方案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'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://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" );
      }
      

      【讨论】:

        猜你喜欢
        • 2018-12-11
        • 2012-07-05
        • 2013-07-13
        • 2011-02-27
        • 2010-09-22
        • 2017-05-15
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多