In the last post, we looked at the binary representation of floating point numbers. We also printed out the three components of a floating point number in a variety of ways.
The previous posts on floating point numbers have been somewhat theoretical. How do these theoretical aspects affect us in everyday real-life programming?
Let’s think about a common everyday requirement of comparing two floating point numbers.
Comparing Integral Numbers
Comparing integral numbers, i.e. whole numbers without a decimal component, is dead easy. We do it all the time without thinking twice about it.
Here’s a simple code snippet:
int a = 42;
int b = 43;
if (a == b) {
// do something
}
The result will be exactly the same even if we had calculated the value of a
in any other way:
int a = 21 * 2;
// or
int a = 84 / 2;
// or
int a = 420000 / 10000;
// etc, etc.
Why is this? Integral values are exact within their range. They don’t have any rounding errors in their binary representation. If the least significant bit changes from a 0
to a 1
, then the number changes by one, nothing more and nothing less.
What About Floating Point Numbers?
However, when we do floating point calculations, the results can often be slightly imprecise. This is due to rounding errors. The rounding errors are generally very small, and depending on the calculations, can often be ignored.
The rounding errors can affect us a lot when we compare numbers that we’re expecting to be the same.
Let’s say we were comparing two floating point numbers, for example salaries paid to employees:
public class Employee extends Person {
private double salary;
// Overridden equals() method
@Override
public boolean equals(Object otherObject) {
// call the equals() method of the superclass
if (!super.equals(otherObject))
return false;
Employee temp = (Employee)otherObject;
return salary == temp.salary; // what could go wrong here?
}
} // end of class
If we use the ==
operator to compare the two salaries, we might be surprised when two supposedly identical double
values aren’t equal. This can happen when the two salaries are calculated differently, which will result in different rounding errors.
Example Code
Let’s illustrate this in the following simple program:
public static void main(String args[]) {
double a = 0.3F; // bigger errors when mixing float and double
double b = 0.3D;
double c = 0.1D + 0.2D;
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
System.out.println();
System.out.printf("a = %.17f%n", a);
System.out.printf("b = %.17f%n", b);
System.out.printf("c = %.17f%n", c);
}
The results are:
a = 0.30000001192092896
b = 0.3
c = 0.30000000000000004
a = 0,30000001192092896
b = 0,30000000000000000
c = 0,30000000000000004
Obviously if we compared any of these values with the ==
operator, we wouldn’t have got the results we expected.
Possible Solutions
There are a number of possible solutions to the comparison problem.
-
We can subtract the two numbers and check if the difference between them is very small. For general floating point usage we would compare this difference to a value called epsilon. However, for our domain of employee salaries, we could check that the difference is less than a few cents or tenths of cents. Check the references at the end of the post for a
nearlyEqual()
method which you could use. -
We can convert the two numbers to their integral representations, and subtract the two values. This difference indicate how many ULPs (Units in the Last Place) the two numbers differ by. If we subtract the two integrals and get a value of
1
, then the two values are as close as they can be without actually being equal (see the next section on epsilon). If we get a value of2
, then the two values are still really close, with just one float value between them. Comparing numbers using ULPs works well in the range of normal numbers. It’s a bit more complex at the extremes of floating-point numbers (like infinity, zero and NaN). Java provides library functionality to convert floating point values to their integral representations (see my previous post).
Epsilon For the More Technically Minded
Epsilon is a hardware-dependent value that defines an upper bound on floating point errors. It is equivalent to the difference between 1.0 and the smallest representable value that is greater than 1.0. We can think of this as the difference between two floating point values that only differ in their least significant bit being a 0
or a 1
.
Go to https://float.exposed/0x3ff0000000000000, select double precision, and toggle the very last (rightmost) bit by clicking on it.
This shows an epsilon value of about 2.22e-016
(the mathematical value is 2.220446049250313e−016
). This is the value when the significand/mantissa is 53 bits.
Further Reading and Signing Off
You can read more on how to compare floating point numbers at https://floating-point-gui.de/errors/comparison/.
The previous link includes a nearlyEqual()
method that works relatively well, but is fairly complex and includes non-obvious code. They also have a test suite for the nearlyEqual()
method.
For some of the more mind-blowing intricacies of floating-point maths, see the tech blog of Bruce Dawson. He has hugely detailed coverage on comparisons using epsilon and ULP, and a lot of other floating point gotchas.
Was this useful? Please share your comments, and as always, stay safe and keep learning!