You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On xarch, xchg r/m, r and lock xadd r/m, r return the previous memory value in the register operand that supplied the data. CodeGen::genLockedInstructions reflects that with a canSkip copy:
so the copy disappears whenever LSRA assigns the def the same register as the data use.
But in the shared GT_XADD/GT_XCHG case of LinearScan::BuildNode (src/coreclr/jit/lsraxarch.cpp), the target preference is set to the address use, which was just marked delay-free:
A delay-free use stays live across the def, so it interferes with the def and that preference can never be satisfied — it is effectively dead. The data use, which is the register the instruction writes its result into, gets no preference at all. The result is an extra mov reg, dataReg ahead of the atomic whenever the data operand is a computed last-use value whose register LSRA did not happen to reuse.
Affected source shape: any Interlocked.Exchange / Interlocked.Add whose result is consumed and whose data operand is a computed, last-use value.
Repro deltas (from the ; Total bytes of code footers, base → diff): LongXchg 35 → 32 (−3), TwoXchg 46 → 44 (−2), ToArray 39 → 38 (−1), ToOut 11 → 10 (−1). Mixed, LoadedData, ConstData and XaddToArray are byte-identical — notably GT_XADD showed no benefit in this repro, because the Interlocked.Add(...) - (v * w) reconstruction keeps the addend live across the exchange.
Corpus measurement — SuperPMI asmdiffs over 12 windows.x64 collections (aspire.nativeaot, aspnet2.run, benchmarks.run, benchmarks.run_pgo, benchmarks.run_pgo_optrepeat, coreclr_tests.run, libraries.crossgen2, libraries.pmi, libraries_tests.run, libraries_tests_no_tiered_compilation.run, realworld.run, smoke_tests.nativeaot) at commit 4856f0c16f89d8cc625a62ee7bab7ed4afe14f9d, Release JIT, exit 0 with no asserts or replay failures:
Per-collection deltas range from −186 (aspire.nativeaot) to −3,866 (libraries_tests.run). Against the full replayed corpus (1.16 GB of base code) this is −0.0009 %.
SuperPMI's own per-collection byte totals sum to −10,396 instead of −10,649; the 253-byte gap is confined to libraries_tests.run and is consistent with the 9 contexts the diff JIT compiled but the base JIT did not (base-missing 38 vs diff-missing 29). Every other collection agrees exactly between the two views. Diff-missing ≤ base-missing in all 12 collections, so no new missing contexts are attributable to the change.
Measurement limitations, stated explicitly:
No PerfScore figure exists. The Release JIT emits no PerfScore, so SuperPMI reported Total PerfScore of base: 0.0 / of diff: 0.0 for every collection, and jit-analyze against these .dasm files warns "the base metric is 0, the diff metric is 0" and reports 0 improved / 0 regressed for both CodeSize and PerfScore. All aggregates above were therefore computed directly from the ; Total bytes of code N footer of each generated .dasm pair. Whether any context regresses in PerfScore is unknown; a checked JIT build is needed to answer that.
No throughput or timing data. No BenchmarkDotNet run, no tpdiff, no instruction counts, no wall-clock measurement of either the generated code or the JIT itself. Code size is a static metric; the honest claim is "one fewer mov r64, r64 per affected exchange and −10 KB of corpus code", not a measured speedup.
Windows x64 only. No Linux x64, no x86 (which shares this code path), and no DOTNET_JitStressRegs run.
Notes
Scope is src/coreclr/jit/lsraxarch.cpp only, in the shared GT_XADD/GT_XCHG case; arm/arm64 do not use this path. The GT_XORR/GT_XAND cmpxchg-loop path above it is untouched.
Correctness: which architectural register carries the data in and the old value out is unobservable to managed code. The address use keeps its setDelayFree, so the def still cannot alias the address register, and the existing assert((node->GetRegNum() != addr->GetRegNum()) || (node->GetRegNum() == data->GetRegNum())) in genLockedInstructions continues to hold. The byte-register mask for varTypeIsByte must be preserved when the BuildUse is moved.
Remaining risks: (1) PerfScore was never measured, so a PerfScore-negative context cannot be ruled out; (2) no JitStressRegs / non-Windows / x86 validation — a preference is opportunistic and may not hold under register pressure; (3) GT_XADD shows no measured repro benefit, so whether the lock xadd reconstruction pattern also wants this is a separate open question.
diff --git a/src/coreclr/jit/lsraxarch.cpp b/src/coreclr/jit/lsraxarch.cpp
index 2695fe7a850..db93a93c6b8 100644
--- a/src/coreclr/jit/lsraxarch.cpp+++ b/src/coreclr/jit/lsraxarch.cpp@@ -528,10 +528,12 @@ int LinearScan::BuildNode(GenTree* tree)
assert(!addr->isContained());
RefPosition* addrUse = BuildUse(addr);
setDelayFree(addrUse);
- tgtPrefUse = addrUse;
assert(!data->isContained());
- BuildUse(data, varTypeIsByte(tree) ? RBM_BYTE_REGS.GetIntRegSet() : RBM_NONE);- srcCount = 2;+ // `xchg`/`lock xadd` return the previous value in the register that supplied `data`, so+ // prefer the def onto the data use. The address use is delay-free and therefore+ // interferes with the def, so it can never satisfy a target preference.+ tgtPrefUse = BuildUse(data, varTypeIsByte(tree) ? RBM_BYTE_REGS.GetIntRegSet() : RBM_NONE);+ srcCount = 2;
assert(dstCount == 1);
BuildDef(tree);
}
On xarch,
xchg r/m, randlock xadd r/m, rreturn the previous memory value in the register operand that supplied the data.CodeGen::genLockedInstructionsreflects that with acanSkipcopy:so the copy disappears whenever LSRA assigns the def the same register as the data use.
But in the shared
GT_XADD/GT_XCHGcase ofLinearScan::BuildNode(src/coreclr/jit/lsraxarch.cpp), the target preference is set to the address use, which was just marked delay-free:A delay-free use stays live across the def, so it interferes with the def and that preference can never be satisfied — it is effectively dead. The data use, which is the register the instruction writes its result into, gets no preference at all. The result is an extra
mov reg, dataRegahead of the atomic whenever the data operand is a computed last-use value whose register LSRA did not happen to reuse.Affected source shape: any
Interlocked.Exchange/Interlocked.Addwhose result is consumed and whose data operand is a computed, last-use value.Minimal repro
Run with
DOTNET_TieredCompilation=0 DOTNET_ReadyToRun=0 DOTNET_JitDisasmDiffable=1andDOTNET_JitDisasm=LongXchgon x64 Release.Current codegen
B01B:LongXchg(byref,long[],long,long), x64 Release, FullOpts — 35 bytes:mov rax, r8is pure overhead:xchgwould have returned the old value inr8just as well.Expected codegen
Same method with the def preferenced onto the data use — 32 bytes:
Impact
Repro deltas (from the
; Total bytes of codefooters, base → diff):LongXchg35 → 32 (−3),TwoXchg46 → 44 (−2),ToArray39 → 38 (−1),ToOut11 → 10 (−1).Mixed,LoadedData,ConstDataandXaddToArrayare byte-identical — notablyGT_XADDshowed no benefit in this repro, because theInterlocked.Add(...) - (v * w)reconstruction keeps the addend live across the exchange.Corpus measurement — SuperPMI
asmdiffsover 12windows.x64collections (aspire.nativeaot,aspnet2.run,benchmarks.run,benchmarks.run_pgo,benchmarks.run_pgo_optrepeat,coreclr_tests.run,libraries.crossgen2,libraries.pmi,libraries_tests.run,libraries_tests_no_tiered_compilation.run,realworld.run,smoke_tests.nativeaot) at commit4856f0c16f89d8cc625a62ee7bab7ed4afe14f9d, Release JIT, exit 0 with no asserts or replay failures:libraries.crossgen220282.dasm4370 → 4326 (−44)libraries_tests_no_tiered_compilation.run111192.dasm250 → 251 (+1)Per-collection deltas range from −186 (
aspire.nativeaot) to −3,866 (libraries_tests.run). Against the full replayed corpus (1.16 GB of base code) this is −0.0009 %.SuperPMI's own per-collection byte totals sum to −10,396 instead of −10,649; the 253-byte gap is confined to
libraries_tests.runand is consistent with the 9 contexts the diff JIT compiled but the base JIT did not (base-missing 38 vs diff-missing 29). Every other collection agrees exactly between the two views. Diff-missing ≤ base-missing in all 12 collections, so no new missing contexts are attributable to the change.Measurement limitations, stated explicitly:
Total PerfScore of base: 0.0/of diff: 0.0for every collection, andjit-analyzeagainst these.dasmfiles warns "the base metric is 0, the diff metric is 0" and reports 0 improved / 0 regressed for both CodeSize and PerfScore. All aggregates above were therefore computed directly from the; Total bytes of code Nfooter of each generated.dasmpair. Whether any context regresses in PerfScore is unknown; a checked JIT build is needed to answer that.tpdiff, no instruction counts, no wall-clock measurement of either the generated code or the JIT itself. Code size is a static metric; the honest claim is "one fewermov r64, r64per affected exchange and −10 KB of corpus code", not a measured speedup.DOTNET_JitStressRegsrun.Notes
src/coreclr/jit/lsraxarch.cpponly, in the sharedGT_XADD/GT_XCHGcase; arm/arm64 do not use this path. TheGT_XORR/GT_XANDcmpxchg-loop path above it is untouched.setDelayFree, so the def still cannot alias the address register, and the existingassert((node->GetRegNum() != addr->GetRegNum()) || (node->GetRegNum() == data->GetRegNum()))ingenLockedInstructionscontinues to hold. The byte-register mask forvarTypeIsBytemust be preserved when theBuildUseis moved.GT_XADDshows no measured repro benefit, so whether thelock xaddreconstruction pattern also wants this is a separate open question.neg/not/bswapnodes) applied the same idea to a different node set and is already in the base commit. PR JIT: Fix GC reporting for xarch interlocked reference results #133855 ("JIT: Fix GC reporting for xarch interlocked reference results") touchesemitxarch.cppGC liveness for thexchg/cmpxchgresult register, is also already in base, and confirms the result register is the data register.Prototype patch
Experimental patch
Note
This issue was generated with GitHub Copilot.