@mbozzi,
If a type does comply, our assumption about self-comparison of NaN is entirely valid.
As long as the implementation claims compliance, all you need to do is avoid telling the compiler to make assumptions that break your code.
|
I can't agree. The type isn't what makes NaN comparisons unequal. The type, or the format of the floating point, doesn't change over various floating point optimizations relative to the discussion.
What changes is the compliance with behavior. One of the issues 'relaxed' in fast-math is the propagation of NaN. That does not imply NaN doesn't exist, or wont be returned from sqrt(-1), but it does mean strict compliance with all forms of propagation of NaN aren't going to be considered, and 't==t' depends on one of the rules of behavior to be strictly enforcement. That makes it brittle. Determining a NaN exists does not have to depend on how the compiler is configured, but 't=='t does.
What you've found is a bug in an implementation. When I checked on VS2015/17/19, LLVM (recent, maybe not the most current if they've updated in the last two months) and GCC (similarly, could be as much as two months old), the example you post above executes correctly under all the permutations I tried of optimizations which cause the idiom 't==t' to fail. Factually, I've not seen std::isnan fail under any circumstance on these 3 compilers, though I can't say I've tried every possible configuration. That makes me curious about the implementation on the compiler you used, but alas, I'm not digging that deep (didn't recognize what compiler that is). It would be hilarious if they used 't!=t', which is nearly as common an alternative to 't==t' found in discussions on the topic.
Think about that a moment.
It confirms what I've been trying to communicate here.
std::isnan is far more likely to work than 't==t'. If std::isnan doesn't work, then, as you pointed out, this makes the implementation a liar (another way of saying it has a bug).
In GCC, isnan is implemented with:
1 2 3 4 5 6 7 8 9
|
int __isnan (double x)
{
int32_t hx, lx;
EXTRACT_WORDS (hx, lx, x);
hx &= 0x7fffffff;
hx |= (uint32_t) (lx | (-lx)) >> 31;
hx = 0x7ff00000 - hx;
return (int) (((uint32_t) hx) >> 31);
}
|
That's not likely to be optimized away under most any circumstance.
In MSVC, it's done with:
1 2 3 4
|
bool isnan(_In_ _Ty _X) throw()
{
return fpclassify(_X) == FP_NAN;
}
|
MSVC uses the same fpclassify method for determining normal or infinite.
The source for fpclassify goes like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
int
__fpclassifyf(float f)
{
union IEEEf2bits u;
u.f = f;
if (u.bits.exp == 0) {
if (u.bits.man == 0)
return (FP_ZERO);
return (FP_SUBNORMAL);
}
if (u.bits.exp == 255) {
if (u.bits.man == 0)
return (FP_INFINITE);
return (FP_NAN);
}
return (FP_NORMAL);
}
|
Which is also not likely to evaporate over fast-math or other optimizations, and is a slightly cleaner 'bit level' approach than GCC's, which makes it more readable.
't==t' is an obvious optimization candidate, and the setting of precise or strict math settings is all that stop the compiler (when optimizing) from treating this idiom as an obvious constant expression that's always true (as you mentioned, from logic, algebra and most programming languages generally, it is the very epitome of the law of identity). It should be clear it's brittle as a C++ expression used to test for NaN. I can't regard fast-math as an esoteric or uncommon optimization choice.
Yes, it CAN work, but that doesn't mean it should be considered valid. One of the stipulations in a wide range of coding standards gives us a rule to prefer the standard library over 'hand built' solutions.
I assert it depends on what one really means by that word 'valid'. Does 't==t' work? Under certain circumstances it will because the compiler emits code which causes the comparison of a NaN in the CPU, and the processor will therefore report the pattern of a NaN's comparison. Note, however, that the standards speak of a NaN comparing to a NaN. They are not actually giving a C programmer the hint of comparing a variable to itself. They're speaking more generally how NaN's compare. That does not impose a requirement that the compiler guarantee that 't==t' isn't trivially optimized into 'true'. The compiler's setting to strictly comply with IEEE behavior keeps the optimizer from employing the rather obvious peephole optimization due to the logical and algebraic implication of 't==t'. It's worth noting that where such optimization choices cause 't==t' to fail to detect NaN (because it doesn't even test 't'), 'p==q', where either or both are NaN, returns correct results where I've tried it on MSVC, GCC and LLVM. This suggests these compilers aren't scrapping all compliance with IEEE, just that they're recognizing 't==t' for trivial optimization without concern for THAT idiom's meaning to programmers using it.
Consider what is being stated in the code. Is the intent of 't==t' clear? Is it obvious? No. It isn't. Is the source for std::isnan in either example above clear? MSVC's looks fairly good, while GCC's is a tad messy, though clearly GCC is examining the bits of a float for 'something' it appears to recognize. The name of the function is otherwise somewhat clarifying. None of that applies to the text 't==t' or 't!=t'.
What is happening in these code examples?
For 't!=t' or 't==t', where t is a floating point type, the processor is called upon to perform a floating point comparison. Such a comparison is performed as a subtraction, where the result is discarded but the flags are used to figure out the result of the comparison. Where a 't' is a NaN, this causes a floating point exception, which is handled in microcode. That's slow on many processors. It would, when it works, use the processor's implementation of IEEE floating point behavior as a means of compliance.
Bit fiddling, on the other hand, can happen in a standard register, won't fire a floating point exception with it's associated performance implication, and focuses determination on the actual CONTENT of the floating point value. This approach better fits the nature of what is being determined. It also does not imply an ambiguous motif that the optimizer might not really 'understand'.
While you have found an implementation where std::isnan failed, I can't repeat that finding on other compilers. That's highly suggestive (if not actual proof) that std::isnan is more likely to be correct. Where it isn't, that should be considered a bug for the compiler vendor to correct. We should expect the engineers writing a library for a compiler put considerable effort into the implementation, which is why many coding standards direct programmers to prefer the standard library where applicable.
There is, however, one thing that occurs to me that would cause the test 't==t' to execute on the processor no matter what optimizations where chosen. One could write that as inline assembler.