From 86974638e27b1e739eef87087cdf5378d8a3e8ee Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 7 May 2026 21:04:12 +0200 Subject: [PATCH 1/7] JIT: Decouple DNER from liveness --- src/coreclr/jit/codegencommon.cpp | 12 +- src/coreclr/jit/codegenwasm.cpp | 2 +- src/coreclr/jit/compiler.cpp | 2 - src/coreclr/jit/compiler.h | 26 ++- src/coreclr/jit/copyprop.cpp | 8 +- src/coreclr/jit/earlyprop.cpp | 2 +- src/coreclr/jit/gentree.cpp | 18 +- src/coreclr/jit/gschecks.cpp | 2 - src/coreclr/jit/inductionvariableopts.cpp | 16 +- src/coreclr/jit/jiteh.cpp | 2 + src/coreclr/jit/lclvars.cpp | 84 +++---- src/coreclr/jit/liveness.cpp | 259 +++++++--------------- src/coreclr/jit/lower.cpp | 6 + src/coreclr/jit/lsra.cpp | 8 +- src/coreclr/jit/lsra.h | 2 + src/coreclr/jit/morph.cpp | 1 - src/coreclr/jit/optimizer.cpp | 2 +- src/coreclr/jit/regalloc.cpp | 132 +++++++---- src/coreclr/jit/regallocwasm.cpp | 4 +- src/coreclr/jit/regallocwasm.h | 1 + src/coreclr/jit/sideeffects.h | 3 +- 21 files changed, 282 insertions(+), 310 deletions(-) diff --git a/src/coreclr/jit/codegencommon.cpp b/src/coreclr/jit/codegencommon.cpp index 790bf2b96a9d8e..37d2903ae33172 100644 --- a/src/coreclr/jit/codegencommon.cpp +++ b/src/coreclr/jit/codegencommon.cpp @@ -3229,7 +3229,7 @@ void CodeGen::genSpillOrAddRegisterParam( } LclVarDsc* varDsc = m_compiler->lvaGetDesc(lclNum); - if (varDsc->lvOnFrame && (!varDsc->lvIsInReg() || varDsc->lvLiveInOutOfHndlr)) + if (varDsc->lvOnFrame && (!varDsc->lvIsInReg() || varDsc->IsLiveInOutOfHandler())) { LclVarDsc* paramVarDsc = m_compiler->lvaGetDesc(paramLclNum); @@ -3291,7 +3291,7 @@ void CodeGen::genSpillOrAddRegisterParam( void CodeGen::genSpillOrAddNonStandardRegisterParam(unsigned lclNum, regNumber sourceReg, RegGraph* graph) { LclVarDsc* varDsc = m_compiler->lvaGetDesc(lclNum); - if (varDsc->lvOnFrame && (!varDsc->lvIsInReg() || varDsc->lvLiveInOutOfHndlr)) + if (varDsc->lvOnFrame && (!varDsc->lvIsInReg() || varDsc->IsLiveInOutOfHandler())) { GetEmitter()->emitIns_S_R(ins_Store(varDsc->TypeGet()), emitActualTypeSize(varDsc), sourceReg, lclNum, 0); } @@ -3779,7 +3779,7 @@ void CodeGen::genCheckUseBlockInit() { if (!varDsc->lvRegister) { - if (!varDsc->lvIsInReg() || varDsc->lvLiveInOutOfHndlr) + if (!varDsc->lvIsInReg() || varDsc->IsLiveInOutOfHandler()) { // Var is on the stack at entry. initStkLclCnt += @@ -4075,7 +4075,7 @@ void CodeGen::genZeroInitFrame(int untrLclHi, int untrLclLo, regNumber initReg, // Locals that are (only) in registers to begin with do not need // their stack home zeroed. Their register will be zeroed later in // the prolog. - if (varDsc->lvIsInReg() && !varDsc->lvLiveInOutOfHndlr) + if (varDsc->lvIsInReg() && !varDsc->IsLiveInOutOfHandler()) { continue; } @@ -5176,7 +5176,7 @@ void CodeGen::genFnProlog() } bool isInReg = varDsc->lvIsInReg(); - bool isInMemory = !isInReg || varDsc->lvLiveInOutOfHndlr; + bool isInMemory = !isInReg || varDsc->IsLiveInOutOfHandler(); // Note that 'lvIsInReg()' will only be accurate for variables that are actually live-in to // the first block. This will include all possibly-uninitialized locals, whose liveness @@ -5186,7 +5186,7 @@ void CodeGen::genFnProlog() // occupying it on entry. if (isInReg) { - if (m_compiler->lvaEnregEHVars && varDsc->lvLiveInOutOfHndlr) + if (m_compiler->lvaEnregEHVars && varDsc->IsLiveInOutOfHandler()) { isInReg = VarSetOps::IsMember(m_compiler, m_compiler->fgFirstBB->bbLiveIn, varDsc->lvVarIndex); } diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 0d89746cf97da6..72d9f0a6ccc6aa 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -251,7 +251,7 @@ void CodeGen::genHomeRegisterParamsOutsideProlog() return; } - if (varDsc->lvOnFrame && (!varDsc->lvIsInReg() || varDsc->lvLiveInOutOfHndlr)) + if (varDsc->lvOnFrame && (!varDsc->lvIsInReg() || varDsc->IsLiveInOutOfHandler())) { LclVarDsc* paramVarDsc = m_compiler->lvaGetDesc(paramLclNum); var_types storeType = genParamStackType(paramVarDsc, segment); diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index 24b980ccc8f755..d9df91daeb8569 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -10707,11 +10707,9 @@ void Compiler::EnregisterStats::RecordLocal(const LclVarDsc* varDsc) m_longParamField++; break; #endif -#ifdef JIT32_GCENCODER case DoNotEnregisterReason::PinningRef: m_PinningRef++; break; -#endif case DoNotEnregisterReason::LclAddrNode: m_lclAddrNode++; break; diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 875106b746944b..673154015e4442 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -476,9 +476,7 @@ enum class DoNotEnregisterReason #if !defined(TARGET_64BIT) LongParamField, // It is a decomposed field of a long parameter. #endif -#ifdef JIT32_GCENCODER PinningRef, -#endif LclAddrNode, // the local is accessed with LCL_ADDR_VAR/FLD. CastTakesAddr, StoreBlkSrc, // the local is used as STORE_BLK source. @@ -513,6 +511,9 @@ enum class AddressExposedReason class LclVarDsc { + template + friend class Liveness; + public: // note this only packs because var_types is a typedef of unsigned char var_types lvType : 5; // TYP_INT/LONG/FLOAT/DOUBLE/REF @@ -542,12 +543,11 @@ class LclVarDsc bool m_addrExposed : 1; // The address of this variable is "exposed" -- passed as an argument, stored in a // global location, etc. // We cannot reason reliably about the value of the variable. + unsigned char lvLiveInOutOfHandler : 1; // The variable is live in or out of an exception handler. public: unsigned char lvDoNotEnregister : 1; // Do not enregister this variable. unsigned char lvFieldAccessed : 1; // The var is a struct local, and a field of the variable is accessed. Affects // struct promotion. - unsigned char lvLiveInOutOfHndlr : 1; // The variable is live in or out of an exception handler, and therefore must - // be on the stack (at least at those boundaries.) unsigned char lvInSsa : 1; // The variable is in SSA form (set by SsaBuilder) unsigned char lvIsCSE : 1; // Indicates if this LclVar is a CSE variable. @@ -1157,6 +1157,12 @@ class LclVarDsc return IsEnregisterableType(); } + bool IsLiveInOutOfHandler() const + { + assert(lvTracked); + return lvLiveInOutOfHandler != 0; + } + //----------------------------------------------------------------------------- // IsAlwaysAliveInMemory: Determines if this variable's value is always // up-to-date on stack. This is possible if this is an EH-var or @@ -1164,7 +1170,7 @@ class LclVarDsc // bool IsAlwaysAliveInMemory() const { - return lvLiveInOutOfHndlr || lvSpillAtSingleDef; + return IsLiveInOutOfHandler() || lvSpillAtSingleDef; } bool CanBeReplacedWithItsField(Compiler* comp) const; @@ -3870,6 +3876,8 @@ class Compiler bool gtGetIndNodeCost(GenTreeIndir* node, int* pCostEx, int* pCostSz); bool gtGetAddrNodeCost(GenTree* addr, var_types type, bool isVolatile, int* pCostEx, int* pCostSz); + bool IsEHVarARegCandidate(LclVarDsc* varDsc); + // Returns true iff the secondNode can be swapped with firstNode. bool gtCanSwapOrder(GenTree* firstNode, GenTree* secondNode); @@ -4183,10 +4191,10 @@ class Compiler bool lvaVarAddrExposed(unsigned varNum) const; void lvaSetVarAddrExposed(unsigned varNum DEBUGARG(AddressExposedReason reason)); void lvaSetHiddenBufferStructArg(unsigned varNum); - void lvaSetVarLiveInOutOfHandler(unsigned varNum); bool lvaVarDoNotEnregister(unsigned varNum); void lvSetMinOptsDoNotEnreg(); + void lvSetEHVarsDoNotEnreg(); bool lvaEnregEHVars; bool lvaEnregMultiRegVars; @@ -5721,6 +5729,10 @@ class Compiler // bool optLoopsCanonical = false; + // This bool tracks if any EH clauses have been removed after the last time we computed liveness. + // This is a likely indicator that LclVarDsc::IsLiveInOutOfHandler() may be overly conservative. + bool optRemovedEHClausesAfterLiveness = false; + bool fgBBVarSetsInited = false; // Track how many artificial ref counts we've added to fgEntryBB (for OSR) @@ -11978,9 +11990,7 @@ class Compiler unsigned m_depField; unsigned m_noRegVars; unsigned m_wasmGcVisibility; -#ifdef JIT32_GCENCODER unsigned m_PinningRef; -#endif // JIT32_GCENCODER #if !defined(TARGET_64BIT) unsigned m_longParamField; #endif // !TARGET_64BIT diff --git a/src/coreclr/jit/copyprop.cpp b/src/coreclr/jit/copyprop.cpp index 4240ca3f3f3d87..2777fde5f83994 100644 --- a/src/coreclr/jit/copyprop.cpp +++ b/src/coreclr/jit/copyprop.cpp @@ -205,10 +205,12 @@ bool Compiler::optCopyProp( continue; } - // It may not be profitable to propagate a 'doNotEnregister' lclVar to an existing use of an - // enregisterable lclVar. + // It may not be profitable to propagate a local if that changes its expected enregister status. LclVarDsc* const newLclVarDsc = lvaGetDesc(newLclNum); - if (varDsc->lvDoNotEnregister != newLclVarDsc->lvDoNotEnregister) + bool enregOld = !varDsc->lvDoNotEnregister && (!varDsc->IsLiveInOutOfHandler() || IsEHVarARegCandidate(varDsc)); + bool enregNew = !newLclVarDsc->lvDoNotEnregister && + (!newLclVarDsc->IsLiveInOutOfHandler() || IsEHVarARegCandidate(newLclVarDsc)); + if (enregOld != enregNew) { continue; } diff --git a/src/coreclr/jit/earlyprop.cpp b/src/coreclr/jit/earlyprop.cpp index 2ec9c5cc5ed284..3b0d9c7ebe041c 100644 --- a/src/coreclr/jit/earlyprop.cpp +++ b/src/coreclr/jit/earlyprop.cpp @@ -637,7 +637,7 @@ bool Compiler::optCanMoveNullCheckPastTree(GenTree* tree, bool isInsideTry, bool if (tree->OperIs(GT_STORE_LCL_VAR)) { LclVarDsc* varDsc = lvaGetDesc(tree->AsLclVar()); - result = varDsc->lvTracked && !varDsc->lvLiveInOutOfHndlr; + result = varDsc->lvTracked && !varDsc->IsLiveInOutOfHandler(); } } else diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index d91d1961bc2ba4..21795a35e59904 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -5000,7 +5000,7 @@ bool Compiler::gtIsLikelyRegVar(GenTree* tree) // If this is an EH-live var, return false if it is a def, // as it will have to go to memory. - if (varDsc->lvTracked && varDsc->lvLiveInOutOfHndlr && ((tree->gtFlags & GTF_VAR_DEF) != 0)) + if (varDsc->lvTracked && varDsc->IsLiveInOutOfHandler() && ((tree->gtFlags & GTF_VAR_DEF) != 0)) { return false; } @@ -5029,6 +5029,22 @@ bool Compiler::gtIsLikelyRegVar(GenTree* tree) return true; } +//------------------------------------------------------------------------ +// IsEHVarARegCandidate: +// Check if a local is a candidate to go in a register even though it is live +// into EH. +// +// Arguments: +// varDsc - Info about the local +// +// Returns: +// True if so. +// +bool Compiler::IsEHVarARegCandidate(LclVarDsc* varDsc) +{ + return lvaEnregEHVars && varDsc->lvSingleDefRegCandidate && varDsc->lvRefCnt() > 1; +} + //------------------------------------------------------------------------ // gtGetLclVarNodeCost: Calculate the cost for a local variable node. // diff --git a/src/coreclr/jit/gschecks.cpp b/src/coreclr/jit/gschecks.cpp index f8e1e8ca013d1d..0a6e30ca969064 100644 --- a/src/coreclr/jit/gschecks.cpp +++ b/src/coreclr/jit/gschecks.cpp @@ -498,8 +498,6 @@ bool Compiler::gsCreateShadowingLocals() shadowVarDsc->SetAddressExposed(varDsc->IsAddressExposed() DEBUGARG(varDsc->GetAddrExposedReason())); shadowVarDsc->lvDoNotEnregister = varDsc->lvDoNotEnregister; shadowVarDsc->lvSingleDefRegCandidate = varDsc->lvSingleDefRegCandidate; - // The old variable will not be used in handlers anymore, allow it to stay enregistered - varDsc->lvLiveInOutOfHndlr = false; #ifdef DEBUG shadowVarDsc->SetDoNotEnregReason(varDsc->GetDoNotEnregReason()); shadowVarDsc->SetDefinedViaAddress(varDsc->IsDefinedViaAddress()); diff --git a/src/coreclr/jit/inductionvariableopts.cpp b/src/coreclr/jit/inductionvariableopts.cpp index ae3df7acc20e69..3384175eee12ae 100644 --- a/src/coreclr/jit/inductionvariableopts.cpp +++ b/src/coreclr/jit/inductionvariableopts.cpp @@ -470,10 +470,9 @@ bool Compiler::optCanSinkWidenedIV(unsigned lclNum, FlowGraphNaturalLoop* loop) #ifdef DEBUG // We currently do not expect to ever widen IVs that are live into - // exceptional exits. Such IVs are expected to have been marked DNER - // previously (EH write-thru is only for single def locals) which makes it - // unprofitable. If this ever changes we need some more expansive handling - // here. + // exceptional exits. Such IVs are not currently register candidates (EH + // write-thru is only for single def locals) which makes it unprofitable. + // If this ever changes we need some more expansive handling here. loop->VisitLoopBlocks([=](BasicBlock* block) { block->VisitAllSuccs(this, [=](BasicBlock* succ) { if (!loop->ContainsBlock(succ) && bbIsHandlerBeg(succ)) @@ -895,12 +894,11 @@ bool Compiler::optWidenPrimaryIV(FlowGraphNaturalLoop* loop, unsigned lclNum, Sc return false; } - // If the IV is not enregisterable then uses/defs are going to go - // to stack regardless. This check also filters out IVs that may be - // live into exceptional exits since those are always marked DNER. - if (lclDsc->lvDoNotEnregister) + // If the IV is not enregisterable, or if it lives into a handler, then + // uses/defs are going to go to stack regardless. + if (lclDsc->lvDoNotEnregister || lclDsc->IsLiveInOutOfHandler()) { - JITDUMP(" V%02u is marked DNER\n", lclNum); + JITDUMP(" V%02u is marked DNER or lives into a handler\n", lclNum); return false; } diff --git a/src/coreclr/jit/jiteh.cpp b/src/coreclr/jit/jiteh.cpp index f1d29578c94e08..4b06bd94e273a5 100644 --- a/src/coreclr/jit/jiteh.cpp +++ b/src/coreclr/jit/jiteh.cpp @@ -1687,6 +1687,8 @@ void Compiler::fgRemoveEHTableEntry(unsigned XTnum) JITDUMP("... done updating ACD entries after EH removal\n"); } + + optRemovedEHClausesAfterLiveness = true; } //------------------------------------------------------------------------ diff --git a/src/coreclr/jit/lclvars.cpp b/src/coreclr/jit/lclvars.cpp index 5178394885930b..c4db8bf84b08d9 100644 --- a/src/coreclr/jit/lclvars.cpp +++ b/src/coreclr/jit/lclvars.cpp @@ -1338,6 +1338,28 @@ void Compiler::lvSetMinOptsDoNotEnreg() } } +//------------------------------------------------------------------------ +// lvSetEHVarsDoNotEnreg: +// Set locals that we expect to not be enregisterable due to EH as DNER, to +// allow specific optimizations for these by lowering. +// +void Compiler::lvSetEHVarsDoNotEnreg() +{ + for (unsigned lclNum = 0; lclNum < lvaCount; lclNum++) + { + LclVarDsc* dsc = lvaGetDesc(lclNum); + if (dsc->lvDoNotEnregister) + { + continue; + } + + if (dsc->lvTracked && dsc->IsLiveInOutOfHandler() && !IsEHVarARegCandidate(dsc)) + { + lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); + } + } +} + //------------------------------------------------------------------------ // StructPromotionHelper constructor. // @@ -2265,50 +2287,6 @@ void Compiler::lvaSetHiddenBufferStructArg(unsigned varNum) lvaSetVarDoNotEnregister(varNum DEBUGARG(DoNotEnregisterReason::HiddenBufferStructArg)); } -//------------------------------------------------------------------------ -// lvaSetVarLiveInOutOfHandler: Set the local varNum as being live in and/or out of a handler -// -// Arguments: -// varNum - the varNum of the local -// -void Compiler::lvaSetVarLiveInOutOfHandler(unsigned varNum) -{ - LclVarDsc* varDsc = lvaGetDesc(varNum); - - varDsc->lvLiveInOutOfHndlr = 1; - - if (varDsc->lvPromoted) - { - noway_assert(varTypeIsStruct(varDsc)); - - for (unsigned i = varDsc->lvFieldLclStart; i < varDsc->lvFieldLclStart + varDsc->lvFieldCnt; ++i) - { - noway_assert(lvaTable[i].lvIsStructField); - lvaTable[i].lvLiveInOutOfHndlr = 1; - // For now, only enregister an EH Var if it is a single def and whose refCnt > 1. - if (!lvaEnregEHVars || !lvaTable[i].lvSingleDefRegCandidate || lvaTable[i].lvRefCnt() <= 1) - { - lvaSetVarDoNotEnregister(i DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); - } - } - } - - // For now, only enregister an EH Var if it is a single def and whose refCnt > 1. - if (!lvaEnregEHVars || !varDsc->lvSingleDefRegCandidate || varDsc->lvRefCnt() <= 1) - { - lvaSetVarDoNotEnregister(varNum DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); - } -#ifdef JIT32_GCENCODER - else if (lvaKeepAliveAndReportThis() && (varNum == info.compThisArg)) - { - // For the JIT32_GCENCODER, when lvaKeepAliveAndReportThis is true, we must either keep the "this" pointer - // in the same register for the entire method, or keep it on the stack. If it is EH-exposed, we can't ever - // keep it in a register, since it must also be live on the stack. Therefore, we won't attempt to allocate it. - lvaSetVarDoNotEnregister(varNum DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); - } -#endif // JIT32_GCENCODER -} - /***************************************************************************** * * Record that the local var "varNum" should not be enregistered (for one of several reasons.) @@ -2360,7 +2338,6 @@ void Compiler::lvaSetVarDoNotEnregister(unsigned varNum DEBUGARG(DoNotEnregister break; case DoNotEnregisterReason::LiveInOutOfHandler: JITDUMP("live in/out of a handler\n"); - varDsc->lvLiveInOutOfHndlr = 1; break; case DoNotEnregisterReason::BlockOp: JITDUMP("written/read in a block op\n"); @@ -2388,12 +2365,10 @@ void Compiler::lvaSetVarDoNotEnregister(unsigned varNum DEBUGARG(DoNotEnregister JITDUMP("it is a decomposed field of a long parameter\n"); break; #endif -#ifdef JIT32_GCENCODER case DoNotEnregisterReason::PinningRef: JITDUMP("pinning ref\n"); assert(varDsc->lvPinned); break; -#endif case DoNotEnregisterReason::LclAddrNode: JITDUMP("LclAddrVar/Fld takes the address of this node\n"); break; @@ -2431,6 +2406,15 @@ void Compiler::lvaSetVarDoNotEnregister(unsigned varNum DEBUGARG(DoNotEnregister break; } #endif + + if (varDsc->lvPromoted && !wasAlreadyMarkedDoNotEnreg) + { + for (unsigned i = 0; i < varDsc->lvFieldCnt; i++) + { + unsigned fieldLclNum = varDsc->lvFieldLclStart + i; + lvaSetVarDoNotEnregister(fieldLclNum DEBUGARG(DoNotEnregisterReason::DepField)); + } + } } //------------------------------------------------------------------------ @@ -3639,7 +3623,7 @@ void Compiler::lvaComputePreciseRefCounts(bool isRecompute, bool setSlotNumbers) // count those in our heuristic for register allocation, since they always // must be stored, so there's no value in enregistering them at defs; only // if there are enough uses to justify it. - if (varDsc->lvTracked && varDsc->lvLiveInOutOfHndlr && !varDsc->lvDoNotEnregister && + if (varDsc->lvTracked && varDsc->IsLiveInOutOfHandler() && !varDsc->lvDoNotEnregister && ((node->gtFlags & GTF_VAR_DEF) != 0)) { varDsc->incRefCnts(0, this); @@ -6361,7 +6345,7 @@ void Compiler::lvaDumpEntry(unsigned lclNum, FrameLayoutState curState, size_t r { printf("V"); } - if (lvaEnregEHVars && varDsc->lvLiveInOutOfHndlr) + if (lvaEnregEHVars && varDsc->lvTracked && varDsc->IsLiveInOutOfHandler()) { printf("%c", varDsc->lvSingleDefDisqualifyReason); } @@ -6436,7 +6420,7 @@ void Compiler::lvaDumpEntry(unsigned lclNum, FrameLayoutState curState, size_t r { printf(" exact"); } - if (varDsc->lvLiveInOutOfHndlr) + if (varDsc->lvTracked && varDsc->IsLiveInOutOfHandler()) { printf(" EH-live"); } diff --git a/src/coreclr/jit/liveness.cpp b/src/coreclr/jit/liveness.cpp index 8449490c068feb..ff3a6d53009067 100644 --- a/src/coreclr/jit/liveness.cpp +++ b/src/coreclr/jit/liveness.cpp @@ -82,6 +82,7 @@ class Liveness void InterBlockLocalVarLiveness(); void DoLiveVarAnalysis(); + void MarkMustInitAndEHVars(VARSET_VALARG_TP finallyVars, VARSET_VALARG_TP exceptVars); bool PerBlockAnalysis(BasicBlock* block, bool keepAliveThis); void ComputeLife(VARSET_TP& tp, GenTree* startNode, @@ -158,26 +159,6 @@ void Liveness::Init() SelectTrackedLocals(); - // We mark a lcl as must-init in a first pass of local variable - // liveness (Liveness1), then assertion prop eliminates the - // uninit-use of a variable Vk, asserting it will be init'ed to - // null. Then, in a second local-var liveness (Liveness2), the - // variable Vk is no longer live on entry to the method, since its - // uses have been replaced via constant propagation. - // - // This leads to a bug: since Vk is no longer live on entry, the - // register allocator sees Vk and an argument Vj as having - // disjoint lifetimes, and allocates them to the same register. - // But Vk is still marked "must-init", and this initialization (of - // the register) trashes the value in Vj. - // - // Therefore, initialize must-init to false for all variables in - // each liveness phase. - for (unsigned lclNum = 0; lclNum < m_compiler->lvaCount; ++lclNum) - { - m_compiler->lvaTable[lclNum].lvMustInit = false; - } - for (BasicBlock* const block : m_compiler->Blocks()) { block->InitVarSets(m_compiler); @@ -424,124 +405,41 @@ void Liveness::SelectTrackedLocals() LclVarDsc* varDsc = m_compiler->lvaGetDesc(lclNum); // Start by assuming that the variable will be tracked. - varDsc->lvTracked = 1; + bool isTracked = true; INDEBUG(varDsc->lvTrackedWithoutIndex = 0); if (varDsc->lvRefCnt(m_compiler->lvaRefCountState) == 0) { // Zero ref count, make this untracked. - varDsc->lvTracked = 0; + isTracked = false; varDsc->setLvRefCntWtd(0, m_compiler->lvaRefCountState); } -#if !defined(TARGET_64BIT) - if (varTypeIsLong(varDsc) && varDsc->lvPromoted) - { - varDsc->lvTracked = 0; - } -#endif // !defined(TARGET_64BIT) - - // Variables that are address-exposed, and all struct locals, are never enregistered, or tracked. - // (The struct may be promoted, and its field variables enregistered/tracked, or the VM may "normalize" - // its type so that its not seen by the JIT as a struct.) - // Pinned variables may not be tracked (a condition of the GCInfo representation) - // or enregistered, on x86 -- it is believed that we can enregister pinned (more properly, "pinning") - // references when using the general GC encoding. + // Variables that are address-exposed are not tracked except for special cases. if (!TLiveness::TrackAddressExposedLocals && varDsc->IsAddressExposed()) { - varDsc->lvTracked = 0; - assert(varDsc->lvType != TYP_STRUCT || varDsc->lvDoNotEnregister); // For structs, should have set this when - // we set m_addrExposed. - } - if (varTypeIsStruct(varDsc)) - { - // Promoted structs will never be considered for enregistration anyway, - // and the DoNotEnregister flag was used to indicate whether promotion was - // independent or dependent. - if (varDsc->lvPromoted) - { - varDsc->lvTracked = 0; - } - else if (!varDsc->IsEnregisterableType()) - { - m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::NotRegSizeStruct)); - } - else if (varDsc->lvType == TYP_STRUCT) - { - if (!varDsc->lvRegStruct && !m_compiler->compEnregStructLocals()) - { - m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::DontEnregStructs)); - } - else if (varDsc->lvIsMultiRegArgOrRet()) - { - // Prolog and return generators do not support SIMD<->general register moves. - m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::IsStructArg)); - } -#if defined(TARGET_ARM) - else if (varDsc->lvIsParam) - { - // On arm we prespill all struct args, - // TODO-Arm-CQ: keep them in registers, it will need a fix - // to "On the ARM we will spill any incoming struct args" logic in codegencommon. - m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::IsStructArg)); - } -#endif // TARGET_ARM - } + isTracked = false; } - if (varDsc->lvIsStructField && - (m_compiler->lvaGetParentPromotionType(lclNum) != Compiler::PROMOTION_TYPE_INDEPENDENT)) + + // Liveness for promoted structs are tracked more precisely for their fields. + if (varDsc->lvPromoted) { - m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::DepField)); + isTracked = false; } + + // Pinned variables are never tracked since they effectively have + // invisible uses by the runtime that we currently do not reason about. + // For example, the nulling of these will look like dead stores, but + // these are actually observed by GC and important to keep. + // Furthermore, x86 GC encoding does not support enregistering these. if (varDsc->lvPinned) { - varDsc->lvTracked = 0; -#ifdef JIT32_GCENCODER - m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::PinningRef)); -#endif - } - if (!m_compiler->compEnregLocals()) - { - m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::NoRegVars)); + isTracked = false; } - var_types type = genActualType(varDsc->TypeGet()); - - switch (type) - { - case TYP_FLOAT: - case TYP_DOUBLE: - case TYP_INT: - case TYP_LONG: - case TYP_REF: - case TYP_BYREF: -#ifdef FEATURE_SIMD - case TYP_SIMD8: - case TYP_SIMD12: - case TYP_SIMD16: -#ifdef TARGET_XARCH - case TYP_SIMD32: - case TYP_SIMD64: -#endif // TARGET_XARCH -#ifdef FEATURE_MASKED_HW_INTRINSICS - case TYP_MASK: -#endif // FEATURE_MASKED_HW_INTRINSICS -#endif // FEATURE_SIMD - case TYP_STRUCT: - break; - - case TYP_UNDEF: - case TYP_UNKNOWN: - noway_assert(!"lvType not set correctly"); - varDsc->lvType = TYP_INT; + varDsc->lvTracked = isTracked; - FALLTHROUGH; - - default: - varDsc->lvTracked = 0; - } - - if (varDsc->lvTracked) + if (isTracked) { trackedCandidates[trackedCandidateCount++] = lclNum; } @@ -1174,60 +1072,7 @@ void Liveness::InterBlockLocalVarLiveness() } } - if (!TLiveness::IsEarly) - { - LclVarDsc* varDsc; - unsigned varNum; - - for (varNum = 0, varDsc = m_compiler->lvaTable; varNum < m_compiler->lvaCount; varNum++, varDsc++) - { - // Ignore the variable if it's not tracked - - if (!varDsc->lvTracked) - { - continue; - } - - // Fields of dependently promoted structs may be tracked. We shouldn't set lvMustInit on them since - // the whole parent struct will be initialized; however, lvLiveInOutOfHndlr should be set on them - // as appropriate. - - bool fieldOfDependentlyPromotedStruct = m_compiler->lvaIsFieldOfDependentlyPromotedStruct(varDsc); - - // Un-init locals may need auto-initialization. Note that the - // liveness of such locals will bubble to the top (fgFirstBB) - // in fgInterBlockLocalVarLiveness() - - if (!varDsc->lvIsParam && !varDsc->lvIsParamRegTarget && - VarSetOps::IsMember(m_compiler, m_compiler->fgFirstBB->bbLiveIn, varDsc->lvVarIndex) && - (m_compiler->info.compInitMem || varTypeIsGC(varDsc->TypeGet())) && !fieldOfDependentlyPromotedStruct) - { - varDsc->lvMustInit = true; - } - - // Mark all variables that are live on entry to an exception handler - // or on exit from a filter handler or finally. - - bool isFinallyVar = VarSetOps::IsMember(m_compiler, finallyVars, varDsc->lvVarIndex); - if (isFinallyVar || VarSetOps::IsMember(m_compiler, exceptVars, varDsc->lvVarIndex)) - { - // Mark the variable appropriately. - m_compiler->lvaSetVarLiveInOutOfHandler(varNum); - - // Mark all pointer variables live on exit from a 'finally' block as - // 'explicitly initialized' (must-init) for GC-ref types. - - if (isFinallyVar) - { - // Set lvMustInit only if we have a non-arg, GC pointer. - if (!varDsc->lvIsParam && !varDsc->lvIsParamRegTarget && varTypeIsGC(varDsc->TypeGet())) - { - varDsc->lvMustInit = true; - } - } - } - } - } + MarkMustInitAndEHVars(finallyVars, exceptVars); /*------------------------------------------------------------------------- * Now fill in liveness info within each basic block - Backward DataFlow @@ -1485,6 +1330,72 @@ void Liveness::DoLiveVarAnalysis() #endif // DEBUG } +//------------------------------------------------------------------------ +// MarkMustInitAndEHVars: +// Set lvLiveInOutOfHndlr and lvMustInit for variables based on liveness results. +// +// Arguments: +// finallyVars - Locals live into finally blocks +// exceptVars - Locals live into catch handlers +// +template +void Liveness::MarkMustInitAndEHVars(VARSET_VALARG_TP finallyVars, VARSET_VALARG_TP exceptVars) +{ + for (unsigned varNum = 0; varNum < m_compiler->lvaCount; varNum++) + { + LclVarDsc* varDsc = m_compiler->lvaGetDesc(varNum); + varDsc->lvLiveInOutOfHandler = false; + varDsc->lvMustInit = false; + + if (!varDsc->lvTracked) + { + continue; + } + + // Fields of dependently promoted structs may be tracked. We shouldn't set lvMustInit on them since + // the whole parent struct will be initialized; however, lvLiveInOutOfHndlr should be set on them + // as appropriate. + + bool fieldOfDependentlyPromotedStruct = m_compiler->lvaIsFieldOfDependentlyPromotedStruct(varDsc); + + // Un-init locals may need auto-initialization. Note that the + // liveness of such locals will bubble to the top (fgFirstBB) + // in fgInterBlockLocalVarLiveness() + + if (!varDsc->lvIsParam && !varDsc->lvIsParamRegTarget && + VarSetOps::IsMember(m_compiler, m_compiler->fgFirstBB->bbLiveIn, varDsc->lvVarIndex) && + (m_compiler->info.compInitMem || varTypeIsGC(varDsc->TypeGet())) && !fieldOfDependentlyPromotedStruct) + { + varDsc->lvMustInit = true; + } + + // Mark all variables that are live on entry to an exception handler + // or on exit from a filter handler or finally. + + bool isFinallyVar = VarSetOps::IsMember(m_compiler, finallyVars, varDsc->lvVarIndex); + if (isFinallyVar || VarSetOps::IsMember(m_compiler, exceptVars, varDsc->lvVarIndex)) + { + // Mark the variable appropriately. + varDsc->lvLiveInOutOfHandler = true; + assert(!varDsc->lvPromoted); + + // Mark all pointer variables live on exit from a 'finally' block as + // 'explicitly initialized' (must-init) for GC-ref types. + + if (isFinallyVar) + { + // Set lvMustInit only if we have a non-arg, GC pointer. + if (!varDsc->lvIsParam && !varDsc->lvIsParamRegTarget && varTypeIsGC(varDsc->TypeGet())) + { + varDsc->lvMustInit = true; + } + } + } + } + + m_compiler->optRemovedEHClausesAfterLiveness = false; +} + template bool Liveness::PerBlockAnalysis(BasicBlock* block, bool keepAliveThis) { diff --git a/src/coreclr/jit/lower.cpp b/src/coreclr/jit/lower.cpp index 73bba8ae8a1914..2ef9e003f11482 100644 --- a/src/coreclr/jit/lower.cpp +++ b/src/coreclr/jit/lower.cpp @@ -8926,6 +8926,12 @@ PhaseStatus Lowering::DoPhase() // The main reason why this flag is not set is that we are running in minOpts. m_compiler->lvSetMinOptsDoNotEnreg(); } + else + { + // By this point tracked locals live into EH clauses are _very_ likely to not be enregistered. + // DNER this for the same reason above, so that we can contain these locals. + m_compiler->lvSetEHVarsDoNotEnreg(); + } if (m_compiler->opts.OptimizationEnabled() && !m_compiler->opts.IsOSR()) { diff --git a/src/coreclr/jit/lsra.cpp b/src/coreclr/jit/lsra.cpp index 1ea129c15d2ca6..7e53cfc764fa40 100644 --- a/src/coreclr/jit/lsra.cpp +++ b/src/coreclr/jit/lsra.cpp @@ -191,7 +191,7 @@ weight_t LinearScan::getWeight(RefPosition* refPos) if (refPos->getInterval()->isSpilled) { // Decrease the weight if the interval has already been spilled. - if (varDsc->lvLiveInOutOfHndlr || refPos->getInterval()->firstRefPosition->singleDefSpill) + if (varDsc->IsLiveInOutOfHandler() || refPos->getInterval()->firstRefPosition->singleDefSpill) { // An EH-var/single-def is always spilled at defs, and we'll decrease the weight by half, // since only the reload is needed. @@ -1580,7 +1580,7 @@ void LinearScan::identifyCandidatesExceptionDataflow() unsigned varNum = m_compiler->lvaTrackedIndexToLclNum(varIndex); LclVarDsc* varDsc = m_compiler->lvaGetDesc(varNum); - assert(varDsc->lvLiveInOutOfHndlr); + assert(varDsc->IsLiveInOutOfHandler()); if (varTypeIsGC(varDsc) && VarSetOps::IsMember(m_compiler, finallyVars, varIndex) && !varDsc->lvIsParam && !varDsc->lvIsParamRegTarget) @@ -1759,6 +1759,8 @@ void LinearScan::identifyCandidates() // the same register assignment throughout varDsc->lvRegister = false; + checkForDNER(lclNum, varDsc); + if (!isRegCandidate(varDsc)) { varDsc->lvLRACandidate = 0; @@ -1819,7 +1821,7 @@ void LinearScan::identifyCandidates() newInt->isStructField = true; } - if (varDsc->lvLiveInOutOfHndlr) + if (varDsc->IsLiveInOutOfHandler()) { newInt->isWriteThru = varDsc->lvSingleDefRegCandidate; setIntervalAsSpilled(newInt); diff --git a/src/coreclr/jit/lsra.h b/src/coreclr/jit/lsra.h index cf64f827cafa7e..cc2ba1760e6ccb 100644 --- a/src/coreclr/jit/lsra.h +++ b/src/coreclr/jit/lsra.h @@ -987,6 +987,8 @@ class LinearScan : public RegAllocInterface bool isContainableMemoryOp(GenTree* node); + void checkForDNER(unsigned lclNum, LclVarDsc* varDsc); + private: // Determine which locals are candidates for allocation template diff --git a/src/coreclr/jit/morph.cpp b/src/coreclr/jit/morph.cpp index 7a62c78684b03d..65f5c6bf240b5b 100644 --- a/src/coreclr/jit/morph.cpp +++ b/src/coreclr/jit/morph.cpp @@ -15359,7 +15359,6 @@ PhaseStatus Compiler::fgRetypeImplicitByRefArgs() // Propagate address-taken-ness and do-not-enregister-ness. newVarDsc->SetAddressExposed(varDsc->IsAddressExposed() DEBUGARG(varDsc->GetAddrExposedReason())); newVarDsc->lvDoNotEnregister = varDsc->lvDoNotEnregister; - newVarDsc->lvLiveInOutOfHndlr = varDsc->lvLiveInOutOfHndlr; newVarDsc->lvSingleDef = varDsc->lvSingleDef; newVarDsc->lvSingleDefRegCandidate = varDsc->lvSingleDefRegCandidate; newVarDsc->lvSpillAtSingleDef = varDsc->lvSpillAtSingleDef; diff --git a/src/coreclr/jit/optimizer.cpp b/src/coreclr/jit/optimizer.cpp index 636d588bb82d2a..61492dbe91912e 100644 --- a/src/coreclr/jit/optimizer.cpp +++ b/src/coreclr/jit/optimizer.cpp @@ -5665,7 +5665,7 @@ void Compiler::optRemoveRedundantZeroInits() } if (!removedExplicitZeroInit && isEntire && - (!hasImplicitControlFlow || (lclDsc->lvTracked && !lclDsc->lvLiveInOutOfHndlr))) + (!hasImplicitControlFlow || (lclDsc->lvTracked && !lclDsc->IsLiveInOutOfHandler()))) { // If compMethodRequiresPInvokeFrame() returns true, lower may later // insert a call to CORINFO_HELP_INIT_PINVOKE_FRAME but that is not a gc-safe point. diff --git a/src/coreclr/jit/regalloc.cpp b/src/coreclr/jit/regalloc.cpp index 259f9cd9cdfb52..9d4ef80486ed6a 100644 --- a/src/coreclr/jit/regalloc.cpp +++ b/src/coreclr/jit/regalloc.cpp @@ -369,52 +369,6 @@ bool RegAllocImpl::isRegCandidate(LclVarDsc* varDsc) return false; } - // Variables that are address-exposed are never enregistered, or tracked. - // A struct may be promoted, and a struct that fits in a register may be fully enregistered. - // Pinned variables may not be tracked (a condition of the GCInfo representation) - // or enregistered, on x86 -- it is believed that we can enregister pinned (more properly, "pinning") - // references when using the general GC encoding. - unsigned lclNum = compiler->lvaGetLclNum(varDsc); - if (varDsc->IsAddressExposed() || !varDsc->IsEnregisterableType() || - (!compiler->compEnregStructLocals() && (varDsc->lvType == TYP_STRUCT))) - { -#ifdef DEBUG - DoNotEnregisterReason dner; - if (varDsc->IsAddressExposed()) - { - dner = DoNotEnregisterReason::AddrExposed; - } - else if (!varDsc->IsEnregisterableType()) - { - dner = DoNotEnregisterReason::NotRegSizeStruct; - } - else - { - dner = DoNotEnregisterReason::DontEnregStructs; - } -#endif // DEBUG - compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(dner)); - return false; - } - else if (varDsc->lvPinned) - { - varDsc->lvTracked = 0; -#ifdef JIT32_GCENCODER - compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::PinningRef)); -#endif // JIT32_GCENCODER - return false; - } - - // Are we not optimizing and we have exception handlers? - // if so mark all args and locals as volatile, so that they - // won't ever get enregistered. - // - if (compiler->opts.MinOpts() && compiler->compHndBBtabCount > 0) - { - compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); - return false; - } - if (varDsc->lvDoNotEnregister) { return false; @@ -464,6 +418,92 @@ bool RegAllocImpl::isRegCandidate(LclVarDsc* varDsc) return true; } +//------------------------------------------------------------------------ +// checkForDNER: Set the lvDoNotEnregister flag for variables that are not +// eligible for register allocation for a few reasons. +// +// Arguments: +// lclNum - The local number +// varDsc - Info about the local +// +void RegAllocImpl::checkForDNER(unsigned lclNum, LclVarDsc* varDsc) +{ + if (varDsc->lvDoNotEnregister) + { + return; + } + + if (!m_compiler->compEnregLocals()) + { + m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::NoRegVars)); + return; + } + + if (!varDsc->lvPromoted) + { + if (!varDsc->IsEnregisterableType()) + { + m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::NotRegSizeStruct)); + return; + } + + if (varDsc->lvType == TYP_STRUCT) + { + if (!varDsc->lvRegStruct && !m_compiler->compEnregStructLocals()) + { + m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::DontEnregStructs)); + return; + } + + if (varDsc->lvIsMultiRegArgOrRet()) + { + // Prolog and return generators do not support SIMD<->general register moves. + m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::IsStructArg)); + return; + } + +#if defined(TARGET_ARM) + if (varDsc->lvIsParam) + { + // On arm we prespill all struct args, + // TODO-Arm-CQ: keep them in registers, it will need a fix + // to "On the ARM we will spill any incoming struct args" logic in codegencommon. + m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::IsStructArg)); + return; + } +#endif // TARGET_ARM + } + } + + if (varDsc->lvPinned) + { + m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::PinningRef)); + return; + } + + if (varDsc->lvTracked && varDsc->IsLiveInOutOfHandler()) + { + // For now, only enregister an EH Var if it is a single def and whose refCnt > 1. + if (!m_compiler->IsEHVarARegCandidate(varDsc)) + { + m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); + return; + } + +#ifdef JIT32_GCENCODER + if (m_compiler->lvaKeepAliveAndReportThis() && (lclNum == m_compiler->info.compThisArg)) + { + // For the JIT32_GCENCODER, when lvaKeepAliveAndReportThis is true, we must either keep the "this" pointer + // in the same register for the entire method, or keep it on the stack. If it is EH-exposed, we can't ever + // keep it in a register, since it must also be live on the stack. Therefore, we won't attempt to allocate + // it. + m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); + return; + } +#endif // JIT32_GCENCODER + } +} + //------------------------------------------------------------------------ // IsContainableMemoryOp: Checks whether this is a memory op that can be contained. // diff --git a/src/coreclr/jit/regallocwasm.cpp b/src/coreclr/jit/regallocwasm.cpp index 2e296c3110a1e0..d67b8a4deb3e8b 100644 --- a/src/coreclr/jit/regallocwasm.cpp +++ b/src/coreclr/jit/regallocwasm.cpp @@ -170,11 +170,13 @@ void WasmRegAlloc::IdentifyCandidates() LclVarDsc* varDsc = m_compiler->lvaGetDesc(lclNum); varDsc->SetRegNum(REG_STK); + checkForDNER(lclNum, varDsc); + bool varIsRegCandidate = isRegCandidate(varDsc); // Wasm RA currently does not support EH write-thru, so any local live in or out // of a handler must be located only on the stack. - if (varDsc->lvLiveInOutOfHndlr) + if (varDsc->lvTracked && varDsc->IsLiveInOutOfHandler()) { m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); varIsRegCandidate = false; diff --git a/src/coreclr/jit/regallocwasm.h b/src/coreclr/jit/regallocwasm.h index f4cba0b3316819..bc0f399b07e5ae 100644 --- a/src/coreclr/jit/regallocwasm.h +++ b/src/coreclr/jit/regallocwasm.h @@ -131,6 +131,7 @@ class WasmRegAlloc : public RegAllocInterface bool isRegCandidate(LclVarDsc* varDsc); bool isContainableMemoryOp(GenTree* node); + void checkForDNER(unsigned lclNum, LclVarDsc* varDsc); private: LIR::Range& CurrentRange(); diff --git a/src/coreclr/jit/sideeffects.h b/src/coreclr/jit/sideeffects.h index 2fba2ae73cd832..47c4dcadd245f0 100644 --- a/src/coreclr/jit/sideeffects.h +++ b/src/coreclr/jit/sideeffects.h @@ -134,9 +134,10 @@ class AliasSet final // globally visible. LclVarDsc* const varDsc = m_compiler->lvaGetDesc(LclNum()); + if (varDsc->lvTracked) { - return varDsc->lvLiveInOutOfHndlr != 0; + return varDsc->IsLiveInOutOfHandler(); } return m_compiler->compHndBBtabCount > 0; From a1bd12f83cb460c03947cb404c32a379f6c82065 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 7 May 2026 21:23:26 +0200 Subject: [PATCH 2/7] Clean up --- src/coreclr/jit/compiler.h | 4 ---- src/coreclr/jit/jiteh.cpp | 2 -- 2 files changed, 6 deletions(-) diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 673154015e4442..507d7d64d6b4a9 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -5729,10 +5729,6 @@ class Compiler // bool optLoopsCanonical = false; - // This bool tracks if any EH clauses have been removed after the last time we computed liveness. - // This is a likely indicator that LclVarDsc::IsLiveInOutOfHandler() may be overly conservative. - bool optRemovedEHClausesAfterLiveness = false; - bool fgBBVarSetsInited = false; // Track how many artificial ref counts we've added to fgEntryBB (for OSR) diff --git a/src/coreclr/jit/jiteh.cpp b/src/coreclr/jit/jiteh.cpp index 4b06bd94e273a5..f1d29578c94e08 100644 --- a/src/coreclr/jit/jiteh.cpp +++ b/src/coreclr/jit/jiteh.cpp @@ -1687,8 +1687,6 @@ void Compiler::fgRemoveEHTableEntry(unsigned XTnum) JITDUMP("... done updating ACD entries after EH removal\n"); } - - optRemovedEHClausesAfterLiveness = true; } //------------------------------------------------------------------------ From 46c4a433fdf25684f60495d17916d02c41e56885 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 7 May 2026 21:41:51 +0200 Subject: [PATCH 3/7] Feedback --- src/coreclr/jit/liveness.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/coreclr/jit/liveness.cpp b/src/coreclr/jit/liveness.cpp index ff3a6d53009067..f680e3b34e6b57 100644 --- a/src/coreclr/jit/liveness.cpp +++ b/src/coreclr/jit/liveness.cpp @@ -1332,7 +1332,8 @@ void Liveness::DoLiveVarAnalysis() //------------------------------------------------------------------------ // MarkMustInitAndEHVars: -// Set lvLiveInOutOfHndlr and lvMustInit for variables based on liveness results. +// Set lvLiveInOutOfHandler / IsLiveInOutOfHandler() state and lvMustInit +// for variables based on liveness results. // // Arguments: // finallyVars - Locals live into finally blocks From 29a65d28afd1b23c3d060b64a6a4ab32c8af6588 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 7 May 2026 21:42:19 +0200 Subject: [PATCH 4/7] Fix --- src/coreclr/jit/liveness.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/coreclr/jit/liveness.cpp b/src/coreclr/jit/liveness.cpp index f680e3b34e6b57..6768bcab025032 100644 --- a/src/coreclr/jit/liveness.cpp +++ b/src/coreclr/jit/liveness.cpp @@ -1393,8 +1393,6 @@ void Liveness::MarkMustInitAndEHVars(VARSET_VALARG_TP finallyVars, VA } } } - - m_compiler->optRemovedEHClausesAfterLiveness = false; } template From 27cc1343e3bfd2911e8663894e8a9d3c8ece661b Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 7 May 2026 22:00:36 +0200 Subject: [PATCH 5/7] More comments --- src/coreclr/jit/compiler.cpp | 2 -- src/coreclr/jit/liveness.cpp | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index d9df91daeb8569..398cb246253cf3 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -10865,9 +10865,7 @@ void Compiler::EnregisterStats::Dump(FILE* fout) const #if !defined(TARGET_64BIT) PRINT_STATS(m_longParamField, notEnreg); #endif // !TARGET_64BIT -#ifdef JIT32_GCENCODER PRINT_STATS(m_PinningRef, notEnreg); -#endif // JIT32_GCENCODER PRINT_STATS(m_lclAddrNode, notEnreg); PRINT_STATS(m_castTakesAddr, notEnreg); PRINT_STATS(m_storeBlkSrc, notEnreg); diff --git a/src/coreclr/jit/liveness.cpp b/src/coreclr/jit/liveness.cpp index 6768bcab025032..8897e68687c35c 100644 --- a/src/coreclr/jit/liveness.cpp +++ b/src/coreclr/jit/liveness.cpp @@ -1354,7 +1354,7 @@ void Liveness::MarkMustInitAndEHVars(VARSET_VALARG_TP finallyVars, VA } // Fields of dependently promoted structs may be tracked. We shouldn't set lvMustInit on them since - // the whole parent struct will be initialized; however, lvLiveInOutOfHndlr should be set on them + // the whole parent struct will be initialized; however, lvLiveInOutOfHandler should be set on them // as appropriate. bool fieldOfDependentlyPromotedStruct = m_compiler->lvaIsFieldOfDependentlyPromotedStruct(varDsc); From 71f7d46c478895f999e79d92b8e9adea13fc9e91 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Fri, 8 May 2026 10:19:27 +0200 Subject: [PATCH 6/7] Fix 32 bit --- src/coreclr/jit/regalloc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/jit/regalloc.cpp b/src/coreclr/jit/regalloc.cpp index 9d4ef80486ed6a..e765550ef2b4d1 100644 --- a/src/coreclr/jit/regalloc.cpp +++ b/src/coreclr/jit/regalloc.cpp @@ -439,7 +439,7 @@ void RegAllocImpl::checkForDNER(unsigned lclNum, LclVarDsc* varDsc) return; } - if (!varDsc->lvPromoted) + if (varTypeIsStruct(varDsc) && !varDsc->lvPromoted) { if (!varDsc->IsEnregisterableType()) { From f963186f90cd29dcc7e0acfc15d513a056ecb489 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Fri, 8 May 2026 12:10:54 +0200 Subject: [PATCH 7/7] Set DNER when creating LCL_FLD for split arg --- src/coreclr/jit/lower.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/coreclr/jit/lower.cpp b/src/coreclr/jit/lower.cpp index 2ef9e003f11482..1f6da42413e4c8 100644 --- a/src/coreclr/jit/lower.cpp +++ b/src/coreclr/jit/lower.cpp @@ -1838,6 +1838,8 @@ void Lowering::SplitArgumentBetweenRegistersAndStack(GenTreeCall* call, CallArg* m_compiler->gtNewLclFldNode(lcl->GetLclNum(), TYP_STRUCT, lcl->GetLclOffs() + stackSeg.Offset, stackLayout); BlockRange().InsertBefore(arg, stackNode); + m_compiler->lvaSetVarDoNotEnregister(lcl->GetLclNum() DEBUGARG(DoNotEnregisterReason::LocalField)); + registersNode = m_compiler->gtNewFieldList(); BlockRange().InsertBefore(arg, registersNode);