https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127960
Bug ID: 127960
Summary: RISC-V soft-HF `_Float16` argument not NaN-boxed
Product: gcc
Version: 15.2.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: target
Assignee: unassigned at gcc dot gnu.org
Reporter: tim.besard at gmail dot com
Target Milestone: ---
Created attachment 65879
--> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65879&action=edit
testcase
Without Zfh/Zfhmin, a _Float16 argument or return value lives in an FPR and
has to be NaN-boxed (bits 63:16 all ones), since a callee built with Zfh
treats any other bit pattern as the canonical NaN. When the _Float16 value
comes from a bit-cast of an integer (memcpy, union, VIEW_CONVERT_EXPR), GCC
moves the integer to the FPR with a bare fmv.s.x. That only fills bits 63:32
with ones; bits 31:16 are zero.
Testcase (no headers):
__attribute__((noipa, target ("arch=+zfh")))
void check (_Float16 x)
{
if ((float) x != 1.5f)
__builtin_abort ();
}
__attribute__((noipa))
void pass_bits (void (*cb) (_Float16), unsigned short u)
{
_Float16 x;
__builtin_memcpy (&x, &u, 2);
cb (x);
}
static unsigned short buf;
__attribute__((noipa))
void pass_local (void (*cb) (_Float16))
{
_Float16 x;
__builtin_memcpy (&x, &buf, 2);
cb (x);
}
int
main (void)
{
pass_bits (check, 0x3e00); /* 1.5 */
buf = 0x3e00;
pass_local (check);
return 0;
}
$ riscv64-linux-gnu-gcc -O2 -march=rv64gc -mabi=lp64d -Wall -Wextra -fPIC
-static pr-f16-nanbox.c
$ qemu-riscv64 -cpu rv64,zfh=true ./a.out
qemu: uncaught target signal 6 (Aborted) - core dumped
There are no compiler diagnostics. The test also aborts with -fno-PIC
(because of pass_bits) and at -O1 and -Os. It passes at -O0, and it passes
when the whole file is built with -march=rv64gc_zfh.
GCC 15.2.0, -O2 -march=rv64gc -mabi=lp64d -fPIC:
pass_bits:
fmv.s.x fa0,a1
jr a0
pass_local:
lhu a5,.LANCHOR0
fmv.s.x fa0,a5
jr a0
Trunk (17.0.0 20261016, on Compiler Explorer, configured with
--enable-default-pie) produces the same code for pass_local, and for
pass_bits it now goes through the stack but still ends in a bare fmv.s.x:
pass_bits:
addi sp,sp,-16
sh a1,14(sp)
lhu a5,14(sp)
addi sp,sp,16
fmv.s.x fa0,a5
jr a0
The expected sequence is what GCC already emits for a plain _Float16 load,
or for pass_local with -fno-PIC:
lhu a5,%lo(buf)(a5)
li a4,-65536
or a5,a4,a5
fmv.w.x fa0,a5 # *movhf_softfloat_boxing
At expand time the bit-cast becomes
(set (reg:HF fa0) (subreg:HF (reg:HI 138) 0))
The NaN-boxing code in riscv_legitimize_move (from r14-6916) only fires for
REG_P (src). This source is a SUBREG, so the move is kept as is and matches
the "f <- r" alternative of *movhf_softfloat, which prints fmv.s.x. With
-fno-PIC pass_local works by accident: combine folds the zero_extend load
into (set (reg:HF fa0) (mem:HF (lo_sum ...))), and LRA reloads that through
gen_movhf, which does box. With -fPIC/-fPIE, *local_pic_loadhi is formed
first, so the subreg move survives until final. -dp output for the
-fPIC case:
lhu a5,.LANCHOR0 # 8 [c=32 l=8] *local_pic_loadhi
fmv.s.x fa0,a5 # 9 [c=4 l=4] *movhf_softfloat/4
The same happens for __bf16 (*movbf_softfloat/4 emits fmv.s.x). Handling a
SUBREG of a non-FP register in that riscv_legitimize_move check would cover
the expand-time case. The GPR->FPR alternative of *movhf_softfloat and
*movbf_softfloat can still print an unboxed fmv.s.x for anything created
after expand, though, so it should either be removed or box the value
itself.
GCC 13.x has no soft-HF boxing at all, so it miscompiles this and also
plain _Float16 loads. 12.x rejects _Float16 on RISC-V. Known to fail:
every release from 13.1 to 16.1, and trunk 20261016. I checked all of them
as asm on Compiler Explorer. 14.2 and 15.2 were also run under qemu-user
11.1 with Zfh enabled, which checks NaN-boxing on half-precision operations.
This was found through Julia's ccall ABI tests, which read NaN16 in a Zfh
callback on a SpacemiT X60 (JuliaLang/julia#63880). The diagnosis and report
was assisted by AI (Claude Code, Opus 5.5)
$ riscv64-linux-gnu-gcc -v
Using built-in specs.
COLLECT_GCC=riscv64-linux-gnu-gcc
COLLECT_LTO_WRAPPER=.../libexec/gcc/riscv64-linux-gnu/15.2.0/lto-wrapper
Target: riscv64-linux-gnu
Configured with: /workspace/srcdir/gcc-15.2.0/configure
--prefix=/workspace/destdir --build=x86_64-linux-musl --host=x86_64-linux-gnu
--target=riscv64-linux-gnu
--with-sysroot=/workspace/destdir/riscv64-linux-gnu/sys-root
--enable-languages=c,c++,fortran --enable-shared --enable-threads=posix
--enable-version-specific-runtime-libs --disable-multilib --disable-bootstrap
--disable-werror --disable-libcc1
Thread model: posix
Supported LTO compression algorithms: zlib
gcc version 15.2.0 (GCC)