Reputation: 86403
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 own NumberFormat
subclass?
UPDATE:
I created a micro-benchmark to compare the performance of the following three methods:
DecimalFormat.format()
: Single DecimalFormat
object reused multiple times.
String.format()
: Multiple independent calls. Internally this method boils down to
public 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()
: Single Formatter
object reused multiple times.
This method is slightly awkward - Formatter
objects created with the default constructor append all strings created by the format()
method to an internal StringBuilder
object, which is not properly accessible and therefore cannot be cleared. As a consequence, multiple calls to format()
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 a setLength(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.
Upvotes: 24
Views: 17718
Reputation: 8312
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.
Upvotes: 2
Reputation: 1325
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" );
}
Upvotes: 1
Reputation: 533680
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.
Upvotes: 12
Reputation: 236114
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();
Upvotes: 2