dotnet / dotnet/runtime

JIT: (bug) x64: unrolled memmove and initblk use YMM/ZMM but never emit `vzeroupper`

Open
#133,784 0 comments 0 reactions 0 assignees View on GitHub
area-CodeGen-coreclr
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.