You are working on a new feature where you need to use decimal numbers and you know from requirements that calculations must be precise. If you are new to java or if your knowledge cache needs a refresh, you search and read a tutorial about floating point numbers. You know what kind of values you need to handle in your program, you check their types, formats and values and then suddenly you wonder why don’t your numbers add up?. You read again the tutorial and you finally understand that:
- float: The float data type is a single-precision 32-bit IEEE 754 floating point. As with the recommendations for byte and short, use a float (instead of double) if you need to save memory in large arrays of floating point numbers. This data type should never be used for precise values, such as currency. For that, you will need to use the java.math.BigDecimal class instead. Numbers and Strings covers BigDecimal and other useful classes provided by the Java platform.
- The double data type is a double-precision 64-bit IEEE 754 floating point. For decimal values, this data type is generally the default choice. As mentioned above, this data type should never be used for precise values, such as currency.
You now have the knowledge about these primitives and you can decide when and how to use them or you can even go to extremes and decide that java float and double primitive types are evil and don’t use them anymore. There are so many resources about this subject that it is really hard to come with something new. The only thing that I want to check now is how fast or slow simple calculations with these types are when compared with the more precise BigDecimal. In order to do this I prepared a jmh benchmark:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
package com.espressoprogrammer.jmh; import org.openjdk.jmh.annotations.*; import java.math.BigDecimal; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Thread) public class FloatingPointOperations { private int scale = 2; float fOp1 = 123.45f; float fOp2 = 543.21f; double dOp1 = 123.45d; double dOp2 = 543.21d; BigDecimal bdOp1 = new BigDecimal("123.45"); BigDecimal bdOp2 = new BigDecimal("543.21"); @Benchmark public void baseline() { } /* * Addition */ @Benchmark public float measureAddFloat() { return fOp1 + fOp2; } @Benchmark public double measureAddDouble() { return dOp1 + dOp2; } @Benchmark public BigDecimal measureAddBigDecimal() { return bdOp1.add(bdOp2); } /* * Subtraction */ @Benchmark public float measureSubFloat() { return fOp1 - fOp2; } @Benchmark public double measureSubDouble() { return dOp1 - dOp2; } @Benchmark public BigDecimal measureSubBigDecimal() { return bdOp1.subtract(bdOp2); } /* * Multiplication */ @Benchmark public float measureMultFloat() { return fOp1 * fOp2; } @Benchmark public double measureMultDouble() { return dOp1 * dOp2; } @Benchmark public BigDecimal measureMultBigDecimal() { return bdOp1.multiply(bdOp2); } /* * Division */ @Benchmark public float measureDivFloat() { return fOp1 / fOp2; } @Benchmark public double measureDivDouble() { return dOp1 / dOp2; } @Benchmark public BigDecimal measureDivBigDecimal() { return bdOp1.divide(bdOp2, scale, BigDecimal.ROUND_HALF_UP); } /* * All */ @Benchmark public float measureAllFloat() { return fOp1 * fOp2 + fOp1 / fOp2 - fOp2 / fOp1 + fOp2 * fOp1; } @Benchmark public double measureAllDouble() { return dOp1 * dOp2 + dOp1 / dOp2 - dOp2 / dOp1 + dOp2 * dOp1; } @Benchmark public BigDecimal measureAllBigDecimal() { return bdOp1.multiply(bdOp2) .add(bdOp1.divide(bdOp2,scale, BigDecimal.ROUND_HALF_UP)) .subtract(bdOp2.divide(bdOp1, scale, BigDecimal.ROUND_HALF_UP)) .add(bdOp2.multiply(bdOp1)); } } |
Do not forget to add a scale and a rounding mode to BigDecimal’s divide method, otherwise you will search for clues about BigDecimal and “java.lang.ArithmeticException: Non-terminating decimal expansion”.
The results of this test were not a big surprise with BigDecimal calculations being the slowest:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
Benchmark Mode Cnt Score Error Units FloatingPointOperations.baseline avgt 10 0.331 ± 0.012 ns/op FloatingPointOperations.measureAddBigDecimal avgt 10 11.618 ± 1.754 ns/op FloatingPointOperations.measureAddDouble avgt 10 2.871 ± 0.098 ns/op FloatingPointOperations.measureAddFloat avgt 10 2.934 ± 0.131 ns/op FloatingPointOperations.measureAllBigDecimal avgt 10 95.048 ± 13.347 ns/op FloatingPointOperations.measureAllDouble avgt 10 9.038 ± 0.155 ns/op FloatingPointOperations.measureAllFloat avgt 10 4.584 ± 0.039 ns/op FloatingPointOperations.measureDivBigDecimal avgt 10 27.309 ± 2.724 ns/op FloatingPointOperations.measureDivDouble avgt 10 4.641 ± 0.210 ns/op FloatingPointOperations.measureDivFloat avgt 10 3.182 ± 0.039 ns/op FloatingPointOperations.measureMultBigDecimal avgt 10 11.956 ± 2.377 ns/op FloatingPointOperations.measureMultDouble avgt 10 2.902 ± 0.049 ns/op FloatingPointOperations.measureMultFloat avgt 10 2.950 ± 0.157 ns/op FloatingPointOperations.measureSubBigDecimal avgt 10 11.120 ± 1.160 ns/op FloatingPointOperations.measureSubDouble avgt 10 2.945 ± 0.096 ns/op FloatingPointOperations.measureSubFloat avgt 10 2.893 ± 0.084 ns/op |
What is a surprise for me is why more calculations with double (see measureAllDouble) take twice as much as the ones with float (see measureAllFloat) when single calculations take almost the same time. Do you have an idea?
Image credit: Steve Morissette, CC0 Public Domain