JIT: Rematerialize spilled FP/SIMD/mask constants instead of stack spill/reload#130313
JIT: Rematerialize spilled FP/SIMD/mask constants instead of stack spill/reload#130313tannergooding wants to merge 12 commits into
Conversation
…ill/reload Floating-point, SIMD, and mask constants (CNS_DBL, CNS_VEC, CNS_MSK) have no GC liveness and can be rematerialized cheaply on xarch without a scratch register (zero via xorps, all-bits-set via pcmpeqd/vpternlogd, or a RIP-relative load from the constant's memory home). Rather than spilling such a value to the stack and reloading it, skip creating the spill temp and store in genProduceReg, and rematerialize the constant at the reload point in genUnspillRegIfNeeded. A shared predicate isRematerializableConstant gates both sites and is currently scoped to TARGET_XARCH; other targets can be enabled later for the subset of values that likewise need no scratch register. Values consumed directly from the spill temp as a contained memory operand (GTF_NOREG_AT_USE) continue to be spilled normally. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Extend the constant rematerialization added for xarch to Arm64, for the subset of CNS_DBL/CNS_VEC/CNS_MSK values that genSetRegToConst encodes directly into an instruction without a scratch register: 0.0/fmov-immediate doubles, zero/all-bits- set/movi-broadcast vectors, and all masks (pfalse/ptrue). Values that Arm64 would otherwise load from the constant pool via adrp/ldr need a scratch address register that is not reserved at the reload point, so those remain spilled. isRematerializableConstant gains an Arm64 branch mirroring the no-temp fast paths in genSetRegToConst, and genUnspillRegIfNeeded rematerializes into the reload target via the GenTree* overload of genSetRegToConst (which honors the explicit target register on Arm64). The genProduceReg skip-store site is already gated by the predicate. SPMI asmdiffs (coreclr_tests, windows-arm64): 554 methods improved, 0 regressions, -5432 bytes, with spill store/reload pairs replaced by movi/fmov/mvni/pfalse/ptrue. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
There was a problem hiding this comment.
Pull request overview
This PR changes CoreCLR JIT codegen to avoid stack spill/reload for certain spilled FP/SIMD/mask constants by rematerializing them at the reload point instead. It introduces a shared predicate to ensure the optimization is only applied when the constant can be materialized without requiring scratch registers (and without GC bookkeeping).
Changes:
- Update
CodeGen::genUnspillRegIfNeededto rematerialize eligible spilledGT_CNS_DBL/GT_CNS_VEC/GT_CNS_MSKinto the reload target register (xarch/arm64) instead of loading from a spill temp. - Add
CodeGen::isRematerializableConstantto centralize the eligibility logic and keep the spill-skip and reload-remat sites consistent. - Update
CodeGen::genProduceRegto skip creating a spill temp / emitting the spill store for eligible constants (while preserving the existing behavior forGTF_NOREG_AT_USEcases that must read from the spill temp as a contained memory operand).
Show a summary per file
| File | Description |
|---|---|
| src/coreclr/jit/codegenlinear.cpp | Implements the rematerialization path on unspill, adds the shared predicate, and skips spill-temp/store creation for eligible constants. |
| src/coreclr/jit/codegen.h | Declares the new isRematerializableConstant(GenTree*) helper on CodeGen. |
Copilot's findings
- Files reviewed: 2/2 changed files
- Comments generated: 0
When a rematerializable FP/SIMD/mask constant is spilled immediately after its definition (GTF_SPILL with spill-after semantics), the value is rematerialized at each reload point and its stack store is skipped. The original definition therefore materializes a value into a register that is never read. Add a shared isRematerializedConstantSpill predicate and use it to also skip emitting the definition (genSetRegToConst) at the genCodeForTreeNode sites on xarch and arm64, in addition to the existing store-skip in genProduceReg. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The data section packed entries sequentially with no per-entry alignment padding, so emitDataGenFind's ((curOffs % alignment) == 0) guard could reject a valid match when the existing constant landed at a misaligned offset, emitting a duplicate. Give each dataSection a dsOffset computed via AlignUp in the begin-functions and use it as the single source of truth for lookup, output, and display. This honors the requested alignment (a bug if it wasn't) while SMALL_CODE still gets tight packing via its smaller requested alignment. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sorry for the noise. The |
…on home on xarch When a rematerializable FP/SIMD/mask constant is spilled after its definition (GTF_SPILL) and its use consumes the value from memory (GTF_NOREG_AT_USE), the value has a legal data-section home and never needs a stack spill temp. On xarch, redirect such uses to the constant's data-section location instead of creating a stack spill temp, eliminating the definition, the store, and the stack slot for optimized code. Non-xarch targets that cannot encode the constant as a memory operand retain the prior behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot's findings
Comments suppressed due to low confidence (1)
src/coreclr/jit/emit.cpp:6970
- With the new
dsOffset-based layout (andAlignUpinemitDataGenBeg/emitBBTableDataGenBeg),emitConsDsc.dsdOffscan now include inter-section alignment padding. Summingsec->dsSizeundercounts the logical read-only data size, soMetrics.ReadOnlyDataBytesmay no longer reflect the actual data-section footprint.
{
comp->Metrics.ReadOnlyDataBytes += sec->dsSize;
- Files reviewed: 9/9 changed files
- Comments generated: 0 new
…n xarch Zero and all-bits-set FP/SIMD/mask constants are materialized directly into a register (xorps, pcmpeqd, etc.) and have no data-section home. Marking them reg-optional let the register allocator resolve the use to memory, forcing a data-section load and a new data-section entry that is strictly worse than rematerializing the value in a register. Report such constants as unsafe to mark reg-optional via IsSafeToMarkRegOptional so that a different operand can be chosen instead. Combined with the existing containment refusal for these constants, they now always keep a required register. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The xarch SIMD integer division codegen unconditionally emitted the all-bits-set (negative one) data-section constant used by the signed overflow check, even for unsigned division where that check is skipped. This left an orphaned data-section entry (an all-bits-set constant with no referencing instruction) in the generated code for unsigned SIMD division. Move the constant's emission inside the varTypeIsSigned branch, alongside the int.MinValue constant it is paired with, so it is only materialized when the signed overflow check that consumes it is emitted. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
CC. @dotnet/jit-contrib, @EgorBo, @AndyAyersMS this should be ready for review. This PR does two things.... First, it fixes a bug in method local constant allocation that did not actually respect the alignment. This meant that data was not necessarily aligned as expected and could result in worse perf, potentially bugs on some platforms that required alignment, and was preventing the constant reuse from kicking in. Fixing this reduces the amount of Concretely, for Linux x64 this is Secondly, it adds support for avoiding spilling constants to the stack and instead consuming them directly from their existing method local home. This doesn't happen on EVEX based Windows or Arm64 due to the existence of callee save registers. However, it is more common on Linux where we have no callee save floating-point registers. -- We may be able to optimize this more on Windows/Arm64 in the future by avoiding callee save registers altogether, which will avoid the prologue spill/restore of that register, but that should be a separate PR and may not pan out. |
EgorBo
left a comment
There was a problem hiding this comment.
I took a quick look and it seems to be fine to me, but am no expert in LSRA, so perhaps someone else from @dotnet/jit-contrib can have another look.
Do we effectively do such rematerialization for int constants today? I'd assume it has to be some generic algorithm
|
I wonder what is better - reload a SIMD16 constant from data section after a call or use callee-saved lower parts of two SIMD regs ( |
We have some logic for this already, as part of So for constants, we basically have to consider what this PR does: no spill and restoring directly from RWD section; which assuming its still in the L1 cache from the previous use is roughly 4-8 cycles and +4 bytes of codegen if a contained load or +8 bytes of codegen if it has to be a standalone reload vs actually spilling/reloading or splitting into two callee save registers and recombining across the callsite. Where a memory load/restore is 6 bytes of codegen and 4-8 latency and a insert/extract is also 6 bytes of codegen and roughly 3 cycles of latency. Given us a minimum comparison of +12 bytes and 6 cycles of latency to the inner loop Given that, I believe that trying to save the theoretical 2 cycles per iteration at the cost of even larger codegen and more instructions isn't worth the complexity (and may not pay off since more iterations means more likely being in L1 cache and lower latency for the load); particularly since for it to be "optimal" we'd need to go and add some preferencing logic so that non-constants are preferenced for that splitting and to have constants only do it if there was free space. |
I don't think so and I didn't include it as part of this work. We don't end up with many integer constants coming from memory at all given that x64 has a It may be worth doing that as well, but I'd prefer to check in a different PR; particularly since it also impacts various edges around icon handles, GC liveness, etc. |
|
CC. @jakobbotsch or @AndyAyersMS this is just pending a second pair of jitcontrib eyes on the LSRA impacting changes. |
|
Workflow state for the Holistic Review Orchestrator. {
"version": 5,
"last_dispatched_commit": "f05decb038e7a547fb3ad091deca11af0a89a796",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "051c1fd3c13fea8c1e60b68d9c7f63c3173e5593",
"last_reviewed_commit": "f05decb038e7a547fb3ad091deca11af0a89a796",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "051c1fd3c13fea8c1e60b68d9c7f63c3173e5593",
"last_recorded_worker_run_id": "29684018172",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "f05decb038e7a547fb3ad091deca11af0a89a796",
"review_id": 4730637214
}
]
} |
There was a problem hiding this comment.
Holistic Review
Motivation: FP/SIMD/mask constants (CNS_DBL, CNS_VEC, CNS_MSK) have no GC liveness and, for a well-defined subset of values, can be rematerialized with a single scratch-free instruction (xorps/pcmpeqd on xarch, movi/fmov/mvni/pfalse/ptrue on arm64) or read directly from their data-section home. Spilling such values to the stack and reloading them is strictly wasteful. This PR elides the spill store, the never-read register definition, and the reload, rematerializing instead. The SPMI numbers (554 methods improved, 0 regressions, -5432 bytes on windows-arm64) confirm the CQ win.
Approach: A shared predicate isRematerializableConstant gates both the skip-store site (genProduceReg) and the rematerialization site (genUnspillRegIfNeeded), and is kept deliberately in sync with the scratch-free fast paths of the target's genSetRegToConst. isRematerializedConstantSpill layers the GTF_SPILL/GTF_NOREG_AT_USE semantics on top so both the store and the definition can be dropped. A separate lowering guard (IsConstantMaterializableInRegWithoutMemory) prevents reg-optional/containment of register-only constants that have no data-section home. Finally, emitDataGenBeg and friends now record an explicit dsOffset per data section and align it up, replacing the previously-recomputed packed offset; this fixes the interaction where a rematerialized/contained constant is read from a properly-aligned data-section home and lets duplicate-constant matching reuse aligned entries. The layering is careful and the design comments are unusually thorough and accurate.
Summary: The change is correct and well-scoped. The three predicates correctly mirror the codegen fast paths I verified in genSetRegToConst (arm64 CNS_DBL zero/fmov, CNS_VEC SIMD8/12/16 zero/allbits/movi, all CNS_MSK; xarch all three operators), and the xarch spill-temp bypass in genOperandDesc/emitInsBinary/instr.cpp consistently routes GTF_NOREG_AT_USE constants to the data-section path. The data-section dsOffset refactor is applied consistently across emitEndCodeGen, emitDataGenFind, emitDataOffsetToPtr, and the arm disassembly helper. I have one minor, debug-only observation below; I found no correctness or performance concerns that block merge. Verdict: LGTM.
Detailed Findings
-
Minor (debug-only): xarch switch-table disassembly still uses packed offset accumulation. In
emitxarch.cppemitDispIns(theINS_i_jmpblock, around line 12379), the label-table lookup still walksdsdListaccumulatingoffs += sizeand comparesoffsagainstidMemCookie(the value returned byemitBBTableDataGenBeg). BecauseemitBBTableDataGenBegnow returns an alignment-adjustedsecOffsrather than a packed offset, this disassembly-only lookup can mismatch if inter-section alignment padding is ever introduced ahead of a jump table. The analogous arm helper inemitarm.cppwas correctly updated in this PR to compare againstjdsc->dsOffset; the xarch switch-table path was not. This affects only--disasm/verbose JIT dumps, not generated code, so it is non-blocking, but updating it to usedsOffsetfor consistency would prevent a confusing disassembly label mismatch. -
Observation (no action): The tight coupling between
isRematerializableConstantand each target'sgenSetRegToConstfast paths is a maintenance hazard — a future edit to the arm64movi/fmovencoding conditions would need a matching edit here. The inline comments call this out explicitly and reference the mirrored code, which is the right mitigation given there is no cheap shared abstraction.
Note
This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.
Generated by Holistic Review · 179.2 AIC · ⌖ 10.9 AIC · ⊞ 10K
Floating-point, SIMD, and mask constants (
CNS_DBL,CNS_VEC,CNS_MSK) have no GC liveness and, for a well-defined subset of values, can be rematerialized cheaply without needing a scratch register. Rather than spilling such a value to the stack and reloading it, this PR skips creating the spill temp/store ingenProduceRegand instead rematerializes the constant at the reload point ingenUnspillRegIfNeeded.Details
A shared predicate
isRematerializableConstantgates both the skip-store site and the rematerialization site. It only returnstruefor values thatgenSetRegToConstcan encode directly, i.e. that need no scratch register:xorps), all-bits-set (pcmpeqd/vpternlogd), or a RIP-relative load from the constant''s memory home.0.0/fmov-immediate doubles, zero/all-bits-set/movi-broadcast vectors, and all masks (pfalse/ptrue). Values that Arm64 would otherwise load from the constant pool viaadrp/ldrneed a scratch address register that is not reserved at the reload point, so those remain spilled.Rematerialization goes through the
GenTree*overload ofgenSetRegToConst, which honors the explicit target register. Values consumed directly from the spill temp as a contained memory operand (GTF_NOREG_AT_USE) continue to be spilled normally.Diffs
SPMI asmdiffs (coreclr_tests, windows-arm64): 554 methods improved, 0 regressions, -5432 bytes, with spill store/reload pairs replaced by
movi/fmov/mvni/pfalse/ptrue.Note
This PR description was generated with the assistance of GitHub Copilot.