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.

Reply via email to