Improving Java Math Performance with Jafama

- jgilman - Element 84

Jafama is a Java library that provides fast numeric operations that are replacements for the methods on java.lang.Math. The description on its web site is the following:

Jafama (Java Fast Math) consists of fast - but not sloppy - counterparts of java.lang.Math treatments, plus additional ones. They are usually about 2-4 (up to 15) times faster, with about 1e-15 accuracy, and handle special cases (NaN, etc.).

It’s a pretty easy drop in replacement. You would write FastMath.sin(angle) instead of Math.sin(angle) in Java. 2 to 4 times faster with a very small accuracy loss sounds like it could be too good to be true, especially for what seems like a semi-obscure Java library hosted on Sourceforge with no presence in public Maven repos. Also, Jafama offers many math functions. Are they all equally fast? Are the accuracy claims realistic?

I’ll show how we can use Clojure to verify the libraries claims of accuracy and performance. The code for this blog post is located in https://github.com/jasongilman/jafama-perf-test

FastMath vs StrictFastMath

The JVM allows for different implementations of Math operations to use platform specific implementations of the underlying mathematical functions like sin. This use of the underlying microprocessor instructions can give much better performance but can lead to slightly different results when running the same Java code on different hardware platforms. Most of the time slight differences are an acceptable trade off for faster run times. Occasionally you’ll want your software to have the exact same result regardless of hardware. Java provides the StrictMath class for that purpose. It’s slower but is guaranteed to have the same answer regardless of hardware.

Jafama has two main classes FastMath and StrictFastMath that are akin to java.lang.Math and java.lang.StrictMath. Use FastMath when you want the best performance and slightly different results on different hardware is okay. Use StrictFastMath when the same answers on any hardware is important to your use cases. If you’re interested in a library like Jafama, which offers better performance at the cost of some accuracy, then you’re probably okay with FastMath which will offer slightly better performance. The tests in this blog post will focus on testing with FastMath only.

Measuring Performance

We’ll measure performance comparing java.lang.Math and the Jafama FastMath for most of the functions available on the class. We’ll use the Clojure Criterium library to measure performance. Criterium is a great tool for benchmarking from Clojure. It automatically includes “a warm-up period, designed to allow the JIT compiler to optimise its code” and takes factors like garbage collection into account. It runs the code being benchmarked many times over in order to provide a realistic estimate of performance.

We can use Criterium to measure the performance of calling sin on the Jafama FastMath class with the following code:

1
2
3
4
5
6
7
(use 'criterium.core)
(import 'net.jafama.FastMath)

(let [v 5.0]
  (with-progress-reporting
      (bench
        (FastMath/sin v))))

That takes between 1 and 2 minutes to run. When it’s done you get some output that looks like the following. (Not the actual results for sin.)

1
2
3
4
5
6
7
8
9
10
11
Evaluation count : 1094186460 in 60 samples of 18236441 calls.
             Execution time mean : 51.726800 ns
    Execution time std-deviation : 3.907849 ns
   Execution time lower quantile : 47.776146 ns ( 2.5%)
   Execution time upper quantile : 62.547784 ns (97.5%)
                   Overhead used : 6.223919 ns

Found 3 outliers in 60 samples (5.0000 %)
  low-severe   1 (1.6667 %)
  low-mild   2 (3.3333 %)
 Variance from outliers : 56.7807 % Variance is severely inflated by outliers

This gives you several different statistics on the measured performance of the code within bench.

Performance Results

I measured the time it took for the following functions on java.lang.Math and Jafama FastMath. The times are in mean nanoseconds as reported by Criterium. The times were measured with Java 8 and Jafama 2.1.

Function Math Mean FastMath Mean Times Faster
acos 58 16 3.6
asin 57 15 3.8
atan 94 15 6.2
atan2 145 23 6.3
cbrt 112 18 6.2
ceil 10 11 0.9
cos 74 13 5.7
cosh 141 28 5.0
exp 73 16 4.6
expm1 113 17 6.6
floor 11 12 0.9
hypot 435 23 18.9
log 31 16 1.9
log10 27 13 2.1
log1p 97 15 6.5
nextAfter 11 10 1.1
nextUp 10 8 1.3
pow 101 38 2.7
round 11 10 1.1
sin 72 16 4.5
sinh 126 23 5.5
sqrt 7 15 0.5
tan 52 14 3.7
tanh 167 26 6.4
toDegrees 7 8 0.9
toRadians 7 7 1.0

Jafama Performance

The results clearly show FastMath having an edge on Java Math for many of the functions especially the trigonometric functions and the hypot function (used for calculating the length of the hypotenuse of a triangle).

Some of the functions like ceil, floor, toDegrees, and toRadians are basically equal to that of Java Math. It’s best to use the Math implementation for those.

The sqrt function is actually slower than the equivalent Math function. The Jafama source code of sqrt, log, and log10 functions by default delegate to java.lang.Math implementations saying they “seem usually fast.” You can override that and use the Jafama implementations by setting system properties jafama.fastlog and jafama.fastsqrt to true. I set those properties to true for the performance and accuracy test to see how they compared. Given the performance of these functions it seems best to stick with the defaults.

Accuracy Results

I measured the accuracy of all the given functions by searching for the worst case difference between a FastMath function and the equivalent Java Math function. A random distribution of 10,000 numbers was used. Smaller is better.

The difference is calculated by first getting the absolute value of the difference between the FastMath and Math results. Then that result is converted to a percentage of the Math result. It’s done as a percentage because certain functions like pow or exp can generate huge results depending on the input. The FastMath and Math results might be proportionally close to each other but still generate a huge number. The accuracy of Java doubles could play a factor as well. As the Java Doubles get larger the observable difference between successive doubles is larger. The difference between MAX_DOUBLE and the next smallest observable double is about 2.0E292 for example. A percentage difference helps account for that.

Function Worst Case % Difference
acos 1E-12
asin 2E-12
atan 5E-13
atan2 4E-13
cbrt 2E-14
ceil 0.0
cos 8E-12
cosh 4E-14
exp 4E-14
expm1 5E-14
floor 0.0
hypot 3E-14
log 3E-14
log10 6E-14
log1p 2E-14
nextAfter 0.0
nextUp 0.0
pow 1E-11
round 0.0
sin 1E-11
sinh 7E-14
sqrt 2E-14
tan 1E-13
tanh 5E-14
toDegrees 2E-14
toRadians 2E-14

Other than the places where the results are exactly the same (a result of 0.0) the percentage difference here is not quite as good as the claimed 1e-15 accuracy. However, it was said to be “about” that accurate and the accuracy may have been measured more as an actual difference rather than a percentage.

Recommendation

Jafama claims “2-4 (up to 15) times faster, with about 1e-15 accuracy.” It mostly lives up to these claim depending on the functions being used. You’ll have to consider carefully whether the trade off is worth it for your own application. Errors can accumulate more if you’re making multiple calls with these functions to build your results. You should test FastMath and Math in your own application and find out for yourself.