https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127908
Bug ID: 127908
Summary: RISC-V: inline ceil/floor/trunc raises FE_INVALID for
inf & large values (fcvt speculated above the range
check)
Product: gcc
Version: 16.2.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: target
Assignee: unassigned at gcc dot gnu.org
Reporter: oo.hehir at gmail dot com
CC: aurelien at aurel32 dot net, vineetg at rivosinc dot com
Target Milestone: ---
Host: x86_64-pc-linux-gnu
Target: riscv64-*-linux-gnu
Created attachment 65848
--> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65848&action=edit
Test case: ceil() raises FE_INVALID for inf & 1e300 (-std=c11 -O2)
With -std=c11 (or any C mode before C23, where ceil may raise inexact), GCC
expands ceil/floor/trunc inline on RISC-V without Zfa as
if (isnan(x)) return x + x;
if (|x| < 2^52) x = copysign((double)fcvt.l.d(x, rup), x);
but the first scheduling pass hoists the fcvt.l.d above the |x| < 2^52 branch.
fcvt.l.d of +-inf or of any |x| >= 2^63 sets the invalid flag, so ceil(inf),
ceil(-inf) & ceil(1e300) raise FE_INVALID, which C (Annex F) & IEEE 754 do not
allow; the default -ftrapping-math should prevent the speculation.
Test case (fe-ceil.c, attached):
#include <fenv.h>
#include <math.h>
#include <stdio.h>
__attribute__((noinline)) void loop(const double *s, double *d, long n)
{
for (; n > 0; --n, ++s, ++d)
*d = ceil(*s);
}
int main(void)
{
const double in[] = { INFINITY, -INFINITY, 1e300, 2.5 };
double out[4];
int bad = 0;
for (int i = 0; i < 4; i++) {
feclearexcept(FE_ALL_EXCEPT);
loop(&in[i], &out[i], 1);
int inv = fetestexcept(FE_INVALID) != 0;
printf("ceil(%g) = %g FE_INVALID=%d\n", in[i], out[i], inv);
bad |= inv;
}
return bad;
}
$ riscv64-poky-linux-gcc -march=rv64imafdc_zicsr_zifencei -mabi=lp64d -std=c11
-O2 fe-ceil.c -lm
(GCC 16.2.0 as built by Yocto Project; its patches only touch multilib paths,
the dynamic
linker name & MIPS tests, nothing in this area)
On hardware (StarFive JH7110, SiFive U74):
ceil(inf) = inf FE_INVALID=1
ceil(-inf) = -inf FE_INVALID=1
ceil(1e+300) = 1e+300 FE_INVALID=1
ceil(2.5) = 3 FE_INVALID=0
Without -std=c11 (C23 default, ceil is a libm call) all four print 0.
Inner loop at -std=c11 -O2:
flt.d a5,fa4,fa3 # |x| < 2^52
fcvt.l.d a4,fa5,rup # executed unconditionally
fcvt.d.l fa4,a4
beq a5,zero,.L6
Order of fcvt vs. the branch by option:
-O1 flt.d, beq, fcvt.l.d (correct)
-O2 flt.d, fcvt.l.d, beq (wrong)
-O2 -fno-schedule-insns2 fcvt.l.d, flt.d, beq (wrong)
-O2 -fno-schedule-insns -fno-schedule-insns2 flt.d, beq, fcvt.l.d (correct)
So sched1 moves the conversion above the range check.
The same order shows on Compiler Explorer with ceil2.c (just the loop above)
& the same options: GCC 16.1.0 & trunk (17.0.0 20261012) give flt.d, fcvt.l.d,
beq at -O2, & -O2 -fno-schedule-insns alone restores flt.d, beq, fcvt.l.d.
GCC 15.1.0 & 15.2.0 also put fcvt.l.d first. GCC 14.3.0 & earlier call ceil()
in libm, so there is no spurious FE_INVALID there.
I suspect this is because the fcvt-with-rounding-mode pattern is an UNSPEC,
which may_trap_p does not treat as raising FP exceptions; I have not confirmed
this in the GCC source.
floor (rdn) & trunc (rtz) behave the same, for float & double.
Found through numpy's test_unary_spurious_fpexception, which fails on riscv64
(numpy builds with -std=c11).
Related: PR 121534 & PR 121652 (fixed in 15.3) reworked this expansion,
round_pattern in riscv.md, so that it raises FE_INVALID only for sNaN; its
range & NaN checks are what sched1 bypasses here. See also PR 107723.