JIT: (bug) x64: unrolled memmove and initblk use YMM/ZMM but never emit `vzeroupper`
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
`LinearScan::BuildBlockStore` calls `SetContainsAVXFlags()` without the SIMD size for `BlkOpKindUnrollMemmove` and for init-block `BlkOpKindUnroll`, so `SetContains256bitOrMoreAVX(true)` is never set even though codegen emits 32/64-byte moves. The result is a method that dirties the upper `YMM`/`ZMM` state without any `vzeroupper` (and, worse, picks the "prolog-only vzeroupper" optimization before dirtying it).
### Minimal Repro
```csharp
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
public static class P
{
public struct Buf { public long A, B, C, D, E; } // 40 bytes
public struct Buf64 { public long A, B, C, D, E, F, G, H; } // 64 bytes
// BlkOpKindUnrollMemmove - uses ymm0, no vzeroupper
[MethodImpl(MethodImplOptions.NoInlining)]
public static void MemmoveUnroll(byte[] src, Span dst) => src.AsSpan(0, 40).CopyTo(dst);
// BlkOpKindUnroll (copy) - uses ymm0, emits vzeroupper (correct, for contrast)
[MethodImpl(MethodImplOptions.NoInlining)]
public static void BlockCopy(ref Buf a, ref Buf b) => a = b;
// Init-block unroll - uses zmm0, no vzeroupper
[MethodImpl(MethodImplOptions.NoInlining)]
public static void InitBlk(ref Buf64 b) => b = default;
[DllImport("kernel32.dll")] public static extern uint GetTickCount();
[MethodImpl(MethodImplOptions.NoInlining)]
public static uint MemmoveThenPInvoke(byte[] src, Span dst)
{
src.AsSpan(0, 40).CopyTo(dst);
return GetTickCount();
}
static Buf s_a, s_b;
static Buf64 s_c;
public static void Main()
{
var buf = new byte[256];
MemmoveUnroll(buf, buf.AsSpan(64));
BlockCopy(ref s_a, ref s_b);
InitBlk(ref s_c);
MemmoveThenPInvoke(buf, buf.AsSpan(128));
}
}
```
```
set DOTNET_TieredCompilation=0
set DOTNET_JitDisasm=MemmoveUnroll BlockCopy InitBlk MemmoveThenPInvoke
corerun out\repro.dll
```
### Expected
Same handling as the copy-block `BlkOpKindUnroll` path, which is correct today:
```asm
; BlockCopy
vmovdqu ymm0, ymmword ptr [rdx]
vmovdqu ymmword ptr [rcx], ymm0
mov rax, qword ptr [rdx+0x20]
mov qword ptr [rcx+0x20], rax
vzeroupper
ret
```
and, for the P/Invoke case, no prolog `vzeroupper` but one before the call and one in the epilog.
### Actual
```asm
; MemmoveUnroll - ymm0 written, epilog has no vzeroupper
vmovdqu ymm0, ymmword ptr [rcx]
vmovdqu xmm1, xmmword ptr [rcx+0x18]
vmovdqu ymmword ptr [rax], ymm0
vmovdqu xmmword ptr [rax+0x18], xmm1
add rsp, 40
ret
; InitBlk - 512-bit store, no vzeroupper
vxorps ymm0, ymm0, ymm0
vmovdqu32 zmmword ptr [rcx], zmm0
ret
; MemmoveThenPInvoke - prolog-only vzeroupper chosen, then ymm1 is dirtied after it
sub rsp, 88
vzeroupper ; <-- prolog-only optimization, wrong here
...
vmovdqu ymm1, ymmword ptr [rsi] ; <-- dirties upper YMM state
...
call [P:GetTickCount():uint] ; <-- no vzeroupper before the unknown callee
...
ret ; <-- no vzeroupper in the epilog either
```
### Notes
- `src/coreclr/jit/lsraxarch.cpp`: init-block `BlkOpKindUnroll` (~L1462) and `BlkOpKindUnrollMemmove` (~L1567) call `SetContainsAVXFlags()` with no size; copy-block `BlkOpKindUnroll` (~L1504) correctly passes `SetContainsAVXFlags(regSize)`.
- `LinearScan::SetContainsAVXFlags` only sets `SetContains256bitOrMoreAVX(true)` when `sizeOfSIMDVector >= 32`; `genClearAvxStateInProlog`/`genClearAvxStateInEpilog` and the pre-call check in `genCall` all key off it.
- Performance issue (AVX/legacy-SSE transition penalty, dirty upper state across a call), not a correctness one.
- Repro machine: Windows x64 with AVX-512; `main` @ `b44cd904110a27d96ea83621e94332d55150d482`. Also reproduces on released .NET 10.0.12, so not a regression.
Contributor guide
Research direction
Start in src/coreclr/jit/lsraxarch.cpp at the init-block and unrolled-memmove cases noted in the issue, then read LinearScan::SetContainsAVXFlags and the vzeroupper decisions in genClearAvxStateInProlog, genClearAvxStateInEpilog, and genCall. Run the supplied C# repro with JIT disassembly; done means unrolled YMM/ZMM operations receive the same vzeroupper handling as the copy-block path, including around the P/Invoke call.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100