You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: content/blog/2025/apfloat-bigdecimal.md
+49-77Lines changed: 49 additions & 77 deletions
Original file line number
Diff line number
Diff line change
@@ -10,6 +10,8 @@ featuredpath = "date"
10
10
type = "post"
11
11
+++
12
12
13
+
*{{< sp orange >}}Edit (2025-05-08):{{</ sp >}} I changed some test parameters and re-run the tests. Adding bar plots.*
14
+
13
15
I recently set out to compare the performance of [`Apfloat`](http://www.apfloat.org) and [`BigDecimal`](https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/math/BigDecimal.html) for arbitrary precision arithmetic in Java. I use arbitrary precision floating point numbers in key places of the update cycle in Gaia Sky, so it made sense to explore this. My initial approach was a naive benchmark: a simple `main()` method running arithmetic operations in a loop and measuring the time taken. The results were strongly in favor of `BigDecimal`, even for large precision values. This was unexpected, as the general consensus I [found](https://stackoverflow.com/questions/277309/java-floating-point-high-precision-library)[online](https://groups.google.com/g/javaposse/c/YDYDPbzxntc?pli=1)[suggested](http://www.apfloat.org/apfloat_java/) that `Apfloat` is more performant, especially for higher precision operations (hundreds of digits).
14
16
15
17
To get more accurate and reliable measurements, I decided to implement a proper [JMH](@"Java Microbenchmark Harness") benchmark. The benchmark project source is available in [this repository](https://codeberg.org/langurmonkey/java-arbitrary-precision-benchmark). The benchmarks test addition, subtraction, multiplication, division, power, natural logarithm, and sine for both `Apfloat` and `BigDecimal` at different precision levels.
@@ -23,15 +25,15 @@ JMH is a benchmarking framework specifically designed for measuring performance
23
25
### The Benchmark Implementation
24
26
25
27
The JMH benchmark project is structured to measure the average time taken for each arithmetic operation over several iterations and precision levels. Here's the structure:
26
-
- Separate benchmarks for **addition**, **subtraction**, **multiplication**, **division**, **natural logarithm**, **power**, and **sine**.
28
+
- Separate benchmarks for **addition**, **subtraction**, **multiplication**, **division**, **natural logarithm**, **power**, and **sine**, additionally to an **allocation** test.
27
29
- Each benchmark tests `Apfloat` and `BigDecimal`.
28
-
- Create the actual objects at benchmark level to factor out allocation costs. Later on I provide a test with in-loop allocations.
29
-
- Settled on two precision levels, representative of *low* and *high* precision settings. They are **25**and **1000**.
30
+
- Create the actual objects at benchmark level to factor out allocation costs. Specific benchmark to test allocation overhead.
31
+
- Settled on four precision levels, on a scale ranging from *low* and *high* precision settings, represented as the number of digits. They are **25**, **50**, **500**, and **1000** digits.
30
32
- Average time mode.
31
-
-200 in-test iterations.
32
-
-Two warm-up iterations of two seconds each to minimize JVM effects.
33
-
-Two main iterations of two seconds each in the main test.
34
-
-Finally, send result into `Blackhole` to prevent JIT optimizations.
33
+
-Every benchmark function only runs one operation once. The allocation test creates a couple of objects and consumes them.
34
+
-One warm-up iterations of one second each to minimize JVM effects (`@Warmup(iterations = 1, time = 1)`).
35
+
-Three main iterations of five seconds each for the measurement (`@Measurement(iterations = 3, time = 5)`).
36
+
-Send results into `Blackhole` to prevent JIT optimizations.
35
37
36
38
Here is an example for the `Sin` benchmark:
37
39
@@ -40,115 +42,85 @@ Here is an example for the `Sin` benchmark:
40
42
41
43
### The Results
42
44
43
-
I have run the benchmark with Java 21 and JMH 1.37. Below are the specs of my laptop and the specific software versions.
45
+
Below are the specs of the system I used to run the tests and the specific software versions used. Only the CPU and the memory should play a significant role.
44
46
45
47
```
46
48
# JMH version: 1.37
47
49
# VM version: JDK 21.0.7, OpenJDK 64-Bit Server VM, 21.0.7+6
Same story here. Division is a notoriously costly operation, but `BigDecimal` still comes out comfortably on top.
97
-
Now, let's test some more involved arithmetic operation like the natural logarithm, sine, and power. Those are implemented directly in the `Apfloat` package. We use the [`big-math` project](https://github.com/eobermuhlner/big-math) for `BigDecimal`.
Again. Division is a notoriously costly operation, but `BigDecimal` still comes out comfortably on top.
83
+
84
+
Now, let's test some more involved arithmetic operations, like the natural logarithm, the sine, and the power function. In `Apfloat`, those are directly implemented in the library. For `BigDecimal`, we use the [`big-math` project](https://github.com/eobermuhlner/big-math).
98
85
99
86
**Log**
100
-
```
101
-
Benchmark (precision) Mode Cnt Score Error Units
102
-
Log.testApfloatLog 25 avgt 2 112.835 ms/op
103
-
Log.testApfloatLog 1000 avgt 2 3977.143 ms/op
104
-
Log.testBigDecimalLog 25 avgt 2 15.191 ms/op
105
-
Log.testBigDecimalLog 1000 avgt 2 6006.199 ms/op
106
-
```
107
87
108
-
The log is roughly twice as fast with `Apfloat` in the high precision setting, but it is much faster in `BigDecimal` in low precision.
Here we clearly see that the allocation overhead dominates the results. Surprisingly, `BigDecimal` seems faster when using 1000 digits of precision than when it uses only 25. The results are otherwise similar for both libraries.
107
+
For science, I thought it would be cool to test the allocation overhead, so I prepared the **Allocation** test, which allocates two instances of either `Apfloat` or `BigDecimal` and consumes them.
We see that allocation is very costly in both libraries. However, while `Apfloat` seems to be roughly constant with the precision, `BigDecimal` shows a higher cost with 25 digits, the lowest precision setting. I though this was weird, so I re-ran the test a bunch of times with the same result. I'm not sure what's the root cause for this, but it is surprising nonetheless.
112
+
113
+
Since both `Apfloat` and `BigDecimal` are immutable, allocation costs need to be factored in. New objects need to be allocated every time new operands are needed.
147
114
148
-
Contrary to expectations, `BigDecimal` consistently outperformed `Apfloat` across all operations and precision levels, including the higher precisions (500 and 1000 digits) where `Apfloat` was expected to excel. There is a single case when `Apfloat` is faster, and that is in the high precision natural logarithm benchmark. It's safe to say that this is due to the particular implementation or algorithm being used. Otherwise, the disparity is particularly noticeable in division and sine operations, where `Apfloat` is significantly slower than `BigDecimal`.
149
115
116
+
### Analysis
117
+
118
+
Contrary to expectations, `BigDecimal` consistently outperformed `Apfloat` across all operations and precision levels, including the higher precisions (500 and 1000 digits) where `Apfloat` was expected to excel. There is a single case when `Apfloat` is faster, and that is in the high precision natural logarithm benchmark. I think it's safe to say that this is due to the particular implementation or algorithm being used. Otherwise, the disparity is particularly noticeable in division and sine operations, where `Apfloat` is significantly slower than `BigDecimal`.
150
119
Specifically, `BigDecimal` was several times faster than `Apfloat` in most operations and precisions. Those are, in my opinion, significant results.
151
120
121
+
Finally, allocation seems to be faster with `Apfloat`, and there's a weird dependency on the precision for `BigDecimal` which I found strange.
122
+
123
+
152
124
### Questions and Next Steps
153
125
154
126
I was genuinely surprised by the outcome of these benchmarks, as it contradicts the general consensus regarding `Apfloat`’s supposed performance advantage in high-precision arithmetic. I am reaching out to the community to validate my methodology and results. Are these findings trustworthy, or did I overlook something crucial in my benchmarking approach? Feedback and insights are very much welcome.
0 commit comments