llvm / llvm/llvm-project

[X86] llc miscompiles function at -O1 -mcpu=x86-64-v3: xmm0 return value clobbered by ymm0 zero-init store, never reloaded

Open
#191,800 2 comments 0 reactions 0 assignees View on GitHub
backend:X86 miscompilation
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

llc at -O1 -mcpu=x86-64-v3 generates incorrect code for the attached function. The double return value in xmm0 is spilled to the stack, then xmm0 is clobbered by vxorpd xmm0 + vmovupd ymm0 used to lower a 32-byte llvm.memset. On the early-exit path, the spilled value is never reloaded — the function returns 0.0 instead of the correct value.

```asm
vmovsd %xmm0, 0x8(%rsp) # spill return value
...
vxorpd %xmm0, %xmm0, %xmm0 # clobber for memset
vmovupd %ymm0, (%rbx) # 32-byte zero store
vxorpd %xmm0, %xmm0, %xmm0 # xmm0 = 0.0 (NOT the spilled value)
...
vzeroupper
retq # returns 0.0 instead of spilled value
```

The buggy function is
```
_ZN2DB18JoinOrderOptimizer18computeSelectivityERKNSt3__16vectorIPNS_13JoinActionRefENS1_9allocatorIS4_EEEERKNS_6BitSetESC_.
```

The bug triggers when AVX2+ is available (v3, v4) and optimization is low (-Og, and -O1 for v4). It's specific to the combination of:
- AVX2 or higher target (enables vmovupd ymm for 32-byte stores)
- Low optimization (-Og/-O1) where the register allocator makes different spill decisions

## Reproduction matrix

Tested with official `clang-21.1.8` (LLVM 21.1.8 Linux x86_64 release binary) compiling the attached `repro_full.ii`:

```
clang -x c++ -std=c++23 -march= -w -c -o repro.o repro_full.ii
```

The bug also reproduces via `llc` on the attached `repro_full.ll`:

```
llc -march=x86-64 -mcpu= -O1 -filetype=obj -o repro.o repro_full.ll
```

### clang optimization level x target architecture

| | x86-64 | x86-64-v2 | x86-64-v3 | x86-64-v4 |
|--------|--------|-----------|-----------|-----------|
| `-O0` | OK | OK | OK | OK |
| `-Og` | OK | OK | **BUG** | **BUG** |
| `-O1` | OK | OK | OK | **BUG** |
| `-O2` | OK | OK | OK | OK |
| `-O3` | OK | OK | OK | OK |
| `-Os` | OK | OK | OK | OK |
| `-Oz` | OK | OK | OK | OK |

### llc: affected LLVM versions (tested with `-march=x86-64 -mcpu=x86-64-v3 -O1`)

Tested on the official release binaries from https://github.com/llvm/llvm-project/tags.

| LLVM version | Result |
|--------------|---------|
| 20.1.0 | **BUG** |
| 21.1.0 | **BUG** |
| 21.1.2 | **BUG** |
| 21.1.4 | **BUG** |
| 21.1.6 | **BUG** |
| 21.1.7 | **BUG** |
| 21.1.8 | **BUG** |
| 22.1.0 | **BUG** |
| 22.1.1 | **BUG** |
| 22.1.3 | **BUG** |

## How to verify the bug

After compiling to an object file, disassemble the function and check that `vmovupd %ymm0` (the 32-byte zero store from the `llvm.memset`) is followed by `retq` without a `vmovsd (%rsp), %xmm0` reload in between:

```bash
llvm-objdump -d --no-show-raw-insn repro.o \
--disassemble-symbols='_ZN2DB18JoinOrderOptimizer18computeSelectivityERKNSt3__16vectorIPNS_13JoinActionRefENS1_9allocatorIS4_EEEERKNS_6BitSetESC_' \
| awk '/vmovupd.*%ymm0/{found=1} found && /retq/{print "BUGGY"; exit} found && /vmovsd.*\(%rsp.*%xmm0/{found=0}'
```

If this prints `BUGGY`, the return value in `xmm0` was clobbered and never
reloaded. A correct compilation reloads the spilled value before `retq`:

```asm
; Correct (not buggy):
vmovsd %xmm0, 0x8(%rsp) ; spill selectivity
...
vxorpd %xmm0, %xmm0, %xmm0 ; zero for memset
vmovupd %ymm0, (%rbx) ; 32-byte zero store
...
vmovsd 0x8(%rsp), %xmm0 ; <-- reload before return
...
retq

; Buggy (missing reload):
vmovsd %xmm0, 0x8(%rsp) ; spill selectivity
...
vxorpd %xmm0, %xmm0, %xmm0 ; zero for memset
vmovupd %ymm0, (%rbx) ; 32-byte zero store
vxorpd %xmm0, %xmm0, %xmm0 ; xmm0 = 0.0, NOT the spilled value
...
vzeroupper
retq ; returns 0.0 instead of correct value
```

#### Attachments:

* Preprocessed C++ source (compressed): [repro_full.ii.zip](https://github.com/user-attachments/files/26674112/repro_full.ii.zip)
* Reduced (compressed): [repro_full.ll.zip](https://github.com/user-attachments/files/26674100/repro_full.ll.zip)
* The full source code is available in https://github.com/ClickHouse/ClickHouse/pull/90043/commits/18f38a6cb145aacb30beca334a626e472929f0c0 too (`cmake .. -DCMAKE_BUILD_TYPE=Debug -DX86_ARCH_LEVEL=3`)

Contributor guide

Open the contributing guide

Research direction

Start with the reduced repro_full.ll attachment and run the documented llc command for x86-64-v3 at -O1, then inspect the generated symbol with llvm-objdump. Trace the x86 backend's low-optimization register allocation and llvm.memset lowering around the spilled return value. Done means a regression test catches the missing reload and affected target/optimization combinations return the correct value.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.