Simple summing loop results in pathological register spilling
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
LLM disclosure: I used an LLM to help me reduce the reproducer to a minimal example. There is no LLM output or analysis in this writeup.
I found the poor generated code in my own (handwritten) codebase, while tinkering with SIMD things.
After failing to make a minimal reproducer myself, I used an LLM to brute-force a reduction down to something with no external deps. I then took the resultant sloppy output and rewrote it, further minimizing it and removing a bunch of hallucinated "necessary" things that turned out not to be.
I entirely ignored any "analysis" and allegations offered by the LLM, as I don't have the expertise required to evaluate them. I verified the asm outputs of the final reproducer and its variations myself.
Reproducer code is at: https://codeberg.org/danderson/columnstore/src/commit/5504d40bde732fb48b38bf8e34d7b6efb5ee1766/repro . This reproduces both with 1.97.1 and with nightly (b07e5a086 2026-08-07).
This small function, compiled for x86_64, produces asm that spills 640 bytes to stack while attempting to sum 64 u64s:
#[inline(never)]
pub fn all_the_spills(input: &[u64; 1024], out: &mut u64, other: &mut u64, flag: bool) {
let mut o = 0u64;
for a in 0..64 {
// The `* 16` is just to defeat the autovectorizer and make the problem more obvious in the
// asm output. Still happens with `input[a]`, but less obvious because SSE2 reduces the
// number of loads required.
o += input[a * 16];
}
// Removing just this store produces good asm.
*other = 1;
// Removing just this branch (i.e. make `*out = o` unconditional) produces good asm.
if flag {
*out = o;
}
}
Generated asm produced by `cargo rustc --release -- --emit asm`
.att_syntax
.file "repro.8e6011e3694ac10c-cgu.0"
.section .text._RNvCscdRkKAp4ykW_5repro14all_the_spills,"ax",@progbits
.globl _RNvCscdRkKAp4ykW_5repro14all_the_spills
.prefalign 4, .Lfunc_end0, nop
.type _RNvCscdRkKAp4ykW_5repro14all_the_spills,@function
_RNvCscdRkKAp4ykW_5repro14all_the_spills:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
pushq %r15
.cfi_def_cfa_offset 24
pushq %r14
.cfi_def_cfa_offset 32
pushq %r13
.cfi_def_cfa_offset 40
pushq %r12
.cfi_def_cfa_offset 48
pushq %rbx
.cfi_def_cfa_offset 56
subq $280, %rsp
.cfi_def_cfa_offset 336
.cfi_offset %rbx, -56
.cfi_offset %r12, -48
.cfi_offset %r13, -40
.cfi_offset %r14, -32
.cfi_offset %r15, -24
.cfi_offset %rbp, -16
movl %ecx, -120(%rsp)
movq %rsi, 272(%rsp)
movq (%rdi), %rax
movq %rax, 168(%rsp)
movq 128(%rdi), %rax
movq %rax, -128(%rsp)
movq 256(%rdi), %rax
movq %rax, 160(%rsp)
movq 384(%rdi), %r10
movq 512(%rdi), %rax
movq %rax, 192(%rsp)
movq 640(%rdi), %r9
movq 768(%rdi), %rax
movq %rax, 248(%rsp)
movq 896(%rdi), %rax
movq %rax, 232(%rsp)
movq 1024(%rdi), %rax
movq %rax, 208(%rsp)
movq 1152(%rdi), %rax
movq %rax, 240(%rsp)
movq 1280(%rdi), %rax
movq %rax, 264(%rsp)
movq $1, (%rdx)
movq 1408(%rdi), %rax
movq %rax, -112(%rsp)
movq 1536(%rdi), %rcx
movq 1664(%rdi), %r11
movq 1792(%rdi), %rdx
movq 1920(%rdi), %r13
movq 2048(%rdi), %rax
movq %rax, -104(%rsp)
movq 2176(%rdi), %rbp
movq 2304(%rdi), %r15
movq 2432(%rdi), %rax
movq 2560(%rdi), %rsi
movq %rsi, -96(%rsp)
movq 2688(%rdi), %r12
movq 2816(%rdi), %rsi
movq %rsi, -88(%rsp)
movq 2944(%rdi), %rsi
movq %rsi, -80(%rsp)
movq 3072(%rdi), %rsi
movq %rsi, -72(%rsp)
movq 3200(%rdi), %rsi
movq %rsi, -64(%rsp)
movq 3328(%rdi), %rsi
movq %rsi, -56(%rsp)
movq 3456(%rdi), %rsi
movq %rsi, -48(%rsp)
movq 3584(%rdi), %r14
movq 3712(%rdi), %rsi
movq %rsi, -40(%rsp)
movq 3840(%rdi), %rsi
movq %rsi, -32(%rsp)
movq 3968(%rdi), %rsi
movq %rsi, -24(%rsp)
movq 4096(%rdi), %rsi
movq %rsi, -16(%rsp)
movq 4224(%rdi), %rsi
movq %rsi, -8(%rsp)
movq 4352(%rdi), %rsi
movq %rsi, (%rsp)
movq 4480(%rdi), %rsi
movq %rsi, 8(%rsp)
movq 4608(%rdi), %rbx
movq 4736(%rdi), %rsi
movq %rsi, 16(%rsp)
movq 4864(%rdi), %rsi
movq %rsi, 24(%rsp)
movq 4992(%rdi), %rsi
movq %rsi, 32(%rsp)
movq 5120(%rdi), %rsi
movq %rsi, 40(%rsp)
movq 5248(%rdi), %rsi
movq %rsi, 48(%rsp)
movq 5376(%rdi), %rsi
movq %rsi, 56(%rsp)
movq 5504(%rdi), %rsi
movq %rsi, 64(%rsp)
movq 5632(%rdi), %rsi
movq %rsi, 72(%rsp)
movq 5760(%rdi), %rsi
movq 5888(%rdi), %r8
movq %r8, 80(%rsp)
movq 6016(%rdi), %r8
movq %r8, 88(%rsp)
movq 6144(%rdi), %r8
movq %r8, 96(%rsp)
movq 6272(%rdi), %r8
movq %r8, 104(%rsp)
movq 6400(%rdi), %r8
movq %r8, 112(%rsp)
movq 6528(%rdi), %r8
movq %r8, 120(%rsp)
movq 6656(%rdi), %r8
movq %r8, 128(%rsp)
movq 6784(%rdi), %r8
movq %r8, 136(%rsp)
movq 6912(%rdi), %r8
movq %r8, 144(%rsp)
movq 7040(%rdi), %r8
movq %r8, 256(%rsp)
movq 7168(%rdi), %r8
movq %r8, 152(%rsp)
movq 7296(%rdi), %r8
movq %r8, 176(%rsp)
movq 7424(%rdi), %r8
movq %r8, 184(%rsp)
movq 7552(%rdi), %r8
movq %r8, 200(%rsp)
movq 7680(%rdi), %r8
movq %r8, 216(%rsp)
movq 7808(%rdi), %r8
movq %r8, 224(%rsp)
movq 7936(%rdi), %r8
movq 8064(%rdi), %rdi
cmpl $0, -120(%rsp)
je .LBB0_2
movq %rdi, -120(%rsp)
movq %r9, %rdi
movq -128(%rsp), %r9
addq 168(%rsp), %r9
addq 160(%rsp), %r10
addq %r9, %r10
addq 192(%rsp), %rdi
movq %r8, -128(%rsp)
movq 248(%rsp), %r8
addq %rdi, %r8
addq %r10, %r8
movq 208(%rsp), %r10
addq 232(%rsp), %r10
movq 240(%rsp), %r9
addq %r10, %r9
movq 264(%rsp), %r10
addq %r9, %r10
addq %r8, %r10
addq -112(%rsp), %rcx
addq %rcx, %r11
addq %r11, %rdx
addq %rdx, %r13
addq %r10, %r13
addq -104(%rsp), %rbp
addq %rbp, %r15
addq %r15, %rax
movq -96(%rsp), %rcx
addq %rax, %rcx
addq %rcx, %r12
addq %r13, %r12
movq -80(%rsp), %rax
addq -88(%rsp), %rax
movq -72(%rsp), %rcx
addq %rax, %rcx
movq -64(%rsp), %rax
addq %rcx, %rax
movq -56(%rsp), %rcx
addq %rax, %rcx
movq -48(%rsp), %rax
addq %rcx, %rax
addq %rax, %r14
addq %r12, %r14
movq -32(%rsp), %rcx
addq -40(%rsp), %rcx
movq -24(%rsp), %rax
addq %rcx, %rax
movq -16(%rsp), %rcx
addq %rax, %rcx
movq -8(%rsp), %rax
addq %rcx, %rax
movq (%rsp), %rcx
addq %rax, %rcx
movq 8(%rsp), %rax
addq %rcx, %rax
addq %rax, %rbx
addq %r14, %rbx
movq 24(%rsp), %rax
addq 16(%rsp), %rax
movq 32(%rsp), %rcx
addq %rax, %rcx
movq 40(%rsp), %rax
addq %rcx, %rax
movq 48(%rsp), %rcx
addq %rax, %rcx
movq 56(%rsp), %rax
addq %rcx, %rax
movq 64(%rsp), %rcx
addq %rax, %rcx
movq 72(%rsp), %rax
addq %rcx, %rax
addq %rax, %rsi
addq %rbx, %rsi
movq 88(%rsp), %rax
addq 80(%rsp), %rax
movq 96(%rsp), %rcx
addq %rax, %rcx
movq 104(%rsp), %rax
addq %rcx, %rax
movq 112(%rsp), %rcx
addq %rax, %rcx
movq 120(%rsp), %rax
addq %rcx, %rax
movq 128(%rsp), %rcx
addq %rax, %rcx
movq 136(%rsp), %rax
addq %rcx, %rax
movq 144(%rsp), %rcx
addq %rax, %rcx
movq 256(%rsp), %rax
addq %rcx, %rax
addq %rsi, %rax
movq 176(%rsp), %rcx
addq 152(%rsp), %rcx
movq 184(%rsp), %rdx
addq %rcx, %rdx
movq 200(%rsp), %rcx
addq %rdx, %rcx
movq 216(%rsp), %rdx
addq %rcx, %rdx
movq 224(%rsp), %rcx
addq %rdx, %rcx
movq -128(%rsp), %rdx
addq %rcx, %rdx
movq -120(%rsp), %rcx
addq %rdx, %rcx
addq %rax, %rcx
movq 272(%rsp), %rax
movq %rcx, (%rax)
.LBB0_2:
addq $280, %rsp
.cfi_def_cfa_offset 56
popq %rbx
.cfi_def_cfa_offset 48
popq %r12
.cfi_def_cfa_offset 40
popq %r13
.cfi_def_cfa_offset 32
popq %r14
.cfi_def_cfa_offset 24
popq %r15
.cfi_def_cfa_offset 16
popq %rbp
.cfi_def_cfa_offset 8
retq
.Lfunc_end0:
.size _RNvCscdRkKAp4ykW_5repro14all_the_spills, .Lfunc_end0-_RNvCscdRkKAp4ykW_5repro14all_the_spills
.cfi_endproc
.ident "rustc version 1.99.0-nightly (771916f90 2026-08-08)"
.section ".note.GNU-stack","",@progbits
As the source code says, removing either the store outside the branch or the branch produces the expected unrolled load+adds:
Generated asm when `*other = 1;` is removed
.att_syntax
.file "repro.8e6011e3694ac10c-cgu.0"
.section .text._RNvCscdRkKAp4ykW_5repro14all_the_spills,"ax",@progbits
.globl _RNvCscdRkKAp4ykW_5repro14all_the_spills
.prefalign 4, .Lfunc_end0, nop
.type _RNvCscdRkKAp4ykW_5repro14all_the_spills,@function
_RNvCscdRkKAp4ykW_5repro14all_the_spills:
.cfi_startproc
testl %ecx, %ecx
je .LBB0_2
movq 128(%rdi), %rax
addq (%rdi), %rax
addq 256(%rdi), %rax
addq 384(%rdi), %rax
addq 512(%rdi), %rax
addq 640(%rdi), %rax
addq 768(%rdi), %rax
addq 896(%rdi), %rax
addq 1024(%rdi), %rax
addq 1152(%rdi), %rax
addq 1280(%rdi), %rax
addq 1408(%rdi), %rax
addq 1536(%rdi), %rax
addq 1664(%rdi), %rax
addq 1792(%rdi), %rax
addq 1920(%rdi), %rax
addq 2048(%rdi), %rax
addq 2176(%rdi), %rax
addq 2304(%rdi), %rax
addq 2432(%rdi), %rax
addq 2560(%rdi), %rax
addq 2688(%rdi), %rax
addq 2816(%rdi), %rax
addq 2944(%rdi), %rax
addq 3072(%rdi), %rax
addq 3200(%rdi), %rax
addq 3328(%rdi), %rax
addq 3456(%rdi), %rax
addq 3584(%rdi), %rax
addq 3712(%rdi), %rax
addq 3840(%rdi), %rax
addq 3968(%rdi), %rax
addq 4096(%rdi), %rax
addq 4224(%rdi), %rax
addq 4352(%rdi), %rax
addq 4480(%rdi), %rax
addq 4608(%rdi), %rax
addq 4736(%rdi), %rax
addq 4864(%rdi), %rax
addq 4992(%rdi), %rax
addq 5120(%rdi), %rax
addq 5248(%rdi), %rax
addq 5376(%rdi), %rax
addq 5504(%rdi), %rax
addq 5632(%rdi), %rax
addq 5760(%rdi), %rax
addq 5888(%rdi), %rax
addq 6016(%rdi), %rax
addq 6144(%rdi), %rax
addq 6272(%rdi), %rax
addq 6400(%rdi), %rax
addq 6528(%rdi), %rax
addq 6656(%rdi), %rax
addq 6784(%rdi), %rax
addq 6912(%rdi), %rax
addq 7040(%rdi), %rax
addq 7168(%rdi), %rax
addq 7296(%rdi), %rax
addq 7424(%rdi), %rax
addq 7552(%rdi), %rax
addq 7680(%rdi), %rax
addq 7808(%rdi), %rax
addq 7936(%rdi), %rax
addq 8064(%rdi), %rax
movq %rax, (%rsi)
.LBB0_2:
retq
.Lfunc_end0:
.size _RNvCscdRkKAp4ykW_5repro14all_the_spills, .Lfunc_end0-_RNvCscdRkKAp4ykW_5repro14all_the_spills
.cfi_endproc
.ident "rustc version 1.99.0-nightly (771916f90 2026-08-08)"
.section ".note.GNU-stack","",@progbits
I originally encountered this in a benchmark, which can be found in the same repo as the repro example: https://codeberg.org/danderson/columnstore/src/commit/5504d40bde732fb48b38bf8e34d7b6efb5ee1766/columnstore/benches/utl.rs#L8. That code has more layers of indirection but is effectively doing the same sum, albeit with two parallel accumulators and using simd types from the fearless_simd crate. With cargo-show-asm, you can view that output with cargo asm -p columnstore --bench utl fearless_simd avx512 u64.
In the minimal repro, I believe the *other = 1 store is standing in for the first of two output stores (o1.store_slice(...)), and the branch is standing in for the slice bounds check for o2.store_slice's argument. In the non-minimal version, it's only the o2 sum that turns into a bunch of spills, the o1 sum turns into the expected unrolled sequence of vpaddq into a single zmm output register.
Meta
rustc --version --verbose:
rustc 1.99.0-nightly (771916f90 2026-08-08)
binary: rustc
commit-hash: 771916f9028e7fe56d2685f2c4f698de5d7d6a45
commit-date: 2026-08-08
host: x86_64-unknown-linux-gnu
release: 1.99.0-nightly
LLVM version: 23.1.0
Also verified the same behavior on stable:
rustc 1.97.1 (8bab26f4f 2026-07-14)
binary: rustc
commit-hash: 8bab26f4f68e0e26f0bb7960be334d5b520ea452
commit-date: 2026-07-14
host: x86_64-unknown-linux-gnu
release: 1.97.1
LLVM version: 22.1.6
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the minimal reproducer in the linked Codeberg repro directory and compile it with cargo rustc --release -- --emit asm on x86_64. Compare the assembly with and without *other = 1 or the conditional branch, then inspect the related benchmark at columnstore/benches/utl.rs and its cargo asm command. Done means the pathological 640-byte spill behavior is explained and addressed without regressing the reported sum.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100