Performance of the BigDecimal/BigInteger multiply() method (and potential improvement)
In our overview of BigDecimal and BigInteger arithmetic
methods and their performance, we mentioned that the multiply() method scaled exponentially,
specifically that multiplying an n-digit number by a m-digit number took within the
order of nm time. It may have occurred to those who know a little about numerical algorithms
that in fact algorithms exist for multiplying numbers which scale better than this.
The standard JDK implementation of the BigInteger (and hence BigDecimal) multiply() method
uses the "naive" algorithm that effectively cycles through every combination of digits to multiply them
together: essentially the same method that you would use if working out a multiplication with pencil and paper.
A common, more efficient algorithm is one known as Karatsuba's method.
This method breaks the problem down into a number of small multiplications (plus extra shifts and additions),
and gains efficiency because it replaces every four multiplications with three. You may therefore be wondering
why the standard JDK implementation doesn't use such a method, or at what point it is actually
beneficial.
As I show below, it turns out that:
On average hardware, the naive implementation is faster
for numbers up to a few thousand digits in length.
The JDK implementers presumably took the view that
this was within a "typical" range. Beyond a few thousand digits, the more scalable
Karatsuba method overtakes the performance. So if you have a specific need to multiply
very large numbers quickly, you may be interested in implementing the Karatsuba algorithm
(or other related algorithms).
Figure 1: Timing of multiplication of two BigIntegers using the
standard "naive" JDK algorithm, an implementation of Karatsuba's
algorithm and a version of this algorithm that parallelises the
three multiplications in each iteration. Timings made on a
(4-core hyperthreading) Intel i7 machine in Hotspot version 1.7.0.
Figure 1 above shows the performance of multiplication of pairs of n-digit
numbers using the standard JDK multiply() method, alongside timings with an
implementation of Karatsuba's algorithm. (Notice that the horizontal scale is
logarithmic: each graduation marks a doubling of the magnitude of the numbers.)
As can be seen, although Karatsuba's method is
more efficient in principle, in practice it only becomes beneficial when the numbers
being multiplied reach an order of around 3,000 digits. This happens because the
naive method, although inefficient in how it scales, has little overhead:
it consists essentially of a nested compact loop without method calls.
Parallelisation
A challenge for modern programming is the trend for new performance innovations
of processors to involve increased parallelism rather than increased core speed.
A potential advantage of Karatsuba's algorithm it permits a simple means of parallelisation:
the three multiplications of each "round" can be run in parallel on separate cores.
I therefore also took timings for a simple parallelised implementation of Karatsuba's algorithm
as described.
Again, with numbers of small magnitude, the overhead of parallelisation clearly outweighs
any potential benefit. Only when the numbers reach a magnitude of around 6,000 digits does
the parallel algorithm show a benefit over the non-parallel Karatsuba implementation. With
this simplistic form parallelisation, the parallel version gave a tangible benefit,
although was only around twice as fast as the non-parallel version.
If you enjoy this Java programming article, please share with friends and colleagues. Follow the author on Twitter for the latest news and rants.
Editorial page content written by Neil Coffey. Copyright © Javamex UK 2021. All rights reserved.