2
votes

There are already a lot of questions and answers about the dangers of expecting two floats produced by separate computations to be exactly equal, because floating point numbers are not real numbers. This question is not about correctness contingent on equality checking, it's about caching based on it.

Imagine you have this code:

if(myfloat != _last_float) {
    refresh_expensive_computation(myfloat);
    _last_float = myfloat;
}

In this instance the equality comparison purely exists to prevent doing redundant work. We are avoiding doing the expensive computation again if its input is unchanged (we assume the expensive function is deterministic and no other inputs to it have changed).

In the event the two are really equal (meaning they would be if we could compute with reals instead of floating point) but are mistakenly detected not to be, in the worst case we do the expensive computation redundantly but our program's answer is still correct. AFAIK they can only mistakenly compare equal if the computation was done in a register that is wider than the memory representation of float (e.g. on 32 bit x86 when the 80 bit fp registers are enabled), and after getting converted to the memory representation they happen to both be bitwise equal. In that case the difference must be beyond the memory representation's precision, which must be below the epsilon for comparisons that matter to me, because otherwise I'd be using a wider type like double.

So I'm going to assert this use of floating point equality is safe. So the first question is, am I wrong?

Secondly, if we assume it's safe, I'd like to avoid mistakenly returning true because it causes a costly computation. One way to avoid that on machines with wider registers than memory representations would be to use memcmp to force it to compare memory representations (the semantics won't be exactly the same for NaN, which will now compare true against the exactly identical bitwise instance of itself, but for caching that's an improvement, or for +0 and -0 but that could be special cased). However that memcmp will be slower than a floating point comparison in registers. Is there a way to detect when a platform has wider registers, so I can #ifdef or similar to get the optimized implementation on platforms where that is safe?

2
How do you know whether the cached value is correct without doing the computation anyway to figure out what it should be?Dmitri
Sorry, cached float should be called last float, edited to be more clear. We're seeing if the input is changing. We assume the same input produces the same output.Joseph Garvin
Okay... if you're saving an input/output pair, and using the saved output value when the new input matches the saved one, then it should be fine as long as only one output value is valid for a given input... that seems obvious, though, so I'm surprised you'd ask.Dmitri

2 Answers

2
votes

Most memcmp implementations have small-value optimizations for registers, so it should be fine to use that. However, if you don't want to rely on that you could also do something like reinterpret_cast<int>(). Add a compile_assert(sizeof(int) == sizeof(float)) if you want to be extra safe and are using a library set that includes such a command.

Do watch out about NaN. NaN is not equal to anything, even another NaN. If you compare memory like this they will show up as equal, which is what it sounds like you want, however you may want to add additional code to make sure all NaNs are treated the same.

0
votes

(C99) To avoid some higher precision FP math from providing a not exact compare, use volatile to force the computation to use the most recent float values.

if ((volatile float) myfloat != (volatile float) _last_float) {
  refresh_expensive_computation(myfloat);
  _last_float = myfloat;
}

Note: using_ as a leading character and then a letter as a variable name is reserved. Better to rename _last_float.

Note: -0.0f equal +0.0f. If these different floats that have the same value, are important, other code is needed than !=.