https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127948
Bug ID: 127948
Summary: [x86_64-w64-mingw32] wrong code: REE keeps a
sign-extended value in the upper half of ymm6 across
calls
Product: gcc
Version: 16.2.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: rtl-optimization
Assignee: unassigned at gcc dot gnu.org
Reporter: kris.fabmoos at gmail dot com
Target Milestone: ---
Created attachment 65872
--> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65872&action=edit
bug.zip: bug.c and helper_pure.c, the minimal reproducer (build commands are in
the description)
Hello,
We ran into a wrong-code bug on Windows (mingw-w64) and managed to shrink it to
two small C files. It shows up at
`-O2 -mavx2` and `-O3 -mavx2` with both gcc 15.2.0 (MSYS2) and gcc 16.2.0
(winlibs build), and goes away with
`-fno-ree`.
**What happens.** A function reads four 32-bit integers from a struct and
widens them to 64-bit values. It then calls
an external function twice, and only after those calls does it store the four
widened values. gcc combines the load
and the widening into one early instruction:
vpmovsxdq 8(%rcx), %ymm6
...
call get_buffer
...
call get_buffer
... (ymm6 is stored after the calls)
So the full 256-bit value sits in ymm6 while the two calls run. On Windows x64
that isn't safe: a called function has
to preserve only the low 128 bits of xmm6-xmm15, and the upper half of
ymm6-ymm15 is free for it to destroy.
Microsoft's ABI page says so directly: "XMM6:XMM15, YMM6:YMM15 - Nonvolatile
(XMM), Volatile (upper half of YMM)"
(https://learn.microsoft.com/en-us/cpp/build/x64-software-conventions,
"Register volatility and preservation").
In our test the called function is plain C that gcc vectorises with ymm
registers, so gcc itself ends it with
`vzeroupper`, which is perfectly legal. That clears the upper half of ymm6, and
the two values stored from it come
out as 0.
**Why we think it is the REE pass.** `-fno-ree` makes the output correct, and
the separate load-then-widen pair comes
back. On our original program, `-fno-ipa-ra`, `-fno-caller-saves`,
`-fno-cprop-registers`, `-fno-schedule-insns2`
and `-fno-lra-remat` did not help. Our guess is that REE merges the extension
into the load without noticing that
the wider register no longer survives the calls in between, but you will know
better than we do.
**How to reproduce.** The callee is in its own file, so gcc cannot see what it
does:
gcc -O3 -mavx2 -std=c11 -c bug.c -o b.o
gcc -O2 -mavx2 -std=c11 -c helper_pure.c -o h.o
gcc b.o h.o -o bug.exe
./bug.exe
What we get: `a = 1 2 c = 0 0` (exit code 1)
What we expect: `a = 1 2 c = 5 -7` (exit code 0)
We get the expected output with `-O3 -mavx2 -fno-ree`, and with plain `-O3` (no
AVX2). `-O2 -mavx2` fails the same
way as `-O3 -mavx2`.
**bug.c**
```c
#include <stdint.h>
#include <stdio.h>
struct in { int32_t unused0, unused1; int32_t a, b, c, d; };
struct out { int64_t a, b, c, d; int64_t extra[24]; };
void *get_buffer(void *buf, int64_t n, int64_t size);
int64_t check(const struct out *o);
__attribute__((noinline)) int64_t work(const struct in *x, const int64_t *v,
void *buf, int64_t n)
{
const int64_t a = x->a, b = x->b;
const int64_t c = x->c, d = x->d;
void *p = get_buffer(buf, n, 24);
void *q = get_buffer(buf, n, 32);
struct out o;
int i;
if (p == NULL || q == NULL)
return -1;
for (i = 0; i < 24; i++)
o.extra[i] = v[i] * 3 + 1;
o.a = a; o.b = b; o.c = c; o.d = d;
return check(&o);
}
int main(void)
{
static struct in x = { 0, 0, 1, 2, 5, -7 };
static int64_t v[24];
static char buf[64];
return work(&x, v, buf, 10) == 0 ? 0 : 1;
}
```
**helper_pure.c**
```c
#include <stdint.h>
#include <stdio.h>
struct out { int64_t a, b, c, d; int64_t extra[24]; };
static int64_t scratch[64];
void *get_buffer(void *buf, int64_t n, int64_t size)
{
int i;
for (i = 0; i < 64; i++)
scratch[i] = scratch[i] * 3 + n + size;
return (char *)buf + ((n * size + scratch[7]) & 7);
}
int64_t check(const struct out *o)
{
printf("a = %lld %lld c = %lld %lld\n", (long long)o->a, (long long)o->b,
(long long)o->c, (long long)o->d);
return (o->c == 5 && o->d == -7) ? 0 : 1;
}
```
The loop in `work` matters, but not for the reason you might guess: replacing
its body with `o.extra[i] = 0;`, so
that no other vector value is live, still shows the bug, while removing the
loop altogether makes it go away. We
don't know why the loop is needed for gcc to produce this shape.
**Where we found it.** In a large C program, the affected function only lost
its values when
gcc inlined it into its single caller. The program then silently produced empty
or wrong results, with no crash and
no warning, which is what made it hard to find. We are adding `-fno-ree` to our
Windows builds as a workaround.
Compilers we tried:
- `gcc.exe (Rev13, Built by MSYS2 project) 15.2.0` - fails
- `gcc.exe (MinGW-W64 x86_64-ucrt-posix-seh, built by Brecht Sanders, r2)
16.2.0` - fails
- We did not try an older gcc, so we don't know whether this is a regression.
Thanks for looking into it.
(Searched for duplicates: PR 127634 (xmm to ymm combine) is a code-quality
issue about unnecessary upper-half clearing; it does not involve values kept
across calls and gives correct results, so we believe this is a separate bug.)