问题
In order to improve its performance, I have been profiling one of my applications with the VisualVM sampler, using the minimum sampling period of 20ms. According to the profiler, the main thread spends almost a quarter of its CPU time in the DecimalFormat.format() method.
I am using DecimalFormat.format()
with the 0.000000
pattern to "convert" double
numbers to a string representation with exactly six decimal digits. I know that this method is relatively expensive and it is called a lot of times, but I was still somewhat surprised by these results.
To what degree are the results of such a sampling profiler accurate? How would I go about verifying them - preferrably without resorting to an instrumenting profiler?
Is there a faster alternative to
DecimalFormat
for my use case? Would it make sense to roll out my ownNumberFormat
subclass?
UPDATE:
I created a micro-benchmark to compare the performance of the following three methods:
DecimalFormat.format()
: SingleDecimalFormat
object reused multiple times.String.format()
: Multiple independent calls. Internally this method boils down topublic static String format(String format, Object ... args) { return new Formatter().format(format, args).toString(); }
Therefore I expected its performance to be very similar to
Formatter.format()
.Formatter.format()
: SingleFormatter
object reused multiple times.This method is slightly awkward -
Formatter
objects created with the default constructor append all strings created by theformat()
method to an internalStringBuilder
object, which is not properly accessible and therefore cannot be cleared. As a consequence, multiple calls toformat()
will create a concatenation of all resulting strings.To work around this issue, I provided my own
StringBuilder
instance that I cleared before use with asetLength(0)
call.
The results where interesting:
DecimalFormat.format()
was the baseline at 1.4us per call.String.format()
was slower by a factor of two at 2.7us per call.Formatter.format()
was also slower by a factor of two at 2.5us per call.
Right now it looks that DecimalFormat.format()
is still the fastest among these alternatives.
回答1:
You can write your own routine given you know exactly what you want.
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);
}
prints
Took 128 ns per append double
If you want even more performance you can write to a direct ByteBuffer (assuming you want to write the data somewhere) so the data you produce does need to be copied or encoded. (Assuming that is ok)
NOTE: this is limited to positive/negative values of less than 9 trillion (Long.MAX_VALUE/1e6) You can add special handling if this might be an issue.
回答2:
Maybe your program doesn't do much intensive work and so this appears to do the most - crunching some numbers.
My point is that your results are still relative to your app.
Put a timer around each DecimalFormatter.format() and see how many millis you are using to get a clearer picture.
But if you're still worried about it, here is an article you might like:
http://onjava.com/pub/a/onjava/2000/12/15/formatting_doubles.html
回答3:
An alternative would be to use the string Formatter, give it a try to see if it performs better:
String.format("%.6f", 1.23456789)
Or even better, create a single formatter and reuse it - as long as there are no multithreading issues, since formatters are not necessarily safe for multithreaded access:
Formatter formatter = new Formatter();
// presumably, the formatter would be called multiple times
System.out.println(formatter.format("%.6f", 1.23456789));
formatter.close();
回答4:
The accepted answer (write your own custom formatter) is correct but OP's desired format is somewhat unusual so probably won't be that helpful to others?
Here is a custom implementation for numbers that: require comma separators; have up to two decimal places. This is useful for enterprisey-things like currencies and percentages.
/**
* 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();
}
This micro-benchmark shows it to be 2x faster than DecimalFormat
(but of course YMMV depending on your use case). Improvements welcome!
/**
* 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" );
}
来源:https://stackoverflow.com/questions/8553672/a-faster-alternative-to-decimalformat-format