0

Context: for the following sample code different compilers produce different results wrt raising of floating-point exceptions.

Question: how to explain the difference?

Sample code (t125.c):

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

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

void show_fe_exceptions(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");
}

/*
 * based on:
 * https://github.com/google/flatbuffers/blob/4133a39df80546ff5269894a3961c7069fcd45d0/tests/test.cpp#L693
 */
#if TID == 32
_Bool is_quiet_nan(float v)
#elif TID == 64
_Bool is_quiet_nan(double v)
#else
#error "unknown TID"
#endif
{
#if TID == 32
    uint32_t qnan_base = 0x7FC00000u;
    uint32_t u;
#elif TID == 64
    uint64_t qnan_base = 0x7FF8000000000000ul;
    uint64_t u;
#else
#error "unknown TID"
#endif
    _Static_assert(sizeof(v) == sizeof(u), "unexpected");
    memcpy(&u, &v, sizeof(v));
    return ((u & qnan_base) == qnan_base);
}

#if TID == 32
float xfma(float x, float y, float z) { return fmaf(x, y, z); }
float xsqrt(float x) { return sqrtf(x); }
#elif TID == 64
double xfma(double x, double y, double z) { return fma(x, y, z); }
double xsqrt(double x) { return sqrt(x); }
#else
#error "unknown TID"
#endif

int main(void)
{
    volatile _Bool b;
#if TID == 32
    QUAL float f = NAN;
#elif TID == 64
    QUAL double f = NAN;
#else
#error "unknown TID"
#endif

    printf("f is                %f\n", f);
    printf("f is quiet nan      %d\n", is_quiet_nan(f));
    printf("NAN is quiet nan    %d\n", is_quiet_nan(NAN));

#define TEST(expr)                      \
    feclearexcept(FE_ALL_EXCEPT);       \
    printf("%-25s => ", #expr);         \
    b = (expr) != 0;                    \
    show_fe_exceptions();

    printf("\n");
    printf("C11, 5.2.4.2.2, p3:\n");
    printf("> A quiet NaN propagates through almost every arithmetic\n");
    printf("> operation without raising a floating-point exception; ...\n");
    TEST(f + f);
    TEST(f - f);
    TEST(f * f);
    TEST(f / f);
    TEST(xsqrt(f));
    TEST(xfma(f, f, f));

    TEST(NAN + NAN);
    TEST(NAN - NAN);
    TEST(NAN * NAN);
    TEST(NAN / NAN);
    TEST(xsqrt(NAN));
    TEST(xfma(NAN, NAN, NAN));

    printf("\n");
    printf("IEEE 754, 5.11, p4:\n");
    printf("> Programs that explicitly take account of the possibility of\n");
    printf("> quiet NaN operands may use the unordered-quiet predicates in\n");
    printf("> Table 5.3 which do not signal such an invalid operation exception.\n");
    TEST(f == f);
    TEST(f != f);
    TEST(f >= f);
    TEST(f <= f);
    TEST(f >  f);
    TEST(f <  f);

    TEST(NAN == NAN);
    TEST(NAN != NAN);
    TEST(NAN >= NAN);
    TEST(NAN <= NAN);
    TEST(NAN >  NAN);
    TEST(NAN <  NAN);

    return b ? 1 : 0;
}

Invocations (attention: many results):

# clang
# const float (baseline)
$ clang t125.c -std=c11 -ffp-model=strict -Wall -Wextra -pedantic -DQUAL=const -O3 -DTID=32 && ./a.exe
t125.c:10:14: warning: pragma STDC FENV_ACCESS ON is not supported, ignoring pragma [-Wunknown-pragmas]
#pragma STDC FENV_ACCESS ON
             ^
1 warning generated.
f is                -nan(ind)
f is quiet nan      1
NAN is quiet nan    1

C11, 5.2.4.2.2, p3:
> A quiet NaN propagates through almost every arithmetic
> operation without raising a floating-point exception; ...
f + f                     => exceptions raised: none
f - f                     => exceptions raised: none
f * f                     => exceptions raised: none
f / f                     => exceptions raised: none
xsqrt(f)                  => exceptions raised: none
xfma(f, f, f)             => exceptions raised: none
NAN + NAN                 => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN - NAN                 => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN * NAN                 => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN / NAN                 => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
xsqrt(NAN)                => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
xfma(NAN, NAN, NAN)       => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW

IEEE 754, 5.11, p4:
> Programs that explicitly take account of the possibility of
> quiet NaN operands may use the unordered-quiet predicates in
> Table 5.3 which do not signal such an invalid operation exception.
f == f                    => exceptions raised: none
f != f                    => exceptions raised: none
f >= f                    => exceptions raised: FE_INVALID
f <= f                    => exceptions raised: FE_INVALID
f > f                     => exceptions raised: FE_INVALID
f < f                     => exceptions raised: FE_INVALID
NAN == NAN                => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN != NAN                => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN >= NAN                => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN <= NAN                => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN > NAN                 => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN < NAN                 => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW

$ get_cmd() { cmd="clang t125.c -std=c11 -ffp-model=strict -Wall -Wextra -pedantic \
-O3 -Wfatal-errors -DQUAL=$QUAL1 -DTID=$TID1 && ./a.exe > out1 && clang t125.c \
-std=c11 -ffp-model=strict -Wall -Wextra -pedantic -O3 -Wfatal-errors -DQUAL=$QUAL2\
-DTID=$TID2 && ./a.exe > out2 && diff out1 out2" ; }

# note: below we do not count `warning: pragma STDC FENV_ACCESS ON is not supported`

# const float vs const double
$ QUAL1=const TID1=32 QUAL2=const TID2=64 get_cmd ; eval $cmd
<nothing>

# const float vs volatile float
$ QUAL1=const TID1=32 QUAL2=volatile TID2=32 get_cmd ; eval $cmd
diff --git a/out1 b/out2
index 72a12bf..d87d79e 100644
--- a/out1
+++ b/out2
@@ -1,4 +1,4 @@
-f is                -nan(ind)
+f is                nan^M
 f is quiet nan      1
 NAN is quiet nan    1

# const float vs volatile double
$ QUAL1=const TID1=32 QUAL2=volatile TID2=64 get_cmd ; eval $cmd
diff --git a/out1 b/out2
index 72a12bf..d87d79e 100644
--- a/out1
+++ b/out2
@@ -1,4 +1,4 @@
-f is                -nan(ind)
+f is                nan^M
 f is quiet nan      1
 NAN is quiet nan    1

# const double vs volatile float
$ QUAL1=const TID1=64 QUAL2=volatile TID2=32 get_cmd ; eval $cmd
diff --git a/out1 b/out2
index 72a12bf..d87d79e 100644
--- a/out1
+++ b/out2
@@ -1,4 +1,4 @@
-f is                -nan(ind)
+f is                nan^M
 f is quiet nan      1
 NAN is quiet nan    1

# const double vs volatile double
$ QUAL1=const TID1=64 QUAL2=volatile TID2=64 get_cmd ; eval $cmd
diff --git a/out1 b/out2
index 72a12bf..d87d79e 100644
--- a/out1
+++ b/out2
@@ -1,4 +1,4 @@
-f is                -nan(ind)
+f is                nan^M
 f is quiet nan      1
 NAN is quiet nan    1

# volatile float vs volatile double
$ QUAL1=volatile TID1=32 QUAL2=volatile TID2=64 get_cmd ; eval $cmd
<nothing>

# gcc
# const float (baseline)
$ gcc t125.c -std=c11 -Wall -Wextra -pedantic -O3 -DQUAL=const -DTID=32 && ./a.exe
t125.c:10: warning: ignoring ‘#pragma STDC FENV_ACCESS’ [-Wunknown-pragmas]
   10 | #pragma STDC FENV_ACCESS ON
      |
f is                nan
f is quiet nan      1
NAN is quiet nan    1

C11, 5.2.4.2.2, p3:
> A quiet NaN propagates through almost every arithmetic
> operation without raising a floating-point exception; ...
f + f                     => exceptions raised: none
f - f                     => exceptions raised: none
f * f                     => exceptions raised: none
f / f                     => exceptions raised: none
xsqrt(f)                  => exceptions raised: none
xfma(f, f, f)             => exceptions raised: none
NAN + NAN                 => exceptions raised: none
NAN - NAN                 => exceptions raised: none
NAN * NAN                 => exceptions raised: none
NAN / NAN                 => exceptions raised: none
xsqrt(NAN)                => exceptions raised: none
xfma(NAN, NAN, NAN)       => exceptions raised: none

IEEE 754, 5.11, p4:
> Programs that explicitly take account of the possibility of
> quiet NaN operands may use the unordered-quiet predicates in
> Table 5.3 which do not signal such an invalid operation exception.
f == f                    => exceptions raised: none
f != f                    => exceptions raised: none
f >= f                    => exceptions raised: none
f <= f                    => exceptions raised: none
f > f                     => exceptions raised: none
f < f                     => exceptions raised: none
NAN == NAN                => exceptions raised: none
NAN != NAN                => exceptions raised: none
NAN >= NAN                => exceptions raised: none
NAN <= NAN                => exceptions raised: none
NAN > NAN                 => exceptions raised: none
NAN < NAN                 => exceptions raised: none

$ get_cmd() { cmd="gcc t125.c -std=c11 -Wall -Wextra -pedantic -O3 -DQUAL=$QUAL1\
-DTID=$TID1 && ./a.exe > out1 && gcc t125.c -std=c11 -Wall -Wextra -pedantic -O3\
-DQUAL=$QUAL2 -DTID=$TID2 && ./a.exe > out2 && diff out1 out2" ; }

# note: below we do not count `warning: pragma STDC FENV_ACCESS ON is not supported`

# const float vs const double
$ QUAL1=const TID1=32 QUAL2=const TID2=64 get_cmd ; eval $cmd
<nothing>

# const float vs volatile float
$ QUAL1=const TID1=32 QUAL2=volatile TID2=32 get_cmd ; eval $cmd
diff --git a/out1 b/out2
index 9ef46f1..65f7158 100644
--- a/out1
+++ b/out2
@@ -24,10 +24,10 @@ IEEE 754, 5.11, p4:
 > Table 5.3 which do not signal such an invalid operation exception.
 f == f                    => exceptions raised: none
 f != f                    => exceptions raised: none
-f >= f                    => exceptions raised: none
-f <= f                    => exceptions raised: none
-f > f                     => exceptions raised: none
-f < f                     => exceptions raised: none
+f >= f                    => exceptions raised: FE_INVALID
+f <= f                    => exceptions raised: FE_INVALID
+f > f                     => exceptions raised: FE_INVALID
+f < f                     => exceptions raised: FE_INVALID
 NAN == NAN                => exceptions raised: none
 NAN != NAN                => exceptions raised: none
 NAN >= NAN                => exceptions raised: none

# const float vs volatile double
$ QUAL1=const TID1=32 QUAL2=volatile TID2=64 get_cmd ; eval $cmd
diff --git a/out1 b/out2
index 9ef46f1..65f7158 100644
--- a/out1
+++ b/out2
@@ -24,10 +24,10 @@ IEEE 754, 5.11, p4:
 > Table 5.3 which do not signal such an invalid operation exception.
 f == f                    => exceptions raised: none
 f != f                    => exceptions raised: none
-f >= f                    => exceptions raised: none
-f <= f                    => exceptions raised: none
-f > f                     => exceptions raised: none
-f < f                     => exceptions raised: none
+f >= f                    => exceptions raised: FE_INVALID
+f <= f                    => exceptions raised: FE_INVALID
+f > f                     => exceptions raised: FE_INVALID
+f < f                     => exceptions raised: FE_INVALID
 NAN == NAN                => exceptions raised: none
 NAN != NAN                => exceptions raised: none
 NAN >= NAN                => exceptions raised: none

# const double vs volatile float
$ QUAL1=const TID1=64 QUAL2=volatile TID2=32 get_cmd ; eval $cmd
diff --git a/out1 b/out2
index 9ef46f1..65f7158 100644
--- a/out1
+++ b/out2
@@ -24,10 +24,10 @@ IEEE 754, 5.11, p4:
 > Table 5.3 which do not signal such an invalid operation exception.
 f == f                    => exceptions raised: none
 f != f                    => exceptions raised: none
-f >= f                    => exceptions raised: none
-f <= f                    => exceptions raised: none
-f > f                     => exceptions raised: none
-f < f                     => exceptions raised: none
+f >= f                    => exceptions raised: FE_INVALID
+f <= f                    => exceptions raised: FE_INVALID
+f > f                     => exceptions raised: FE_INVALID
+f < f                     => exceptions raised: FE_INVALID
 NAN == NAN                => exceptions raised: none
 NAN != NAN                => exceptions raised: none
 NAN >= NAN                => exceptions raised: none

# const double vs volatile double
$ QUAL1=const TID1=64 QUAL2=volatile TID2=64 get_cmd ; eval $cmd
diff --git a/out1 b/out2
index 9ef46f1..65f7158 100644
--- a/out1
+++ b/out2
@@ -24,10 +24,10 @@ IEEE 754, 5.11, p4:
 > Table 5.3 which do not signal such an invalid operation exception.
 f == f                    => exceptions raised: none
 f != f                    => exceptions raised: none
-f >= f                    => exceptions raised: none
-f <= f                    => exceptions raised: none
-f > f                     => exceptions raised: none
-f < f                     => exceptions raised: none
+f >= f                    => exceptions raised: FE_INVALID
+f <= f                    => exceptions raised: FE_INVALID
+f > f                     => exceptions raised: FE_INVALID
+f < f                     => exceptions raised: FE_INVALID
 NAN == NAN                => exceptions raised: none
 NAN != NAN                => exceptions raised: none
 NAN >= NAN                => exceptions raised: none

# volatile float vs volatile double
$ QUAL1=volatile TID1=32 QUAL2=volatile TID2=64 get_cmd ; eval $cmd
<nothing>

# cl
# const float (baseline)
$ cl t125.c /std:c11 /fp:strict /DQUAL=const /DTID=32 && t125
f is                nan
f is quiet nan      1
NAN is quiet nan    1

C11, 5.2.4.2.2, p3:
> A quiet NaN propagates through almost every arithmetic
> operation without raising a floating-point exception; ...
f + f                     => exceptions raised: none
f - f                     => exceptions raised: none
f * f                     => exceptions raised: none
f / f                     => exceptions raised: none
xsqrt(f)                  => exceptions raised: none
xfma(f, f, f)             => exceptions raised: none
NAN + NAN                 => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN - NAN                 => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN * NAN                 => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN / NAN                 => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
xsqrt(NAN)                => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
xfma(NAN, NAN, NAN)       => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW

IEEE 754, 5.11, p4:
> Programs that explicitly take account of the possibility of
> quiet NaN operands may use the unordered-quiet predicates in
> Table 5.3 which do not signal such an invalid operation exception.
f == f                    => exceptions raised: none
f != f                    => exceptions raised: none
f >= f                    => exceptions raised: FE_INVALID
f <= f                    => exceptions raised: FE_INVALID
f > f                     => exceptions raised: FE_INVALID
f < f                     => exceptions raised: FE_INVALID
NAN == NAN                => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN != NAN                => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN >= NAN                => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN <= NAN                => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN > NAN                 => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW
NAN < NAN                 => exceptions raised: FE_INEXACT FE_INVALID FE_OVERFLOW

$ get_cmd() { cmd="cl t125.c /std:c11 /fp:strict /DQUAL=$QUAL1 /DTID=$TID1\
&& ./t125.exe > out1 && cl t125.c /std:c11 /fp:strict /DQUAL=$QUAL2 /DTID=$TID2\
&& ./t125.exe > out2 && diff out1 out2" ; }

# const float vs const double
$ QUAL1=const TID1=32 QUAL2=const TID2=64 get_cmd ; eval $cmd
<nothing>

# const float vs volatile float
$ QUAL1=const TID1=32 QUAL2=volatile TID2=32 get_cmd ; eval $cmd
<nothing>

# const float vs volatile double
$ QUAL1=const TID1=32 QUAL2=volatile TID2=64 get_cmd ; eval $cmd
<nothing>

# const double vs volatile float
$ QUAL1=const TID1=64 QUAL2=volatile TID2=32 get_cmd ; eval $cmd
<nothing>

# const double vs volatile double
$ QUAL1=const TID1=64 QUAL2=volatile TID2=64 get_cmd ; eval $cmd
<nothing>

# volatile float vs volatile double
$ QUAL1=volatile TID1=32 QUAL2=volatile TID2=64 get_cmd ; eval $cmd
<nothing>

$ gcc --version
gcc (GCC) 10.2.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

clang --version
clang version 11.0.1
Target: x86_64-pc-windows-msvc
Thread model: posix

$ cl
Microsoft (R) C/C++ Optimizing Compiler Version 19.28.29913 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

Update: It turns out that __STDC_IEC_559__ value (if defined) depends of the host OS. For example, both gcc 10.2.0 (with -frounding-math -fsignaling-nans) and clang 11.0.1 (with -ffp-model=strict) running on Windows 10 (version 2004) do not define __STDC_IEC_559__ to 1. Hence, switch host OS from Windows 10 to Ubuntu 20.04 LTS, then redo the tests: https://pastebin.com/NJwcyVkm (too much content in the body, have to use external content hosting service).

pmor
  • 5,392
  • 4
  • 17
  • 36
  • 1
    `how to explain?` Well, because you are using different compilers. What other explanation do you expect? Yes, compilers differ from each other, so... it results in different results. `Invocations (attention: many results` These are _a lot_ of lines without much context. What do the lines represent? Could you explain the steps that you take there? – KamilCuk Mar 31 '21 at 22:15
  • 1
    Useful to know which compilers define `__STDC_IEC_559__` "conform to the specifications in this annex" "Annex F (normative) IEC 60559 floating-point arithmetic" That goes a long way to "how to explain the difference?" – chux - Reinstate Monica Mar 31 '21 at 22:32
  • @chux-ReinstateMonica Thanks for the support!. I always forget that neither gcc, nor clang, nor cl defines __STDC_IEC_559__. Hence, the test should be changed to `#if ! __STDC_IEC_559__ puts("no IEEE 754 conformance, non-IEEE 754 conformant results may be produced, exiting"); exit(1); #endif`. – pmor Apr 01 '21 at 14:56
  • @KamilCuk Yes, the reason is that these compilers have no IEEE 754 conformance. No other explanations are expected. I was under impression that the compilers produce so different results. It seems that in this test gcc produces more correct results, than clang and cl. The lines represent test cases (permutations `qualifier` / `type`) followed by diff results (to save the space). The steps are: apply permutations, compare results, expect the results are (1) correct (IEEE 754 conformant), (2) consistent between (a) permutations, (b) compilers. – pmor Apr 01 '21 at 15:05
  • pmor, `__STDC_IEC_559__` is not defined even if the compile might be able to as it is not worth it to even validate it. True complete __STDC_IEC_559__ compliance is _very hard_ to achieve and most FP code simply does not require it, instead accepting a sporadic deviation. To get compilers to subscribe to __STDC_IEC_559__ takes incentive or lots of time. As I see it, the industry does not want to pay for that level of correctness today. – chux - Reinstate Monica Apr 01 '21 at 15:26
  • @chux-ReinstateMonica Important update: It turns out that `__STDC_IEC_559__` value (if defined) depends of the host OS. For example, both gcc 10.2.0 (with `-frounding-math -fsignaling-nans`) and clang 11.0.1 (with `-ffp-model=strict`) running on Windows 10 (version 2004) do not define `__STDC_IEC_559__` to 1. However, both gcc and clang running on Linux define `__STDC_IEC_559__` to 1. – pmor Apr 01 '21 at 15:28
  • 1
    pmor, From your analysis, when `__STDC_IEC_559__` is 1, does the compiler fully achieve IEEE 754 compliance? – chux - Reinstate Monica Apr 01 '21 at 15:31
  • @chux-ReinstateMonica I see that even under `__STDC_IEC_559__` is 1 both gcc and clang produce executable files that raise FP exceptions. See update. What do to? Report to gcc and clang? Maybe I miss something (misinterpret the standards / compiler's options)? – pmor Apr 01 '21 at 16:43
  • pmor, "Report to gcc and clang?" --> suggest googling "what to do when I find a compiler bug?" if you really think it a bug and not some allowed UB or implementation defined behavior. – chux - Reinstate Monica Apr 01 '21 at 18:31

0 Answers0