4

Under certain circumstances, comparing a normal floating point number with std::numeric_limits<float>::quiet_NaN() always yields "true" when compiled with GCC + "-ffast-math" (tested on Linux and MinGW). On other circumstances it always yields "false" (which would be the IEEE compliant behavior).

If one of the values is known to be a quiet_NaN at compile time, then the behavior is IEEE compliant and branches get optimized away.

I know that "-ffast-math" allows to break IEEE rules and that it implies "-ffinite-math-only" which assumes that there are no NaNs. But even with "-ffast-math" the result of quiet_NaN() has a specific bit pattern (e.g. 7FF80000), so how can a comparison with a normal float like 0.5f possibly yield true?

Here's a code sample showing the different cases. Please compile with "g++ -O3 -ffast-math".

#include <iostream>
#include <limits>

int main() {
#if 1
    // make sure that the value of 'x' is not known at compile time
    bool isnan;
    std::cout << "use nan (0|1): ";
    std::cin >> isnan;
#else
    // otherwise GCC correctly applies IEEE rules for NaN and the branch below is optimized away accordingly
    bool isnan = true;
#endif
    float x = isnan ? std::numeric_limits<float>::quiet_NaN() : 0.5f;
    std::cout << "x: " << x << std::endl;
    float a;
    std::cout << "type a float: ";
    std::cin >> a;
#if 1
    // *always* prints 1 (!)
    std::cout << a << " equal to " << x << ": " << (x == a) << std::endl;
#else
    // always prints false - the opposite from above!
    std::cout << a << " equal to " << x << ": " << ((x == a) ? "true" : "false") << std::endl;
#endif
    return 0;
}

also, here's a godbolt link. I guess the relevant part is at line 66 (with "-ffast-math") resp. 67 (without "-ffast-math"). Can someone explain to me the difference between those instructions?

Is this behavior of GCC acceptable or should I file a bug report?

EDIT: I want to make clear that I don't need to know whether a particular number is a NaN (I know that this is unspecified with "-ffast-math"), I'm only interested whether two numbers are (not) equal. In my actual code, I have a cache of floating point values and I perform update operations only if the input is different than the cached value. I've initialized the cache with quite NaNs, so they won't compare equal to any normal float and the first input is guaranteed to cause an update. This worked fine but as soon as I've added "-ffast-math", the check for newval != oldval always returned false, so there would never be an update. I've seen this quiet_NaN pattern in the SuperCollider source code and found it quite elegant.

  • There is no point reporting a bug, this is as documented. Using the guarantee that you provided (there is no NaN), gcc is able to generate smaller, more efficient code (you could look at the code generated for a function that just returns arg0==arg1, it is interesting). Now if you lied to your compiler and there are NaNs... – Marc Glisse Sep 11 '19 at 19:31

1 Answers1

6

gcc documentation says:

-ffast-math

Sets the options -fno-math-errno, -funsafe-math-optimizations, -ffinite-math-only, -fno-rounding-math, -fno-signaling-nans, -fcx-limited-range and -fexcess-precision=fast.

This option causes the preprocessor macro __FAST_MATH__ to be defined.

This option is not turned on by any -O option besides -Ofast since it can result in incorrect output for programs that depend on an exact implementation of IEEE or ISO rules/specifications for math functions. It may, however, yield faster code for programs that do not require the guarantees of these specifications.

In addition, -ffinite-math-only (included in -ffast-math) causes std::isnan, std::isinf, as well as x != x, to always return false, std::isfinite to always return true.


https://www.felixcloutier.com/x86/ucomisd

The COMISD instruction differs from the UCOMISD instruction in that it signals a SIMD floating-point invalid operation exception (#I) when a source operand is either a QNaN or SNaN. The UCOMISD instruction signals an invalid numeric exception only if a source operand is an SNaN.

-ffinite-math-only causes the compiler to emit COMISD instead of UCOMISD and omit checking the result of the comparison for NaN

The generated code is:

comiss  xmm2, DWORD PTR [rsp+28]
sete    sil

Comparing with NaN sets ZF, PF, CF flags, causing sete sil to produce true value when comparing with NaN, but no NaN is expected with -ffinite-math-only, so this is how undefined behaviour manifests itself here.

When NaN is expected the emitted assembly checks PF flag which is set when at least one operand is NaN:

ucomiss xmm1, DWORD PTR [rsp+28]
mov     eax, 0
setnp   sil # <---- condition on NaN
cmovne  esi, eax

With -ffast-math you may like to use these: Checking if a double (or float) is NaN in C++

Community
  • 1
  • 1
Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
  • thanks for your answer. actually, I'm not interested in finding out if a float is a NaN (I know that this is unspecified with -ffast-math), but only whether `f1 == f2`. how/why does this yield true if f1 is a normal float and f2 has the bit pattern of a quiet NaN? and this is acceptable behavior of GCC? (I'll edit my question to clarify) – Spacechild1 Sep 11 '19 at 19:12
  • It isn't just comisd vs ucomisd, gcc also checks different flags after the comparison. – Marc Glisse Sep 11 '19 at 19:29
  • @Maxim Egorushkin I've found another relevant part in the documentation for COMISD: `The EFLAGS register is not updated if an unmasked SIMD floating-point exception is generated.` since COMISD signals exceptions for quite NaNs, EFLAGS is not updated and so the output of the following comparison instructions is garbage. this would explain, why I only get this behavior in certain situations. am I correct? – Spacechild1 Sep 11 '19 at 19:45
  • @Christof Added details how `true` is produced when comparing with `NaN` for you. – Maxim Egorushkin Sep 11 '19 at 19:51
  • The link to "check if a double (or float) is NaN in C++" gives unreliable solution. I found out that at least for gcc9+ and icx (Intel OneAPI), isnanf() still works. If you use std::isnan() it will throw a warning. ::isnan() should work for double but I did not test. – fchen Mar 15 '22 at 17:51