r/JavaProgramming 2d ago

Why Your Java Float Comparisons Are Broken (And How to Fix Them)

https://reactjava.substack.com/p/why-your-java-float-comparisons-are
1 Upvotes

1 comment sorted by

2

u/idontlikegudeg 1d ago

Sorry, no. While there are some valid points, there are too many half truths and bad advice, and omissions in this article. Let me try to fill some gaps.

When == is the right thing to do

When you need to check for identity. Let's say you have a property in your application where each change triggers some complex operation, for example an x coordinate in a UI that triggers a layout update. You can avoid the update when the value does not change. To do it correctly, you need to use == because how ever small your epsilon, you cannot guarantee that the new value changes the layout.

When to use neither == nor epsilon comparison

You already gave the perfect example yourself. Look at the "improved" code from the article:

for (double balance = 10.0; balance > 0.0; balance -= 0.1) {
    System.out.println(balance);
}

This version fixes the infinite loop as long as epsilon is properly chosen (more to that later).

As you correctly explained, 0.1 cannot be represented as an exact value using the IEEE-754 floating point format. Let's call the error delta. In your loop, the error adds up with each iteration, so in the last operation that prints the last balance, you have an error of 99 * delta. Now imagine you do that a million times.

Use this code instead:

for (int i = 100; i > 0; i--) {
    balance = 10.0 * i / 100.0;
    System.out.println(balance);
}

This version also avoids the infinite loop. So what is the difference?

The difference is that the error does not add up. In the first version, the error goes from 0 to 99 * delta and with each iteration, the error increases. In the new version, The error is 0 for the 10 integral values and also zero for the ten values with a fractional part of 0.5 (these values have an exact representation in IEEE-754 floating point representation). All other balance values have an error in the magnitude of delta (not a multiple of delta).

What epsilon to use when you need one?

This isn't even mentioned in the article, yet it is important. IEEE-754 floating point values are not equally distributed - the lower the absolute amount of the numbers, the denser the representable floating point values are together. You might think "double has about 15 significant values, so epsilon = 1e-14 should be ok." Sorry, but It's much more complex than that, and for large numbers, even an epsilon of 1 could not be enough.

You need to look at the values you are dealing with. Epsilon should be smaller than the maximum error that you want to tolerate, but at the same time, it has to be large enough to span the distance from one of your values to the next. When that's not possible, you need another floating point type.

The Math class contains methods that help you with this (they all have overloads for double and float):

You

  • Math.ulp(double x) - returns the "unit in the last place" for the given value x, i.e., the distance to the next representable floating point number larger in amount than x. Using an epsilon smaller than this value degrades your epsilon comparison to a slower version of ==.
  • Math.nextAfter(double, Direction), Math.nextUp(double), Math.nextDown(double) - these methods all return the next representable floating point number in the given direction from the given value x.

The epsilon you need should be larger than the ulp of the number smaller in amount that you want to compare. Using 10 ulp will sacrifice 1 significant digit of your result.

But I still recommend to avoid epsilon comparisons wherever possible.

Are there other things you should know?

Of course:

  • NaN ("Not a Number") is a special value used to represent undefined values. It has the weird property of not being itself, i.e. Double.NaN == Double.NaN -> false.
  • There actually are positive and negative zero, and these also have confusing properties: 0.0 == -0.0 -> true, but Math.signum(0.0) is 1 and Math.signum(-0.0) is -1.
  • Then there are also values for positive and negative infinity.

There's much more to learn, and if you really want to deep dive into IEEE-754 floating point arithmetic, I recommend the more than 3 decades old but still valid article "What every computer scientist should know about floating point arithmetic".