My understanding is that it's rarely a good idea to compare two floating point numbers due to inherent inaccuracies. However, my understanding of those inaccuracies are that they are deterministic and will always show up the same way given two identical inputs and identical transformations, hardware errors notwithstanding. So, if a collection of fp values are gathered from the same source and manipulated in the exact same ways, is it ever appropriate to test them for equality?
If by equality you mean bit by bit comparisons I have a time when that would be acceptable: when confirming that you've stored the numbers without error.
Any IEEE 754 number has a value that another IEEE 754 number of the same length (precision, whatever) can take on and be identical to it.
So the issue here isn't that IEEE 754 floating point values can never be bit by bit equal. It's that in very many other cases being or not being bit by bit equal is meaningless. That's simply because like any number that isn't allowed to go on forever the shorter numbers end up pretending they're the same thing. 3.14 is not π but we like to pretend it is. When it turns out not to be π don't blame IEEE 754.
Just like π messes with our base ten decimal numbers some base ten decimal numbers mess with IEEE 754. For example 0.1 is too big to fit in any IEEE 754 number for the same kind of reason you can't fit π on a single sheet of paper. In base 2 a 0.1 goes on forever. What trips people up is they are used to π behaving this way. They aren't used to 0.1 behaving that way.
Now if you're asking are there cases when IEEE 754 numbers map to real world values without inaccuracies I have an example of that: inches. Traditional inches (not those weird decimal inches your drafting teacher told you about) are base 2. You divide them in half, or a fourth, or an eighth, etc.
Unlike most things that get measured with IEEE 754, inches have an error free representation in the IEEE 754 floating points. But then people enter them, or display them, as base 10 numbers with a decimal point and weirdness happens. That's not IEEE 754's fault. It's because you store it one way and look at it another.
Scientists typically chose IEEE 754 not because it's good at bit by bit comparisons. They chose it because it makes good use of the bits available to create a "good enough" representation of what they're measuring. We understand good enough when we stop π at 3.14 but not so well when packing 64 bits in a IEEE 754 float. Which is why bankers (who work with decimal money) should never choose IEEE 754 floats.
So if anyone left you thinking that IEEE 754 isn't completely predictable they were pulling your leg. But many have come to expect unpredictability from people who work with IEEE 754.
I never use
== for FP -- and I also use
== for FP all the time:
I never use it in production code that does anything domain-specifically meaningful with the values.
- Like, test if the thing you calculated equals what the user entered; check if a measured value from subsystem A is the same measured value as the one from subsystem B, etc.
==all the time (but note: not always) when Unit Testing functions manipulating floating point values.
- As you note IEEE754 manipulations/calculations are deterministic (within a given subsystem at least) and if you have a (pure) function that has some float input and some float output, you can - most often - equality compare the expected UT results just fine.
- Or, if you store a float value in a file/database, and if you require roundtrip accuracy, then comparing with
==is what you actually wanna do. (Yeah and handle NaN etc specially)
Exact equality can be used when you know that the values are exact numbers. E.g. in C syntax:
for (double i = 0.0; i <= 10.0; i++) ...
this will step through the exact values 0.0, 1.0, ... 1.0.
Integers within a certain range are exactly represented in IEEE floating-point, and this can be relied upon.
Another class of number that is exactly represented are arithmetic combinations of powers of two, both fractional and integral within the limits of the mantissa. For instance 0.5, 0.125, 0.625, 2.125, 7.25 and other such numbers that are sums of powers of two. This is actually the more general class which includes the aforementioned integer representations. We can understand 7.25 as being represented exactly because it's a sum of 1.0, 2.0 and 4.0 and 0.25.
These loops should be fine:
for (double i = 0.125; i <= 10.625; i += 0.5) ... for (double i = 0.375; i <= 6.0; i *= 2.0) ...
My frank answer is – "just use range tests and be done with it." Anytime you test an FP value for "literal equality," you are making assumptions about their underlying representation ... but your source-code doesn't clearly reflect that. Therefore, someone who's debugging your code might not realize what you had in mind ... why you wrote that bug in that way.
"There's no Return On Investment (ROI) here ..."
It's trivial to simply define a boolean function, e.g.
isFPequal(x,y), which not only incorporates the desired test, but also makes it extremely obvious to anyone else who's reading your code exactly what's going on.
"Obvious" is A Very Good Thing.™
The obvious: You use == and != to check if two numbers are equal according to floating-point rules. This is not a bitwise comparison. NaN ≠ NaN, and +0 = -0.
If you add floating-point numbers to a hash-table you need to decide what to do with NaN. Naively using == will allow you to add NaN multiple times, without being able to look It up. And you need the same hash value for +0 and -0.
“Same sequence of operations gives same results” is a bit dubious. The C language allows operations to be performed with higher precision. You can allow fused multiply-add which gives results that are faster, more precise, and slightly different. Underflow can be treated in different ways. You can hope that some things give identical results, like 3.06 in a JSON document and in your source code. If the code doing the conversion is good but not identical, you’ll get the same result 99.9999999999% of the time, but not 100%.
Now if you make sure that your compiler behaves well, you will be fine. And of course you need to be aware that different calculations can give different results.