Is it considered normal that f = NAN may cause raising floating-point exceptions?

421 Views Asked by At

C2x (as well as previous):

The macro NAN is defined if and only if the implementation supports quiet NaNs for the float type. It expands to a constant expression of type float representing a quiet NaN.

Sample code (t0a.c)

#include <stdio.h>
#include <math.h>
#include <fenv.h>

#if _MSC_VER && ! __clang__ && ! __INTEL_COMPILER
#pragma fenv_access (on)
#else
#pragma STDC FENV_ACCESS ON
#endif

void print_fe_excepts_raised(void)
{
    printf("exceptions raised ");
    if (fetestexcept(FE_DIVBYZERO))  printf(" FE_DIVBYZERO");
    if (fetestexcept(FE_INEXACT))    printf(" FE_INEXACT");
    if (fetestexcept(FE_INVALID))    printf(" FE_INVALID");
    if (fetestexcept(FE_OVERFLOW))   printf(" FE_OVERFLOW");
    if (fetestexcept(FE_UNDERFLOW))     printf(" FE_UNDERFLOW");
    if (fetestexcept(FE_ALL_EXCEPT)==0) printf(" none");
    printf("\n");
}

int main(void)
{
    float f;

    feclearexcept(FE_ALL_EXCEPT);
    f = NAN;
    print_fe_excepts_raised();
    (void)f;
    return 0;
}

Invocations:

# msvc (version 19.29.30133 for x64)
$ cl t0a.c /std:c11 /Za /fp:strict && t0a.exe
exceptions raised  FE_INEXACT FE_INVALID FE_OVERFLOW

# clang on Windows (version 13.0.0)
$ clang t0a.c -std=c11 -pedantic -Wall -Wextra -ffp-model=strict && ./a.exe
exceptions raised  FE_INEXACT FE_INVALID FE_OVERFLOW

# gcc on Windows (version 11.2.0)
$ gcc t0a.c -std=c11 -pedantic -Wall -Wextra  && ./a.exe
exceptions raised  none

# gcc on Linux (version 11.2.0)
$ gcc t0a.c -std=c11 -pedantic -Wall -Wextra  && ./a.out
exceptions raised  none

# clang on Linux (version 13.0.0)
$ clang t0a.c -std=c11 -pedantic -Wall -Wextra -ffp-model=strict && ./a.out
exceptions raised  none

For msvc and clang on Windows: this is because:

C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\ucrt\corecrt_math.h:94:9
#define NAN ((float)(INFINITY * 0.0F))

C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\ucrt\corecrt_math.h:90:9
#define INFINITY ((float)(_HUGE_ENUF * _HUGE_ENUF))

C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\ucrt\corecrt_math.h:87:13
#define _HUGE_ENUF 1e+300 // _HUGE_ENUF*_HUGE_ENUF must overflow

Here we see that NAN "expands to a constant expression of type float representing a quiet NaN". Which implies that f = NAN may cause floating-point exceptions. However, f = NAN is usually seen as "writing to memory". Hence, people may wonder: "how writing to memory may cause raising floating-point exceptions?".

1

There are 1 best solutions below

7
John Bollinger On BEST ANSWER

For the record, this ...

The macro NAN is defined if and only if the implementation supports quiet NaNs for the float type. It expands to a constant expression of type float representing a quiet NaN.

... is C17 7.12/5, and it probably has the same or similar numbering in C2x.


Updated

The fact that when used with MSVC or Clang on Windows, your test program causes the FE_INVALID FP exception to be raised suggests that the combination of

  • Microsoft's C standard library and runtime environment with
  • the MSVC and Clang compilers and
  • the options you are specifying to those

is causing a signaling NaN to be generated and used as an arithmetic operand. I would agree that that is an unexpected result, probably indicating that these combinations fail to fully conform to the C language specification in this area.

has nothing to do with whether the resulting NaN is a quiet or a signaling one. The misconception there is that the FE_INVALID flag would be raised only as a consequence of generating or operating on a signaling NaN. That is not the case.

For one thing, IEEE-754 does not define any case in which a signaling NaN is generated. All defined operations that produce an NaN produce a quiet NaN, including operations in which one operand is a signaling NaN (so MSVC and Clang on Windows almost certainly do produce a quiet NaN as the value of the NAN macro). Most operations with at least one signaling NaN as an operand do, by default, cause the FE_INVALID flag to be raised, but that is not the usual reason for that flag to be raised.

Rather, under default exception handling, the FE_INVALID flag is raised simply because of a request to compute an operation with no defined result, such as infinity times 0. The result will be a quiet NaN. Note that this does not include operations with at least one NaN operand, which do have a defined result: a quiet NaN in many cases, unordered / false for comparisons, and other results in a few cases.

With that for context, it is important to recognize that just because NAN expands to a constant expression (in a conforming C implementation) does not mean that the value of that expression is computed at compile time. Indeed, given the specifications for MSVC's and Clang's strict fp modes, I would expect those modes to disable most, if not all, compile-time computation of FP expressions (or at minimum to propogate FP status flags as if the computations were performed at run time).

Thus, raising FE_INVALID is not necessarily an effect of the assignment in f = NAN. If (as in Microsoft's C standard library) NAN expands to an expression involving arithmetic operations then the exception should be raised as a result of the evaluating that expression, notwithstanding that the resulting NaN is quiet. At least in implementations that claim full conformance with IEC 60559 by defining the __STDC_IEC_559__ feature-test macro.

Therefore, although I will not dispute that

people may wonder: "how writing to memory may cause raising floating-point exceptions?".

, no convincing evidence has been presented to suggest that such causation has been observed.

Nevertheless, the value represented by a particular appearance of NAN in an expression that is evaluated has some kind of physical manifestation. It is plausible for that to be in an FPU register, and storing a signaling NaN from an FPU register to memory indeed could cause a FP exception to be raised on some architectures.