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

            Bug ID: 127743
           Summary: [15/16 Regression] x86: V2HF CONSTRUCTOR {x, 0.0}
                    expanded as pinsrw + 32-bit store, lane 1 never zeroed
           Product: gcc
           Version: 16.2.0
            Status: UNCONFIRMED
          Keywords: wrong-code
          Severity: normal
          Priority: P3
         Component: target
          Assignee: unassigned at gcc dot gnu.org
          Reporter: khatrishubham88 at gmail dot com
  Target Milestone: ---
            Target: x86_64-pc-linux-gnu

For `struct C { _Float16 re, im; }`, `v.push_back({x, 0})` is vectorised into a
V2HF constructor
`{x, 0.0}`. The optimized GIMPLE (`-fdump-tree-optimized`) is correct:

```text
_25 = {_9, 0.0};
MEM <vector(2) _Float16> [(_Float16 *)prephitmp_115] = _25;
```

but it is expanded as "insert x into lane 0 of a register that still holds an
older value, then
store all 32 bits". Lane 1 (`im`, must be 0) gets whatever bits 16..31 of that
register held.
The testcase is UB-free (clean under `-fsanitize=address,undefined` at `-O0`
and `-O2`).

## Testcase

Exits 0 when correct, aborts when miscompiled.

```cpp
#include <vector>
struct C { _Float16 re, im; };

[[gnu::noinline]] std::vector<C> conv(const std::vector<C>& p, float s) {
  std::vector<C> half;
  half.reserve(p.size() + 1);
  half.push_back({p.front().re, 0});
  for (unsigned k = 1; k < p.size(); ++k)
    half.push_back({(_Float16)((float)p[k].re * s), p[k].im});
  half.push_back({p.front().im, 0});   // <-- this store; im must be 0
  return half;
}

int main() {
  std::vector<C> y(2);
  y[0] = {(_Float16)0.5f, (_Float16)0.5f};
  y[1] = {(_Float16)0.5f, (_Float16)0.0f};
  std::vector<C> r = conv(y, 1234.5f);
  if (r.back().im != 0) __builtin_abort();
}
```

```console
$ g++ -O2 t.cpp && ./a.out
```

## Code generated for the marked store

`g++ -S`, x86-64, Ubuntu 24.04.

### WRONG: GCC 16.2.0 `-O2` (identical with 16.0.1 20260315 `-O2`)

```asm
    pinsrw  $0, 2(%rdx), %xmm0     # <-- writes bits 0..15 only (re =
p.front().im)
    cmpq    %r12, %r15
    je      .L13
    movss   %xmm0, (%r15)          # <-- stores bits 0..31: im = stale bits
16..31 of %xmm0
                                   #     (%xmm0 last held the loop's float/half
value) -> abort
```

### WRONG: GCC 16.0.1 `-O3`

```asm
    pinsrw  $0, 2(%r15), %xmm0     # <-- bits 0..15 only
    cmpq    8(%rsp), %r14
    je      .L10
    leaq    4(%r14), %rdx
    movss   %xmm0, (%r14)          # <-- 32-bit store of stale bits 16..31 ->
abort
```

### WRONG (latent): GCC 16.0.1 `-O2 -march=x86-64-v3` (identical with
`-march=x86-64-v4`)

```asm
    vpinsrw $0, 2(%rsi), %xmm0, %xmm0   # <-- bits 0..15 only
    cmpq    %r13, %rdx
    je      .L14
    vmovss  %xmm0, (%rdx)               # <-- 32-bit store of stale bits 16..31
```

Passes only by luck: the loop's `vinsertps $0xe` + `vcvtps2ph` happened to
leave bits 16..31 zero.

### OK: GCC 16.0.1 `-O2 -mavx512fp16`

```asm
    vmovw   2(%rsi), %xmm0         # <-- VMOVW m16 zeroes the rest of the
register
    cmpq    %r12, %rdx
    je      .L14
    vmovss  %xmm0, (%rdx)          # <-- 32-bit store, bits 16..31 = 0
```

### OK: GCC 16.0.1 `-O2 -fno-tree-sra`

```asm
    movzwl  2(%rdx), %r13d         # <-- zero-extends to 32 bits
    cmpq    %rbx, %r12
    je      .L13
    movl    %r13d, (%rbx)          # <-- 32-bit store, upper half 0
```

### OK: GCC 16.0.1 `-O2 -fno-tree-slp-vectorize` (same shape: GCC 14.2 `-O2`,
GCC 15.2 `-O2`)

```asm
    movzwl  2(%rdx), %r13d
    cmpq    %r12, %rbx
    je      .L13
    movw    %r13w, (%rbx)          # <-- re
    addq    $4, %rbx
    movw    $0x0000, -2(%rbx)      # <-- im = 0, stored explicitly
```

### OK: clang 21 `-O2`

```asm
    pinsrw  $0, 2(%rbp), %xmm0
    cmpq    %r15, %r13
    je      .LBB0_6
    pextrw  $0, %xmm0, %ecx        # <-- extracts only the 16 bits that were
written
    movw    %cx, (%r13)            # <-- re
    movw    $0, 2(%r13)            # <-- im = 0
```

## Results for this testcase

| Compiler / flags | Store sequence | Code | Run |
| --- | --- | --- | --- |
| GCC 14.2.0 `-O2` | `movw` re ; `movw $0` | ok | ok |
| GCC 15.2.0 `-O2` | `movw` re ; `movw $0` | ok | ok |
| GCC 16.0.1, 16.2.0 `-O2` / `-O3` | `pinsrw` ; `movss` | **BAD** | abort |
| GCC 16.0.1 `-O2 -march=x86-64-v3` / `-v4` | `vpinsrw` ; `vmovss` | **BAD** |
ok (luck) |
| GCC 16.0.1 `-O2 -mavx512fp16` | `vmovw` ; `vmovss` | ok | ok |
| GCC 16.0.1 `-O2 -fno-tree-sra` | `movzwl` ; `movl` | ok | ok |
| GCC 16.0.1 `-O2 -fno-tree-slp-vectorize` | `movw` re ; `movw $0` | ok | ok |
| GCC 16.0.1 `-O1` / `-Os` | no V2HF store | ok | ok |
| clang 21 `-O2` | `pextrw` ; `movw` ; `movw $0` | ok | ok |

## GCC 15.2: larger testcase

GCC 15.2 emits correct code for the small testcase above, but the same bad
sequence for a larger one
from our project (attached `gcc-bug-orig.ii.xz`, preprocessed by GCC 15.2):

```asm
    pinsrw  $0, 2(%r8), %xmm0      # <-- bits 0..15 only
    ...
    movss   %xmm0, (%rbx)          # <-- 32-bit store of stale bits 16..31
```

```console
$ g++ -std=c++23 -O2 -ffp-contract=off -x c++-cpp-output gcc-bug-orig.ii &&
./a.out
```

It prints `(0.5,1.66602)` as the last element where `(0.5,0)` is expected, and
is correct with
`-fno-tree-slp-vectorize`. So the regression appeared between GCC 14.2 and GCC
15.2.

Attachments: t.cpp (the testcase above), gcc-bug-orig.ii.xz (GCC 15.2
preprocessed larger testcase).

Reply via email to