[x86][CodeGen] Inefficient stack parameter passing for 16-byte structs
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
When passing multiple 16-byte trivially copyable structures (such as `std::span`, which consists of a pointer and a size_t) to functions where parameters spill onto the stack, Clang generates severely suboptimal assembly.
Instead of lowering argument copies using general-purpose registers or native `push` instructions, Clang pre-allocates stack space and copies memory through 128-bit vector registers (`vmovups xmm0`). This creates a sequence of immediate vector reads-after-writes on stack memory, triggering Store-to-Load Forwarding (STLF) stalls in the CPU execution pipeline, alongside significant code bloat and unnecessary vector register usage.
---
### Code Example
```cpp
struct LDPC_Decoder {
virtual void compute_check_to_var_msgs(std::span span1,
std::span span2,
std::span span3,
std::span span4,
std::span span5,
uint32_t layer_idx) = 0;
int update_messages(std::span s1, std::span s2,
std::span s3, std::span s4,
std::span s5, uint32_t layer_idx);
};
std::span change_span(std::span input, unsigned shift);
int LDPC_Decoder::update_messages(std::span s1,
std::span s2,
std::span s3,
std::span s4,
std::span s5,
uint32_t layer_idx) {
auto s1c = change_span(s1, 0);
auto s2c = change_span(s3, 0);
auto s3c = change_span(s4, 0);
auto s4c = change_span(s5, 0);
auto s5c = change_span(s5, 0);
compute_check_to_var_msgs(s1c, s2c, s3c, s4c, s5c, layer_idx);
return 0;
}
```
**Flags:** `-O3 -march=znver5 -std=c++20`
**Godbolt:** https://godbolt.org/z/nWExM3Yq
OR
```cpp
std::string_view modify(std::string_view str, uint32_t len);
__attribute__((noinline)) uint32_t compute_size(void *ptr, std::string_view s1,
std::string_view s2,
std::string_view s3,
std::string_view s4,
std::string_view s5) {
return s1.size() + s2.size() + s3.size() + s4.size() + s5.size();
}
__attribute__((noinline)) uint32_t process(std::string_view s1,
std::string_view s2,
std::string_view s3,
std::string_view s4,
std::string_view s5, uint32_t len) {
auto s1c = modify(s1, len);
auto s2c = modify(s3, len);
auto s3c = modify(s4, len);
auto s4c = modify(s5, len);
auto s5c = modify(s5, len);
auto size = compute_size(&s1, s1c, s2c, s3c, s4c, s5c);
return size + 10;
}
```
**Flags:** `-O3 -march=znver5`
**Godbolt:** https://godbolt.org/z/a38qsK5xq
---
### Assembly Comparison
#### GCC 15 (Clean GPR Stack Frame Setup via `push`)
GCC sets up arguments using standard 64-bit general-purpose registers and stack pushes:
```assembly
mov r10, QWORD PTR [rsp+40]
mov rcx, QWORD PTR [r10]
mov rdi, r10
mov r11, QWORD PTR [rcx]
push rdx ; Push 16-byte span members using standard GPRs
push rax
push r12
push r13
push r15
push r14
mov r9d, DWORD PTR [rsp+84]
mov rcx, QWORD PTR [rsp+64]
mov r8, QWORD PTR [rsp+72]
mov rsi, QWORD PTR [rsp+48]
mov rdx, QWORD PTR [rsp+56]
call r11 ; Call virtual method
```
#### Clang 22 (Vector Stack Copies & STLF Stalls)
Clang lowers parameter copies into repeated 128-bit vector loads and stores on stack slots, reading from local stack space directly into xmm0 and writing to the argument call frame:
```assembly
vmovups xmm0, xmmword ptr [rsp + 104] ; Load span struct into XMM
mov rax, qword ptr [rdi]
mov rax, qword ptr [rax]
vmovups xmmword ptr [rsp + 32], xmm0 ; Store span struct onto call stack (STLF hazard)
vmovups xmm0, xmmword ptr [rsp + 120] ; Load next span
vmovups xmmword ptr [rsp + 16], xmm0 ; Store onto call stack
vmovups xmm0, xmmword ptr [rsp + 136] ; Load next span
vmovups xmmword ptr [rsp], xmm0 ; Store onto call stack
mov rsi, qword ptr [rsp + 96]
mov rdx, qword ptr [rsp + 88]
mov rcx, qword ptr [rsp + 72]
mov r8, qword ptr [rsp + 64]
mov r9d, dword ptr [rsp + 60]
call rax ; Call virtual method
```
---
### Benchmark Results (AVX-512 Hardware)
To quantify the real-world impact of this code generation, a microbenchmark calling `process` (from the second code sample) in a tight loop ($10^8$ iterations) was executed on an AVX-512 machine:
| Compiler | Execution Time ($10^8$ calls) | Throughput / Overhead |
| :--- | :--- | :--- |
| **GCC** | **0.50 s** | Baseline |
| **Clang** | **2.22 s** | **> 4.4x slowdown** |
### Impact
1. **Store-to-Load Forwarding (STLF) Stalls & Severe Slowdown:** Executing `vmovups` stores followed immediately by loads/calls targeting overlapping stack locations forces the CPU's memory execution unit to stall when forwarding data between store buffers and load queues. As shown in the benchmark above, this degrades call-setup throughput by **over 4.4x** (0.50s vs 2.22s).
2. **Instruction & Code Bloat:** Generating many vector load/store instructions for a function call setup inflates the binary size and adds unnecessary instruction bloat compared to scalar GPR moves or push instructions.
---
### Expected Behavior
SelectionDAG / FrameLowering in the x86 backend should optimize parameter passing on the stack for small 16-byte structs (`std::span`, `std::string_view`, `std::pair`), preferring scalar GPR copies or `push` instructions over redundant 128-bit vector memory operations.
Contributor guide
Assessment
This issue has not been assessed yet.