为了提高性能,我使用VisualVM采样器分析了我的一个应用程序,使用的最小采样周期为20ms。根据分析器,主线程在DecimalFormat.format()
方法中花费了近四分之一的CPU时间。
我正在使用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.7便时减慢了两倍。Formatter.format()
在每次通话2.5便时也减慢了两倍。现在看来DecimalFormat.format()
仍然是这些替代品中最快的。
答案 0 :(得分:10)
如果你确切知道自己想要什么,你可以编写自己的例程。
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 :(得分:2)
也许你的程序没有做太多密集的工作,所以这似乎做得最多 - 处理一些数字。
我的观点是,您的结果仍与您的应用相关。
在每个DecimalFormatter.format()周围放一个计时器,看看你用了多少毫秒来获得更清晰的画面。
但是如果你仍然担心它,这里有一篇你可能喜欢的文章:
http://onjava.com/pub/a/onjava/2000/12/15/formatting_doubles.html
答案 2 :(得分:0)
另一种方法是使用字符串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();
答案 3 :(得分:0)
接受的答案(编写自己的自定义格式化程序)是正确的,但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" );
}