https://gcc.gnu.org/bugzilla/show_bug.cgi?id=126921

            Bug ID: 126921
           Summary: [RISC-V] RVV wrong code for loop-vectorized unrolled
                    byte accumulations
           Product: gcc
           Version: 17.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: c++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: florian.maurin at laposte dot net
  Target Milestone: ---

Created attachment 65355
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65355&action=edit
cpp file to reproduce the bug

## Summary

GCC 15 through current GCC 17 trunk generate wrong RVV code for a manually
unrolled `unsigned char` accumulation loop
at `-O3` with a fixed 256-bit vector size. GCC 16 also reproduces at `-O2`. The
correct computation has `out[8] == 1`
and returns process status zero; the failing build computes `out[8] == 0` and
returns status one. Disabling the tree
loop vectorizer, adding `#pragma GCC novector` before the peeled loop, or
compiling with GCC 14 restores the correct
result.

The controls establish that the failure is in the loop-vectorized path. They do
not by themselves distinguish a
middle-end vectorizer defect from a defect in the RISC-V vector permutation or
lowering machinery. GCC 14 also
vectorizes the loop, but selects narrower vector modes and produces correct
code.

The same failure occurs when `SCALAR` is defined as `signed char`. It was not
reproduced with `signed short`,
`unsigned short`, or wider integer types.

## Standalone reproducer

```cpp
#ifndef SCALAR
#define SCALAR unsigned char
#endif
using u8 = SCALAR;

struct Rhs4 {
  u8 x[4];
};

__attribute__((always_inline)) inline void load_rhs(const u8* p, Rhs4& r) {
  r.x[0] = p[0];
  r.x[1] = p[1];
  r.x[2] = p[2];
  r.x[3] = p[3];
}

__attribute__((always_inline)) inline void madd(u8 a, u8 b, u8& c, u8& tmp) {
  tmp = b;
  tmp = a * tmp;
  c += tmp;
}

__attribute__((noinline)) void kernel(u8* out, const u8* packed_a, const u8*
packed_b, long rows, long depth) {
  const long peeled = depth & -8;
  for (long row = 0; row < rows; row += 2) {
    const u8* a = packed_a + row * depth;
    const u8* b = packed_b;
    u8 c[12];
    for (int i = 0; i < 8; ++i) c[i] = 0;
    u8 lhs[2];

#define STEP(K)                                  \
  lhs[0] = a[2 * (K)];                          \
  lhs[1] = a[2 * (K) + 1];                      \
  load_rhs(b + 4 * (K), rhs);                   \
  madd(lhs[0], rhs.x[0], c[0], tmp);            \
  madd(lhs[1], rhs.x[0], c[4], tmp);            \
  madd(lhs[0], rhs.x[1], c[1], tmp);            \
  madd(lhs[1], rhs.x[1], c[5], tmp);            \
  madd(lhs[0], rhs.x[2], c[2], tmp);            \
  madd(lhs[1], rhs.x[2], c[6], tmp);            \
  madd(lhs[0], rhs.x[3], c[3], tmp);            \
  madd(lhs[1], rhs.x[3], c[7], tmp)

    for (long k = 0; k < peeled; k += 8) {
      u8 tmp;
      Rhs4 rhs;
      STEP(0);
      STEP(1);
      STEP(2);
      STEP(3);
      STEP(4);
      STEP(5);
      STEP(6);
      STEP(7);
      a += 16;
      b += 32;
    }
    for (long k = peeled; k < depth; ++k) {
      u8 tmp;
      Rhs4 rhs;
      STEP(0);
      a += 2;
      b += 4;
    }
    for (int j = 0; j < 4; ++j)
      for (int p = 0; p < 2; ++p) out[10 * j + row + p] += c[j + 4 * p];
  }
}

volatile long runtime_depth = 32;

int main() {
  u8 a[4096] = {};
  u8 b[1024] = {};
  u8 out[40] = {};
  const long depth = runtime_depth;
  const long k = depth / 2;
  a[8 * depth + 2 * k] = b[4 * k] = 1;
  kernel(out, a, b, 10, depth);
  return out[8] == 1 ? 0 : 1;
}
```

The reproducer has no external dependencies and does not include Eigen.

## Compilation and execution

The following is the exact mixed-runtime procedure used for GCC 16. The GCC 16
compiler produces the application
translation unit, while GCC 14 performs only the static link. This avoids an
unrelated instruction-set mismatch in
the GCC 16 cross-toolchain runtime and does not change the generated `kernel`
object code.

```sh
riscv64-linux-gnu-g++-16 \
  -std=c++14 -O3 \
  -march=rv64gcv_zvl256b \
  -mrvv-vector-bits=zvl \
  -c reproducer.cpp -o reproducer-under-test.o

riscv64-linux-gnu-g++-14 \
  -static \
  -march=rv64gcv_zvl256b \
  -mrvv-vector-bits=zvl \
  reproducer-under-test.o -o reproducer

qemu-riscv64 \
  -cpu rv64,v=true,vlen=256,elen=64,vext_spec=v1.0 \
  ./reproducer

echo $?
```

For GCC 15, replace the first compiler command with `riscv64-linux-gnu-g++-15`.
For GCC 17 trunk, replace it with
`riscv64-unknown-linux-gnu-g++`. The same source and options reproduce the
failure. The unrelated `zfh` extension is
not required.

### Actual result with GCC 15, GCC 16, and GCC 17 trunk

```text
out[8] = 0
process exit status = 1
```

A diagnostic variant that returns `out[8]` directly also exits zero with each
failing compiler, confirming that the
missing contribution is calculated as zero rather than merely as some value
other than one.

### Expected result

```text
out[8] = 1
process exit status = 0
```

## Optimization-pass isolation

The following application-object build is correct after linking as above and
returns process status zero:

```sh
riscv64-linux-gnu-g++-16 \
  -std=c++14 -O3 -fno-tree-loop-vectorize \
  -march=rv64gcv_zvl256b \
  -mrvv-vector-bits=zvl \
  -c reproducer.cpp -o reproducer-no-loop-vectorize.o
```

Disabling only SLP vectorization does **not** fix the failure:

```sh
riscv64-linux-gnu-g++-16 \
  -std=c++14 -O3 -fno-tree-slp-vectorize \
  -march=rv64gcv_zvl256b \
  -mrvv-vector-bits=zvl \
  -c reproducer.cpp -o reproducer-no-slp.o
```

Adding `#pragma GCC novector` immediately before the peeled `for` loop also
produces the correct result.

## Vectorizer diagnostics

Adding `-fopt-info-vec-optimized=vectorization.log` to the application-object
compilation reports the following for
the peeled loop at line 45 of `reproducer.cpp`:

```text
GCC 14.2.0:
reproducer.cpp:45:24: optimized: loop vectorized using 16 byte vectors
reproducer.cpp:45:24: optimized: loop vectorized using 8 byte vectors

GCC 15.2.0:
reproducer.cpp:45:24: optimized: loop vectorized using 32 byte vectors
reproducer.cpp:45:24: optimized: loop vectorized using 8 byte vectors

GCC 16.0.1 and GCC 17 trunk:
reproducer.cpp:45:24: optimized: loop vectorized using 32 byte vectors and
unroll factor 4
reproducer.cpp:45:24: optimized: epilogue loop vectorized using 8 byte vectors
and unroll factor 1
```

GCC 14 therefore exercises the loop vectorizer but does not select the failing
32-byte form. With GCC 15.3 and GCC
16.1 at `-O3`, changing the exact vector width while retaining the same source
gives:

| `-march` vector width | Result |
|---|---|
| `zvl128b` | Correct, exit 0 |
| `zvl256b` | Wrong code, exit 1 |
| `zvl512b` | Correct, exit 0 |
| `zvl1024b` | Wrong code, exit 1 |

For the 1024-bit configuration and this runtime trip count, the vectorizer
reports a 32-byte epilogue, which also
fails. This further narrows the failure to the generated 32-byte vector form
rather than to RVV execution in general.

## Tested versions

| Compiler | Build identity | `-O2` | `-O3` |
|---|---|---|---|
| GCC 14.2.0 | Ubuntu `14.2.0-4ubuntu2~24.04.1` | Not tested | Correct, exit 0
|
| GCC 15.2.0 | Ubuntu `15.2.0-16ubuntu1` | Not tested | Wrong code, exit 1 |
| GCC 15.3.0 | Debian `15.3.0-2` | Correct, exit 0 | Wrong code, exit 1 |
| GCC 16.0.1 | Ubuntu `16-20260322-1ubuntu1`, `r16-8246-g569ace1fa50` | Wrong
code, exit 1 | Wrong code, exit 1 |
| GCC 16.1.0 | Debian `16.1.0-3` | Wrong code, exit 1 | Wrong code, exit 1 |
| GCC 17.0.0 trunk | GCC revision `a1a07cea`, 2026-08-04 | Not tested | Wrong
code, exit 1 |

For every GCC 15, GCC 16, and GCC 17 check above, the application translation
unit was compiled by the stated
compiler and linked against the GCC 14 static runtime. The generated
application object therefore contains the code
from the compiler under test while avoiding the unrelated cross-runtime
mismatch.

The source also builds and runs correctly with native x86-64 GCC 13 at `-O3`. A
native
`-fsanitize=address,undefined` build exits successfully, and `-Wall -Wextra
-Wpedantic -Wconversion
-Wsign-conversion` produces no diagnostics.

## Compiler and emulator identity

The relevant fields from `g++ -v` for the original failing toolchains are
included below.

### GCC 15.2.0

```text
Target: riscv64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu
15.2.0-16ubuntu1' --with-bugurl=file:///usr/share/doc/gcc-15/README.Bugs
--enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2,cobol,algol68
--prefix=/usr --with-gcc-major-version-only --program-suffix=-15
--enable-shared --enable-linker-build-id --libexecdir=/usr/libexec
--without-included-gettext --enable-threads=posix --libdir=/usr/lib
--enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug
--enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new
--enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-libquadmath
--disable-libquadmath-support --enable-plugin --enable-default-pie
--with-system-zlib --enable-libphobos-checking=release
--without-target-system-zlib --enable-multiarch --disable-werror
--disable-multilib --with-abi=lp64d
--with-arch=rv64i_m_a_f_d_c_b_v_zic64b_zicbom_zicbop_zicboz_ziccamoa_ziccif_zicclsm_ziccrse_zicntr_zicond_zicsr_zifencei_zihintntl_zihintpause_zihpm_zimop_zmmul_za64rs_zaamo_zalrsc_zawrs_zfa_zfhmin_zca_zcb_zcd_zcmop_zba_zbb_zbs_zkt_zvbb_zve32f_zve32x_zve64d_zve64f_zve64x_zvfhmin_zvkb_zvkt_zvl128b_zvl32b_zvl64b_supm
--enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu
--target=riscv64-linux-gnu --program-prefix=riscv64-linux-gnu-
--includedir=/usr/riscv64-linux-gnu/include
--with-build-config=bootstrap-lto-lean --enable-link-serialization=2
Thread model: posix
gcc version 15.2.0 (Ubuntu 15.2.0-16ubuntu1)
```

### GCC 16.0.1

```text
Target: riscv64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu
16-20260322-1ubuntu1' --with-bugurl=file:///usr/share/doc/gcc-16/README.Bugs
--enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2,cobol,algol68
--prefix=/usr --with-gcc-major-version-only --program-suffix=-16
--enable-shared --enable-linker-build-id --libexecdir=/usr/libexec
--without-included-gettext --enable-threads=posix --libdir=/usr/lib
--enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug
--enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new
--enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-libquadmath
--disable-libquadmath-support --enable-plugin --enable-default-pie
--with-system-zlib --enable-libphobos-checking=release
--without-target-system-zlib
--with-target-libxml2-include=/build/gcc-16-cross-Oa6HNX/gcc-16-cross-9ubuntu1/debian/tmp.riscv64/usr/include/libxml2
--with-target-libxml2-lib=/build/gcc-16-cross-Oa6HNX/gcc-16-cross-9ubuntu1/debian/tmp.riscv64/usr/lib
--enable-multiarch --disable-werror --disable-multilib --with-abi=lp64d
--with-arch=rv64i_m_a_f_d_c_b_v_zic64b_zicbom_zicbop_zicboz_ziccamoa_ziccif_zicclsm_ziccrse_zicntr_zicond_zicsr_zifencei_zihintntl_zihintpause_zihpm_zimop_zmmul_za64rs_zaamo_zalrsc_zawrs_zfa_zfhmin_zca_zcb_zcd_zcmop_zba_zbb_zbs_zkt_zvbb_zve32f_zve32x_zve64d_zve64f_zve64x_zvfhmin_zvkb_zvkt_zvl128b_zvl32b_zvl64b_supm
--enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu
--target=riscv64-linux-gnu --program-prefix=riscv64-linux-gnu-
--includedir=/usr/riscv64-linux-gnu/include
--with-build-config=bootstrap-lto-lean --enable-link-serialization=2
Thread model: posix
gcc version 16.0.1 20260322 (experimental) [trunk r16-8246-g569ace1fa50]
(Ubuntu 16-20260322-1ubuntu1)
```

### GCC 17 trunk

```text
Target: riscv64-unknown-linux-gnu
Configured with: /opt/.build/riscv64-unknown-linux-gnu/src/gcc/configure
--build=x86_64-build_pc-linux-gnu --host=x86_64-build_pc-linux-gnu
--target=riscv64-unknown-linux-gnu
--prefix=/opt/compiler-explorer/riscv64/gcc-trunk-20260804/riscv64-unknown-linux-gnu
--exec_prefix=/opt/compiler-explorer/riscv64/gcc-trunk-20260804/riscv64-unknown-linux-gnu
--with-sysroot=/opt/compiler-explorer/riscv64/gcc-trunk-20260804/riscv64-unknown-linux-gnu/riscv64-unknown-linux-gnu/sysroot
--enable-languages=c,c++,fortran,d,objc,obj-c++,go --with-arch=rv64gc
--with-abi=lp64d --with-pkgversion='crosstool-NG UNKNOWN' --enable-__cxa_atexit
--enable-libmudflap --enable-libgomp --enable-libssp --enable-libquadmath
--enable-libquadmath-support --disable-libsanitizer --disable-libmpx
--with-gmp=/opt/.build/riscv64-unknown-linux-gnu/buildtools
--with-mpfr=/opt/.build/riscv64-unknown-linux-gnu/buildtools
--with-mpc=/opt/.build/riscv64-unknown-linux-gnu/buildtools
--with-isl=/opt/.build/riscv64-unknown-linux-gnu/buildtools --enable-lto
--enable-threads=posix --enable-default-pie --enable-target-optspace
--disable-plugin --disable-nls --disable-multilib
--with-local-prefix=/opt/compiler-explorer/riscv64/gcc-trunk-20260804/riscv64-unknown-linux-gnu/riscv64-unknown-linux-gnu/sysroot
--enable-long-long
Thread model: posix
gcc version 17.0.0 20260804 (experimental) (crosstool-NG UNKNOWN)
GCC source revision: a1a07cea
```

The tests ran on an x86-64 Linux host with QEMU user-mode emulation:

```text
qemu-riscv64 version 8.2.2 (Debian 1:8.2.2+ds-0ubuntu1.17)
CPU: rv64,v=true,vlen=256,elen=64,vext_spec=v1.0
```

Reply via email to