Without -fsignaling-nans, GCC optimizes the floating-point multiplication by 1 as a no-op, even when it may generate a trap due to underflow. This is wrong.
#pragma STDC FENV_ACCESS ON
double f (double x)
return x * 1.0;
double g (double x)
volatile double one = 1.0;
return x * one;
int main (void)
volatile double x = DBL_MIN / 2, y;
y = f(x);
printf ("f: %d\n", y != 0);
y = g(x);
printf ("g: %d\n", y != 0);
Here, x is a subnormal, so that its multiplication by 1 signals an underflow. Note that since the result is exact, the underflow flag is not raised under the default exception handling, so that without -ftrapping-math and -fsignaling-nans, f() may be optimized to no-op. With -fsignaling-nans, GCC disables the optimization as expected. But with -ftrapping-math, it doesn't (the optimization is done at any optimization level, even -O0); this can be seen in the generated asm code, with -S.
When executing the code (with -O3 -ftrapping-math -lm):
Floating point exception (core dumped)
showing that f() was incorrectly optimized, and that g(), which is not optimized due to volatile, gives a floating-point exception as expected.
Similar issue with the multiplication by -1 (at least on x86_64), where GCC just negates the value.
Bug reproducible from at least GCC 4.6 to GCC 11.2.0.
Division by 1 and -1 is affected too (as being equivalent to multiplication).
Note that F.9.2 "Expression transformations" in ISO C17 says:
1 × x and x/1 → x The expressions 1 × x, x/1, and x are equivalent
(on IEC 60559 machines, among others).374)
374) Strict support for signaling NaNs — not required by this
specification — would invalidate these and other transformations
that remove arithmetic operators.
and the current C2x draft (N2596) has something similar. But this is incorrect as it misses the underflow case with enabled trap (allowed by F.8).
In the testcase, I forgot
#pragma STDC FP_CONTRACT OFF
Anyway, it is currently ignored by GCC.
(In reply to Vincent Lefèvre from comment #2)
> In the testcase, I forgot
> #pragma STDC FP_CONTRACT OFF
> Anyway, it is currently ignored by GCC.
You can use -ffp-contract=off but I don't think these transforms are affected.
Note FENV_ACCESS is also ignored but then -ftrapping-math should indeed make your testcase valid. Are denormals standardized or left towards the implementation? One could argue the underflow exception with * 1 on a denormal isn't covered?
(In reply to Richard Biener from comment #3)
> You can use -ffp-contract=off but I don't think these transforms are
I confirm that the test behaves in the same way with -ffp-contract=off too.
BTW, -ftrapping-math is documented to be the default.
> Note FENV_ACCESS is also ignored but then -ftrapping-math should indeed make
> your testcase valid. Are denormals standardized or left towards the
> implementation? One could argue the underflow exception with * 1 on a
> denormal isn't covered?
Subnormals are actually even standardized in the main C specification, but are required only for float and double if __STDC_IEC_559__ is defined (Annex F): float and double match IEC 60559 (= IEEE 754) single and double formats respectively, and all IEEE 754 formats have subnormals.
But what is important here is more the specification of underflow.
In C17, F.10:
The “underflow” floating-point exception is raised whenever a result is tiny
(essentially subnormal or zero) and suffers loss of accuracy.377)
377) IEC 60559 allows different definitions of underflow. They all result in
the same values, but differ on when the floating-point exception is raised.
However, IMHO, if the processor has some definition of underflow (typically conforming to the IEEE 754 one), optimization should not yield a different one.
IEEE 754-2019 (which has a simpler definition than 754-1985, by eliminating choices that have never been used in practice, according to discussions in the stds-754 list) says: "The underflow exception shall be signaled when a tiny non-zero result is detected." It still allows differences about tininess detection (i.e. either after rounding or before rounding, the difference being for exact values close to the minimum normal number in magnitude), but this does not matter on my example.
It also says: "In addition, under default exception handling for underflow, if the rounded result is inexact — that is, it differs from what would have been computed were both exponent range and precision unbounded — the underflow flag shall be raised and the inexact (see 7.6) exception shall be signaled. If the rounded result is exact, no flag is raised and no inexact exception is signaled. This is the only case in this standard of an exception signal receiving default handling that does not raise the corresponding flag. Such an underflow signal has no observable effect under default handling."
When the underflow trap is enabled, this is not the default exception handling for underflow. Thus, whether the result is exact or not, the exception is expected to be signaled in the same way, here with a trap.
Exception traps (and thus exact underflow) are outside the scope of ISO C.
(Some forms of alternate exception handling are described in TS 18661-5,
which is *not* being integrated into C2x, but none of them are like
feenableexcept; they involve lexically scoped, not dynamically scoped,
alternate exception handling.) Note "This specification does not require
support for trap handlers that maintain information about the order or
count of floating-point exceptions.", concerning a different case where
trap handlers could observe transformations that are not otherwise
Maybe we should split -ftrapping-math into the on-by-default flag
concerned with the raising of exception flags, and an off-by-default flag
concerned with actual changed-flow-of-control traps. (That's apart from
the question of splitting -ftrapping-math into the part concerned with
local transformations keeping the correct set of exception flags raised by
expressions whose results are used, and the part (largely not implemented,
but see Marc Glisse's -ffenv-access patches from August 2020) concerned
with treating flag raising as an actual side effect and preventing code
movement or removal that would be valid if flag raising weren't handled as
a side effect.)
Issues relating to traps being enabled are still bugs (unlike e.g. any
issues with changing x87 rounding precision, which are definitely "don't
do that" when it invalidates assumptions made by the compiler) - e.g. we
have tests in the testsuite for exact underflow being handled
appropriately by soft-fp for __float128 - but correct semantics when traps
are enabled can't usefully be read from ISO C or any part of TS 18661,
since trap enablement is out of scope there.
According to the current documentation, -f(no-)trapping-math is only about traps, not flags.
And Glibc provides functions feenableexcept and fedisableexcept to enable and disable traps, so that their possible existence needs to be supported.
I think that documentation should be changed to say it's primarily about
flags, not traps, with trapping being considered much more of a legacy
feature rather than something it's normally a good idea to use (although
the option should generally cover the case of traps as well, while still
allowing variations in the nonzero number of times an exception is raised
or the order in which different exceptions are raised).