From e9ae3d89753fbeaf9796d53318f057bf97fc3314 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Tue, 17 Feb 2026 15:12:22 +0100 Subject: [PATCH 01/22] WIP --- src/coreclr/jit/async.cpp | 360 ++++++++++++++++++++++++++++++++++- src/coreclr/jit/fginline.cpp | 2 +- src/coreclr/jit/importer.cpp | 2 +- 3 files changed, 354 insertions(+), 10 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index c900d83ee79106..67b93c82558cde 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -422,17 +422,323 @@ BasicBlock* Compiler::CreateReturnBB(unsigned* mergedReturnLcl) return newReturnBB; } +//------------------------------------------------------------------------ +// IsDefaultValue: +// Check if a node represents a default (zero) value. +// +// Parameters: +// node - The node to check. +// +// Returns: +// True if the node is a constant zero value (integral, floating-point, or +// vector). +// +static bool IsDefaultValue(GenTree* node) +{ + return node->IsIntegralConst(0) || node->IsFloatPositiveZero() || node->IsVectorZero(); +} + +//------------------------------------------------------------------------ +// DefaultValueAnalysis: +// Computes which tracked locals have their default (zero) value at each +// basic block entry. A tracked local that still has its default value at a +// suspension point does not need to be hoisted into the continuation. +// +// The analysis has two phases: +// 1. Per-block: compute which tracked locals are mutated (assigned a +// non-default value or have their address taken) in each block. +// 2. Inter-block: forward dataflow to propagate default value information +// across blocks. At merge points the sets are intersected (a local has +// default value only if it has default value on all incoming paths). +// +class DefaultValueAnalysis +{ + Compiler* m_compiler; + VARSET_TP* m_mutatedVars; // Per-block set of locals mutated to non-default. + VARSET_TP* m_defaultVarsIn; // Per-block set of locals with default value on entry. + + // DataFlow::ForwardAnalysis callback used in Phase 2. + class DataFlowCallback + { + DefaultValueAnalysis& m_analysis; + Compiler* m_compiler; + bool m_isFirstPred; + VARSET_TP m_preMergeIn; + + public: + DataFlowCallback(DefaultValueAnalysis& analysis, Compiler* compiler) + : m_analysis(analysis) + , m_compiler(compiler) + , m_isFirstPred(true) + , m_preMergeIn(VarSetOps::MakeEmpty(compiler)) + { + } + + void StartMerge(BasicBlock* block) + { + // Save the current in set for change detection later. + VarSetOps::Assign(m_compiler, m_preMergeIn, m_analysis.m_defaultVarsIn[block->bbNum]); + + // Optimistically assume all locals have default value; Merge will + // narrow via intersection. + VarSetOps::AssignNoCopy(m_compiler, m_analysis.m_defaultVarsIn[block->bbNum], + VarSetOps::MakeFull(m_compiler)); + m_isFirstPred = true; + } + + void Merge(BasicBlock* block, BasicBlock* predBlock, unsigned dupCount) + { + // The out set of a predecessor is its in set minus the locals + // mutated in that block: defaultOut = defaultIn - mutated. + VARSET_TP predOut(VarSetOps::MakeCopy(m_compiler, m_analysis.m_defaultVarsIn[predBlock->bbNum])); + VarSetOps::DiffD(m_compiler, predOut, m_analysis.m_mutatedVars[predBlock->bbNum]); + + // Intersect: a local has default value only if all predecessors + // agree. + VarSetOps::IntersectionD(m_compiler, m_analysis.m_defaultVarsIn[block->bbNum], predOut); + m_isFirstPred = false; + } + + void MergeHandler(BasicBlock* block, BasicBlock* firstTryBlock, BasicBlock* lastTryBlock) + { + // A handler can be reached from any point in the try region. + // A local has its default value at handler entry only if it has + // its default value at the try region entry AND is not mutated + // anywhere within the try region. + VARSET_TP tryDefault(VarSetOps::MakeCopy(m_compiler, m_analysis.m_defaultVarsIn[firstTryBlock->bbNum])); + + for (BasicBlock* tryBlock = firstTryBlock; tryBlock != lastTryBlock->Next(); + tryBlock = tryBlock->Next()) + { + VarSetOps::DiffD(m_compiler, tryDefault, m_analysis.m_mutatedVars[tryBlock->bbNum]); + } + + VarSetOps::IntersectionD(m_compiler, m_analysis.m_defaultVarsIn[block->bbNum], tryDefault); + m_isFirstPred = false; + } + + bool EndMerge(BasicBlock* block) + { + if (m_isFirstPred) + { + // No predecessors (entry block or unreachable). All locals + // start with default value. + VarSetOps::AssignNoCopy(m_compiler, m_analysis.m_defaultVarsIn[block->bbNum], + VarSetOps::MakeFull(m_compiler)); + } + + return !VarSetOps::Equal(m_compiler, m_preMergeIn, m_analysis.m_defaultVarsIn[block->bbNum]); + } + }; + +public: + DefaultValueAnalysis(Compiler* compiler) + : m_compiler(compiler) + , m_mutatedVars(nullptr) + , m_defaultVarsIn(nullptr) + { + } + + void Run(); + VARSET_TP* GetDefaultVarsIn(BasicBlock* block) const; + +private: + void ComputePerBlockMutatedVars(); + void ComputeInterBlockDefaultValues(); + + INDEBUG(void DumpMutatedVars()); + INDEBUG(void DumpDefaultVarsIn()); +}; + +//------------------------------------------------------------------------ +// DefaultValueAnalysis::Run: +// Run the default value analysis: compute per-block mutation sets, then +// propagate default value information forward through the flow graph. +// +void DefaultValueAnalysis::Run() +{ + ComputePerBlockMutatedVars(); + ComputeInterBlockDefaultValues(); +} + +VARSET_TP* DefaultValueAnalysis::GetDefaultVarsIn(BasicBlock* block) const +{ + return &m_defaultVarsIn[block->bbNum]; +} + +//------------------------------------------------------------------------ +// DefaultValueAnalysis::ComputePerBlockMutatedVars: +// Phase 1: For each reachable basic block compute the set of tracked locals +// that are mutated to a non-default value. +// +// A tracked local is considered mutated if: +// - It has a store (STORE_LCL_VAR / STORE_LCL_FLD) whose data operand is +// not a zero constant. +// - It has a LCL_ADDR use with GTF_VAR_DEF (address taken for a +// definition whose result we cannot reason about). +// +void DefaultValueAnalysis::ComputePerBlockMutatedVars() +{ + m_mutatedVars = m_compiler->fgAllocateTypeForEachBlk(CMK_Async); + + for (unsigned i = 0; i <= m_compiler->fgBBNumMax; i++) + { + VarSetOps::AssignNoCopy(m_compiler, m_mutatedVars[i], VarSetOps::MakeEmpty(m_compiler)); + } + + const FlowGraphDfsTree* dfsTree = m_compiler->m_dfsTree; + for (unsigned i = 0; i < dfsTree->GetPostOrderCount(); i++) + { + BasicBlock* block = dfsTree->GetPostOrder(i); + VARSET_TP& mutated = m_mutatedVars[block->bbNum]; + + for (GenTree* node : LIR::AsRange(block)) + { + if (!node->OperIsLocalStore() && !node->OperIs(GT_LCL_ADDR)) + { + continue; + } + + GenTreeLclVarCommon* lclNode = node->AsLclVarCommon(); + unsigned lclNum = lclNode->GetLclNum(); + LclVarDsc* varDsc = m_compiler->lvaGetDesc(lclNum); + + if (!varDsc->lvTracked) + { + continue; + } + + unsigned varIndex = varDsc->lvVarIndex; + + if (node->OperIs(GT_LCL_ADDR)) + { + // Address taken — we cannot know how the address will be used + // so conservatively treat this as a non-default mutation. + VarSetOps::AddElemD(m_compiler, mutated, varIndex); + continue; + } + + if (node->OperIsLocalStore()) + { + GenTree* data = lclNode->Data(); + if (!IsDefaultValue(data)) + { + VarSetOps::AddElemD(m_compiler, mutated, varIndex); + } + } + } + } + + JITDUMP("Default value analysis: per-block mutated vars\n"); + DBEXEC(m_compiler->verbose, DumpMutatedVars()); +} + +#ifdef DEBUG +//------------------------------------------------------------------------ +// DefaultValueAnalysis::DumpMutatedVars: +// Debug helper to print the per-block mutated variable sets. +// +void DefaultValueAnalysis::DumpMutatedVars() +{ + const FlowGraphDfsTree* dfsTree = m_compiler->m_dfsTree; + for (unsigned i = 0; i < dfsTree->GetPostOrderCount(); i++) + { + BasicBlock* block = dfsTree->GetPostOrder(i); + if (!VarSetOps::IsEmpty(m_compiler, m_mutatedVars[block->bbNum])) + { + printf(" " FMT_BB " mutated: ", block->bbNum); + VarSetOps::Iter iter(m_compiler, m_mutatedVars[block->bbNum]); + unsigned varIndex = 0; + const char* sep = ""; + while (iter.NextElem(&varIndex)) + { + unsigned lclNum = m_compiler->lvaTrackedToVarNum[varIndex]; + printf("%sV%02u", sep, lclNum); + sep = ", "; + } + printf("\n"); + } + } +} +#endif + +//------------------------------------------------------------------------ +// DefaultValueAnalysis::ComputeInterBlockDefaultValues: +// Phase 2: Forward dataflow to compute for each block the set of tracked +// locals that have their default (zero) value on entry. +// +// Transfer function: defaultOut[B] = defaultIn[B] - mutated[B] +// Merge: defaultIn[B] = intersection of defaultOut[pred] for all preds +// +// At entry, all tracked locals have their default value (all bits set in the +// VARSET_TP). +// +void DefaultValueAnalysis::ComputeInterBlockDefaultValues() +{ + m_defaultVarsIn = m_compiler->fgAllocateTypeForEachBlk(CMK_Async); + + for (unsigned i = 0; i <= m_compiler->fgBBNumMax; i++) + { + VarSetOps::AssignNoCopy(m_compiler, m_defaultVarsIn[i], VarSetOps::MakeFull(m_compiler)); + } + + DataFlowCallback callback(*this, m_compiler); + DataFlow flow(m_compiler); + flow.ForwardAnalysis(callback); + + JITDUMP("Default value analysis: per-block default vars on entry\n"); + DBEXEC(m_compiler->verbose, DumpDefaultVarsIn()); +} + +#ifdef DEBUG +//------------------------------------------------------------------------ +// DefaultValueAnalysis::DumpDefaultVarsIn: +// Debug helper to print the per-block default value sets. +// +void DefaultValueAnalysis::DumpDefaultVarsIn() +{ + const FlowGraphDfsTree* dfsTree = m_compiler->m_dfsTree; + for (unsigned i = dfsTree->GetPostOrderCount(); i > 0; i--) + { + BasicBlock* block = dfsTree->GetPostOrder(i - 1); + printf(" " FMT_BB " default: ", block->bbNum); + + if (VarSetOps::IsEmpty(m_compiler, m_defaultVarsIn[block->bbNum])) + { + printf(""); + } + else + { + VarSetOps::Iter iter(m_compiler, m_defaultVarsIn[block->bbNum]); + unsigned varIndex = 0; + const char* sep = ""; + while (iter.NextElem(&varIndex)) + { + unsigned lclNum = m_compiler->lvaTrackedToVarNum[varIndex]; + printf("%sV%02u", sep, lclNum); + sep = ", "; + } + } + printf("\n"); + } +} +#endif + class AsyncLiveness { Compiler* m_compiler; TreeLifeUpdater m_updater; unsigned m_numVars; + DefaultValueAnalysis& m_defaultValueAnalysis; + VARSET_TP m_defaultValues; public: - AsyncLiveness(Compiler* comp) + AsyncLiveness(Compiler* comp, DefaultValueAnalysis& defaultValueAnalysis) : m_compiler(comp) , m_updater(comp) , m_numVars(comp->lvaCount) + , m_defaultValueAnalysis(defaultValueAnalysis) + , m_defaultValues(VarSetOps::MakeEmpty(comp)) { } @@ -457,6 +763,7 @@ class AsyncLiveness void AsyncLiveness::StartBlock(BasicBlock* block) { VarSetOps::Assign(m_compiler, m_compiler->compCurLife, block->bbLiveIn); + VarSetOps::Assign(m_compiler, m_defaultValues, *m_defaultValueAnalysis.GetDefaultVarsIn(block)); } //------------------------------------------------------------------------ @@ -470,6 +777,26 @@ void AsyncLiveness::StartBlock(BasicBlock* block) void AsyncLiveness::Update(GenTree* node) { m_updater.UpdateLife(node); + + if (node->OperIs(GT_LCL_ADDR)) + { + LclVarDsc* dsc = m_compiler->lvaGetDesc(node->AsLclVarCommon()); + if (dsc->lvTracked) + { + VarSetOps::RemoveElemD(m_compiler, m_defaultValues, dsc->lvVarIndex); + } + + return; + } + + if (node->OperIsLocalStore()) + { + LclVarDsc* dsc = m_compiler->lvaGetDesc(node->AsLclVarCommon()); + if (dsc->lvTracked && !IsDefaultValue(node->AsLclVarCommon()->Data())) + { + VarSetOps::RemoveElemD(m_compiler, m_defaultValues, dsc->lvVarIndex); + } + } } //------------------------------------------------------------------------ @@ -585,16 +912,16 @@ bool AsyncLiveness::IsLive(unsigned lclNum) // // A dependently promoted struct is live if any of its fields are live. + bool anyLive = false; + bool allDefault = true; for (unsigned i = 0; i < dsc->lvFieldCnt; i++) { LclVarDsc* fieldDsc = m_compiler->lvaGetDesc(dsc->lvFieldLclStart + i); - if (!fieldDsc->lvTracked || VarSetOps::IsMember(m_compiler, m_compiler->compCurLife, fieldDsc->lvVarIndex)) - { - return true; - } + anyLive |= !fieldDsc->lvTracked || VarSetOps::IsMember(m_compiler, m_compiler->compCurLife, fieldDsc->lvVarIndex); + allDefault &= fieldDsc->lvTracked && VarSetOps::IsMember(m_compiler, m_defaultValues, fieldDsc->lvVarIndex); } - return false; + return anyLive && !allDefault; } if (dsc->lvIsStructField && (m_compiler->lvaGetParentPromotionType(dsc) == Compiler::PROMOTION_TYPE_DEPENDENT)) @@ -602,7 +929,22 @@ bool AsyncLiveness::IsLive(unsigned lclNum) return false; } - return !dsc->lvTracked || VarSetOps::IsMember(m_compiler, m_compiler->compCurLife, dsc->lvVarIndex); + if (!dsc->lvTracked) + { + return true; + } + + if (!VarSetOps::IsMember(m_compiler, m_compiler->compCurLife, dsc->lvVarIndex)) + { + return false; + } + + if (VarSetOps::IsMember(m_compiler, m_defaultValues, dsc->lvVarIndex)) + { + return false; + } + + return true; } //------------------------------------------------------------------------ @@ -725,7 +1067,9 @@ PhaseStatus AsyncTransformation::Run() INDEBUG(m_compiler->mostRecentlyActivePhase = PHASE_ASYNC); VarSetOps::AssignNoCopy(m_compiler, m_compiler->compCurLife, VarSetOps::MakeEmpty(m_compiler)); - AsyncLiveness liveness(m_compiler); + DefaultValueAnalysis defaultValues(m_compiler); + defaultValues.Run(); + AsyncLiveness liveness(m_compiler, defaultValues); // Now walk the IR for all the blocks that contain async calls. Keep track // of liveness and outstanding LIR edges as we go; the LIR edges that cross diff --git a/src/coreclr/jit/fginline.cpp b/src/coreclr/jit/fginline.cpp index a7f6be9f486df2..c0c15346d8f84b 100644 --- a/src/coreclr/jit/fginline.cpp +++ b/src/coreclr/jit/fginline.cpp @@ -2369,7 +2369,7 @@ Statement* Compiler::fgInlinePrependStatements(InlineInfo* inlineInfo) if (tmpNum != BAD_VAR_NUM) { LclVarDsc* const tmpDsc = lvaGetDesc(tmpNum); - if (!fgVarNeedsExplicitZeroInit(tmpNum, bbInALoop, bbIsReturn)) + if (!fgVarNeedsExplicitZeroInit(tmpNum, bbInALoop, bbIsReturn) || impInlineRoot()->compIsAsync()) { JITDUMP("\nSuppressing zero-init for V%02u -- expect to zero in prolog\n", tmpNum); tmpDsc->lvSuppressedZeroInit = 1; diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index 70b2aeb3a46c14..64813a4d37546b 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -8891,7 +8891,7 @@ void Compiler::impImportBlockCode(BasicBlock* block) bool bbIsReturn = block->KindIs(BBJ_RETURN) && (!compIsForInlining() || (impInlineInfo->iciBlock->KindIs(BBJ_RETURN))); LclVarDsc* const lclDsc = lvaGetDesc(lclNum); - if (fgVarNeedsExplicitZeroInit(lclNum, bbInALoop, bbIsReturn)) + if (fgVarNeedsExplicitZeroInit(lclNum, bbInALoop, bbIsReturn) || compIsAsync()) { // Append a tree to zero-out the temp GenTree* newObjInit = From a44cdb57a86338df4454e0b49bc88f84eef8b2fe Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Tue, 17 Feb 2026 15:27:17 +0100 Subject: [PATCH 02/22] Make parameters and OSR locals have non-default values --- src/coreclr/jit/async.cpp | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 67b93c82558cde..56e4968ff3aee6 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -454,8 +454,9 @@ static bool IsDefaultValue(GenTree* node) class DefaultValueAnalysis { Compiler* m_compiler; - VARSET_TP* m_mutatedVars; // Per-block set of locals mutated to non-default. - VARSET_TP* m_defaultVarsIn; // Per-block set of locals with default value on entry. + VARSET_TP* m_mutatedVars; // Per-block set of locals mutated to non-default. + VARSET_TP* m_defaultVarsIn; // Per-block set of locals with default value on entry. + VARSET_TP m_nonDefaultAtEntry; // Locals that do not have default value at method entry. // DataFlow::ForwardAnalysis callback used in Phase 2. class DataFlowCallback @@ -521,10 +522,13 @@ class DefaultValueAnalysis { if (m_isFirstPred) { - // No predecessors (entry block or unreachable). All locals - // start with default value. + // No predecessors (entry block or unreachable). Start with + // all locals having default value, then remove parameters and + // OSR locals whose initial values are not default. VarSetOps::AssignNoCopy(m_compiler, m_analysis.m_defaultVarsIn[block->bbNum], VarSetOps::MakeFull(m_compiler)); + VarSetOps::DiffD(m_compiler, m_analysis.m_defaultVarsIn[block->bbNum], + m_analysis.m_nonDefaultAtEntry); } return !VarSetOps::Equal(m_compiler, m_preMergeIn, m_analysis.m_defaultVarsIn[block->bbNum]); @@ -536,6 +540,7 @@ class DefaultValueAnalysis : m_compiler(compiler) , m_mutatedVars(nullptr) , m_defaultVarsIn(nullptr) + , m_nonDefaultAtEntry(VarSetOps::MakeEmpty(compiler)) { } @@ -682,6 +687,18 @@ void DefaultValueAnalysis::ComputeInterBlockDefaultValues() VarSetOps::AssignNoCopy(m_compiler, m_defaultVarsIn[i], VarSetOps::MakeFull(m_compiler)); } + // Parameters and OSR locals do not have default values at method entry. + for (unsigned i = 0; i < m_compiler->lvaTrackedCount; i++) + { + unsigned lclNum = m_compiler->lvaTrackedToVarNum[i]; + LclVarDsc* varDsc = m_compiler->lvaGetDesc(lclNum); + + if (varDsc->lvIsParam || varDsc->lvIsOSRLocal) + { + VarSetOps::AddElemD(m_compiler, m_nonDefaultAtEntry, varDsc->lvVarIndex); + } + } + DataFlowCallback callback(*this, m_compiler); DataFlow flow(m_compiler); flow.ForwardAnalysis(callback); From 62d1353df4d7a48495a2297dd69d0225aab07ac2 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Tue, 17 Feb 2026 15:27:56 +0100 Subject: [PATCH 03/22] Run jit-format --- src/coreclr/jit/async.cpp | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 56e4968ff3aee6..32855956c8c4b6 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -508,8 +508,7 @@ class DefaultValueAnalysis // anywhere within the try region. VARSET_TP tryDefault(VarSetOps::MakeCopy(m_compiler, m_analysis.m_defaultVarsIn[firstTryBlock->bbNum])); - for (BasicBlock* tryBlock = firstTryBlock; tryBlock != lastTryBlock->Next(); - tryBlock = tryBlock->Next()) + for (BasicBlock* tryBlock = firstTryBlock; tryBlock != lastTryBlock->Next(); tryBlock = tryBlock->Next()) { VarSetOps::DiffD(m_compiler, tryDefault, m_analysis.m_mutatedVars[tryBlock->bbNum]); } @@ -527,8 +526,7 @@ class DefaultValueAnalysis // OSR locals whose initial values are not default. VarSetOps::AssignNoCopy(m_compiler, m_analysis.m_defaultVarsIn[block->bbNum], VarSetOps::MakeFull(m_compiler)); - VarSetOps::DiffD(m_compiler, m_analysis.m_defaultVarsIn[block->bbNum], - m_analysis.m_nonDefaultAtEntry); + VarSetOps::DiffD(m_compiler, m_analysis.m_defaultVarsIn[block->bbNum], m_analysis.m_nonDefaultAtEntry); } return !VarSetOps::Equal(m_compiler, m_preMergeIn, m_analysis.m_defaultVarsIn[block->bbNum]); @@ -544,7 +542,7 @@ class DefaultValueAnalysis { } - void Run(); + void Run(); VARSET_TP* GetDefaultVarsIn(BasicBlock* block) const; private: @@ -594,7 +592,7 @@ void DefaultValueAnalysis::ComputePerBlockMutatedVars() const FlowGraphDfsTree* dfsTree = m_compiler->m_dfsTree; for (unsigned i = 0; i < dfsTree->GetPostOrderCount(); i++) { - BasicBlock* block = dfsTree->GetPostOrder(i); + BasicBlock* block = dfsTree->GetPostOrder(i); VARSET_TP& mutated = m_mutatedVars[block->bbNum]; for (GenTree* node : LIR::AsRange(block)) @@ -690,8 +688,8 @@ void DefaultValueAnalysis::ComputeInterBlockDefaultValues() // Parameters and OSR locals do not have default values at method entry. for (unsigned i = 0; i < m_compiler->lvaTrackedCount; i++) { - unsigned lclNum = m_compiler->lvaTrackedToVarNum[i]; - LclVarDsc* varDsc = m_compiler->lvaGetDesc(lclNum); + unsigned lclNum = m_compiler->lvaTrackedToVarNum[i]; + LclVarDsc* varDsc = m_compiler->lvaGetDesc(lclNum); if (varDsc->lvIsParam || varDsc->lvIsOSRLocal) { @@ -929,12 +927,13 @@ bool AsyncLiveness::IsLive(unsigned lclNum) // // A dependently promoted struct is live if any of its fields are live. - bool anyLive = false; + bool anyLive = false; bool allDefault = true; for (unsigned i = 0; i < dsc->lvFieldCnt; i++) { LclVarDsc* fieldDsc = m_compiler->lvaGetDesc(dsc->lvFieldLclStart + i); - anyLive |= !fieldDsc->lvTracked || VarSetOps::IsMember(m_compiler, m_compiler->compCurLife, fieldDsc->lvVarIndex); + anyLive |= + !fieldDsc->lvTracked || VarSetOps::IsMember(m_compiler, m_compiler->compCurLife, fieldDsc->lvVarIndex); allDefault &= fieldDsc->lvTracked && VarSetOps::IsMember(m_compiler, m_defaultValues, fieldDsc->lvVarIndex); } From 130d0067864ed5324ad41e973976037918552a57 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Tue, 17 Feb 2026 15:29:20 +0100 Subject: [PATCH 04/22] Remove old code --- src/coreclr/jit/fginline.cpp | 2 +- src/coreclr/jit/importer.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/fginline.cpp b/src/coreclr/jit/fginline.cpp index c0c15346d8f84b..a7f6be9f486df2 100644 --- a/src/coreclr/jit/fginline.cpp +++ b/src/coreclr/jit/fginline.cpp @@ -2369,7 +2369,7 @@ Statement* Compiler::fgInlinePrependStatements(InlineInfo* inlineInfo) if (tmpNum != BAD_VAR_NUM) { LclVarDsc* const tmpDsc = lvaGetDesc(tmpNum); - if (!fgVarNeedsExplicitZeroInit(tmpNum, bbInALoop, bbIsReturn) || impInlineRoot()->compIsAsync()) + if (!fgVarNeedsExplicitZeroInit(tmpNum, bbInALoop, bbIsReturn)) { JITDUMP("\nSuppressing zero-init for V%02u -- expect to zero in prolog\n", tmpNum); tmpDsc->lvSuppressedZeroInit = 1; diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index 64813a4d37546b..70b2aeb3a46c14 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -8891,7 +8891,7 @@ void Compiler::impImportBlockCode(BasicBlock* block) bool bbIsReturn = block->KindIs(BBJ_RETURN) && (!compIsForInlining() || (impInlineInfo->iciBlock->KindIs(BBJ_RETURN))); LclVarDsc* const lclDsc = lvaGetDesc(lclNum); - if (fgVarNeedsExplicitZeroInit(lclNum, bbInALoop, bbIsReturn) || compIsAsync()) + if (fgVarNeedsExplicitZeroInit(lclNum, bbInALoop, bbIsReturn)) { // Append a tree to zero-out the temp GenTree* newObjInit = From be260f6515ba66f6ea000853f977a9bea31e8413 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 16:16:28 +0100 Subject: [PATCH 05/22] Feedback --- src/coreclr/jit/async.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 32855956c8c4b6..9dff784ddeaac8 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -577,8 +577,7 @@ VARSET_TP* DefaultValueAnalysis::GetDefaultVarsIn(BasicBlock* block) const // A tracked local is considered mutated if: // - It has a store (STORE_LCL_VAR / STORE_LCL_FLD) whose data operand is // not a zero constant. -// - It has a LCL_ADDR use with GTF_VAR_DEF (address taken for a -// definition whose result we cannot reason about). +// - It has a LCL_ADDR use (address taken that we cannot reason about). // void DefaultValueAnalysis::ComputePerBlockMutatedVars() { From 3e576e8833ab53c33f60570f6169a2772e5eb1e5 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 16:25:12 +0100 Subject: [PATCH 06/22] Add a range variable for the new analysis --- src/coreclr/jit/async.cpp | 17 +++++++++++++++++ src/coreclr/jit/jitconfigvalues.h | 2 ++ 2 files changed, 19 insertions(+) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 9dff784ddeaac8..78221affce00de 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -560,6 +560,23 @@ class DefaultValueAnalysis // void DefaultValueAnalysis::Run() { +#ifdef DEBUG + static ConfigMethodRange s_range; + s_range.EnsureInit(JitConfig.JitAsyncDefaultValueAnalysisRange()); + + if (!s_range.Contains(m_compiler->info.compMethodHash())) + { + JITDUMP("Default value analysis disabled because of method range\n"); + m_defaultVarsIn = m_compiler->fgAllocateTypeForEachBlk(CMK_Async); + for (BasicBlock* block : m_compiler->Blocks()) + { + VarSetOps::AssignNoCopy(m_compiler, m_defaultVarsIn[block->bbNum], VarSetOps::MakeEmpty(m_compiler)); + } + + return; + } + +#endif ComputePerBlockMutatedVars(); ComputeInterBlockDefaultValues(); } diff --git a/src/coreclr/jit/jitconfigvalues.h b/src/coreclr/jit/jitconfigvalues.h index 0ce78c2c314cad..0e4419d2903c08 100644 --- a/src/coreclr/jit/jitconfigvalues.h +++ b/src/coreclr/jit/jitconfigvalues.h @@ -589,6 +589,8 @@ OPT_CONFIG_INTEGER(JitDoOptimizeMaskConversions, "JitDoOptimizeMaskConversions", // conversions OPT_CONFIG_INTEGER(JitOptimizeAwait, "JitOptimizeAwait", 1) // Perform optimization of Await intrinsics +OPT_CONFIG_STRING(JitAsyncDefaultValueAnalysisRange, + "JitAsyncDefaultValueAnalysisRange") // Enable async default value analysis based on method hash range RELEASE_CONFIG_INTEGER(JitEnableOptRepeat, "JitEnableOptRepeat", 1) // If zero, do not allow JitOptRepeat RELEASE_CONFIG_METHODSET(JitOptRepeat, "JitOptRepeat") // Runs optimizer multiple times on specified methods From f4699529c9f7c17ef18e5b4ff9b0072f0668195d Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 16:56:01 +0100 Subject: [PATCH 07/22] Negate inter block set --- src/coreclr/jit/async.cpp | 140 +++++++++++++++++++++++--------------- 1 file changed, 84 insertions(+), 56 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 78221affce00de..550837e39997fd 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -454,9 +454,9 @@ static bool IsDefaultValue(GenTree* node) class DefaultValueAnalysis { Compiler* m_compiler; - VARSET_TP* m_mutatedVars; // Per-block set of locals mutated to non-default. - VARSET_TP* m_defaultVarsIn; // Per-block set of locals with default value on entry. - VARSET_TP m_nonDefaultAtEntry; // Locals that do not have default value at method entry. + VARSET_TP* m_mutatedVars; // Per-block set of locals mutated to non-default. + VARSET_TP* m_mutatedVarsIn; // Per-block set of locals mutated to non-default on entry. + VARSET_TP m_mutatedAtEntry; // Locals that are mutated at method entry (params, OSR locals). // DataFlow::ForwardAnalysis callback used in Phase 2. class DataFlowCallback @@ -478,42 +478,40 @@ class DefaultValueAnalysis void StartMerge(BasicBlock* block) { // Save the current in set for change detection later. - VarSetOps::Assign(m_compiler, m_preMergeIn, m_analysis.m_defaultVarsIn[block->bbNum]); + VarSetOps::Assign(m_compiler, m_preMergeIn, m_analysis.m_mutatedVarsIn[block->bbNum]); - // Optimistically assume all locals have default value; Merge will - // narrow via intersection. - VarSetOps::AssignNoCopy(m_compiler, m_analysis.m_defaultVarsIn[block->bbNum], - VarSetOps::MakeFull(m_compiler)); + // Optimistically assume no locals have been mutated; Merge will + // grow via union. + VarSetOps::ClearD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum]); m_isFirstPred = true; } void Merge(BasicBlock* block, BasicBlock* predBlock, unsigned dupCount) { - // The out set of a predecessor is its in set minus the locals - // mutated in that block: defaultOut = defaultIn - mutated. - VARSET_TP predOut(VarSetOps::MakeCopy(m_compiler, m_analysis.m_defaultVarsIn[predBlock->bbNum])); - VarSetOps::DiffD(m_compiler, predOut, m_analysis.m_mutatedVars[predBlock->bbNum]); - - // Intersect: a local has default value only if all predecessors - // agree. - VarSetOps::IntersectionD(m_compiler, m_analysis.m_defaultVarsIn[block->bbNum], predOut); + // The out set of a predecessor is its in set plus the locals + // mutated in that block: mutatedOut = mutatedIn | mutated. + VARSET_TP predOut(VarSetOps::MakeCopy(m_compiler, m_analysis.m_mutatedVarsIn[predBlock->bbNum])); + VarSetOps::UnionD(m_compiler, predOut, m_analysis.m_mutatedVars[predBlock->bbNum]); + + // Union: a local is mutated if it is mutated on any incoming path. + VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], predOut); m_isFirstPred = false; } void MergeHandler(BasicBlock* block, BasicBlock* firstTryBlock, BasicBlock* lastTryBlock) { // A handler can be reached from any point in the try region. - // A local has its default value at handler entry only if it has - // its default value at the try region entry AND is not mutated - // anywhere within the try region. - VARSET_TP tryDefault(VarSetOps::MakeCopy(m_compiler, m_analysis.m_defaultVarsIn[firstTryBlock->bbNum])); + // A local is mutated at handler entry if it was mutated at try + // entry or mutated anywhere within the try region. + VARSET_TP tryMutated(VarSetOps::MakeCopy(m_compiler, m_analysis.m_mutatedVarsIn[firstTryBlock->bbNum])); - for (BasicBlock* tryBlock = firstTryBlock; tryBlock != lastTryBlock->Next(); tryBlock = tryBlock->Next()) + for (BasicBlock* tryBlock = firstTryBlock; tryBlock != lastTryBlock->Next(); + tryBlock = tryBlock->Next()) { - VarSetOps::DiffD(m_compiler, tryDefault, m_analysis.m_mutatedVars[tryBlock->bbNum]); + VarSetOps::UnionD(m_compiler, tryMutated, m_analysis.m_mutatedVars[tryBlock->bbNum]); } - VarSetOps::IntersectionD(m_compiler, m_analysis.m_defaultVarsIn[block->bbNum], tryDefault); + VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], tryMutated); m_isFirstPred = false; } @@ -521,15 +519,13 @@ class DefaultValueAnalysis { if (m_isFirstPred) { - // No predecessors (entry block or unreachable). Start with - // all locals having default value, then remove parameters and - // OSR locals whose initial values are not default. - VarSetOps::AssignNoCopy(m_compiler, m_analysis.m_defaultVarsIn[block->bbNum], - VarSetOps::MakeFull(m_compiler)); - VarSetOps::DiffD(m_compiler, m_analysis.m_defaultVarsIn[block->bbNum], m_analysis.m_nonDefaultAtEntry); + // No predecessors (entry block or unreachable). Parameters + // and OSR locals are considered mutated at method entry. + VarSetOps::Assign(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], + m_analysis.m_mutatedAtEntry); } - return !VarSetOps::Equal(m_compiler, m_preMergeIn, m_analysis.m_defaultVarsIn[block->bbNum]); + return !VarSetOps::Equal(m_compiler, m_preMergeIn, m_analysis.m_mutatedVarsIn[block->bbNum]); } }; @@ -537,20 +533,21 @@ class DefaultValueAnalysis DefaultValueAnalysis(Compiler* compiler) : m_compiler(compiler) , m_mutatedVars(nullptr) - , m_defaultVarsIn(nullptr) - , m_nonDefaultAtEntry(VarSetOps::MakeEmpty(compiler)) + , m_mutatedVarsIn(nullptr) + , m_mutatedAtEntry(VarSetOps::MakeEmpty(compiler)) { } - void Run(); - VARSET_TP* GetDefaultVarsIn(BasicBlock* block) const; + void Run(); + bool HasDefaultValue(BasicBlock* block, unsigned varIndex) const; + const VARSET_TP& GetMutatedVarsIn(BasicBlock* block) const; private: void ComputePerBlockMutatedVars(); void ComputeInterBlockDefaultValues(); INDEBUG(void DumpMutatedVars()); - INDEBUG(void DumpDefaultVarsIn()); + INDEBUG(void DumpMutatedVarsIn()); }; //------------------------------------------------------------------------ @@ -567,10 +564,10 @@ void DefaultValueAnalysis::Run() if (!s_range.Contains(m_compiler->info.compMethodHash())) { JITDUMP("Default value analysis disabled because of method range\n"); - m_defaultVarsIn = m_compiler->fgAllocateTypeForEachBlk(CMK_Async); + m_mutatedVarsIn = m_compiler->fgAllocateTypeForEachBlk(CMK_Async); for (BasicBlock* block : m_compiler->Blocks()) { - VarSetOps::AssignNoCopy(m_compiler, m_defaultVarsIn[block->bbNum], VarSetOps::MakeEmpty(m_compiler)); + VarSetOps::AssignNoCopy(m_compiler, m_mutatedVarsIn[block->bbNum], VarSetOps::MakeFull(m_compiler)); } return; @@ -581,9 +578,40 @@ void DefaultValueAnalysis::Run() ComputeInterBlockDefaultValues(); } -VARSET_TP* DefaultValueAnalysis::GetDefaultVarsIn(BasicBlock* block) const +//------------------------------------------------------------------------ +// DefaultValueAnalysis::HasDefaultValue: +// Check if a tracked local has its default value at the entry of the +// specified block. +// +// Parameters: +// block - The basic block. +// varIndex - The tracking index of the local variable. +// +// Returns: +// True if the local is guaranteed to have its default value at block entry. +// +bool DefaultValueAnalysis::HasDefaultValue(BasicBlock* block, unsigned varIndex) const +{ + assert(m_mutatedVarsIn != nullptr); + return !VarSetOps::IsMember(m_compiler, m_mutatedVarsIn[block->bbNum], varIndex); +} + +//------------------------------------------------------------------------ +// DefaultValueAnalysis::GetMutatedVarsIn: +// Get the set of tracked locals that have been mutated to a non-default +// value on entry to the specified block. +// +// Parameters: +// block - The basic block. +// +// Returns: +// The VARSET_TP of tracked locals mutated on entry. A local NOT in this +// set is guaranteed to have its default value. +// +const VARSET_TP& DefaultValueAnalysis::GetMutatedVarsIn(BasicBlock* block) const { - return &m_defaultVarsIn[block->bbNum]; + assert(m_mutatedVarsIn != nullptr); + return m_mutatedVarsIn[block->bbNum]; } //------------------------------------------------------------------------ @@ -684,21 +712,20 @@ void DefaultValueAnalysis::DumpMutatedVars() //------------------------------------------------------------------------ // DefaultValueAnalysis::ComputeInterBlockDefaultValues: // Phase 2: Forward dataflow to compute for each block the set of tracked -// locals that have their default (zero) value on entry. +// locals that have been mutated to a non-default value on entry. // -// Transfer function: defaultOut[B] = defaultIn[B] - mutated[B] -// Merge: defaultIn[B] = intersection of defaultOut[pred] for all preds +// Transfer function: mutatedOut[B] = mutatedIn[B] | mutated[B] +// Merge: mutatedIn[B] = union of mutatedOut[pred] for all preds // -// At entry, all tracked locals have their default value (all bits set in the -// VARSET_TP). +// At entry, only parameters and OSR locals are considered mutated. // void DefaultValueAnalysis::ComputeInterBlockDefaultValues() { - m_defaultVarsIn = m_compiler->fgAllocateTypeForEachBlk(CMK_Async); + m_mutatedVarsIn = m_compiler->fgAllocateTypeForEachBlk(CMK_Async); for (unsigned i = 0; i <= m_compiler->fgBBNumMax; i++) { - VarSetOps::AssignNoCopy(m_compiler, m_defaultVarsIn[i], VarSetOps::MakeFull(m_compiler)); + VarSetOps::AssignNoCopy(m_compiler, m_mutatedVarsIn[i], VarSetOps::MakeEmpty(m_compiler)); } // Parameters and OSR locals do not have default values at method entry. @@ -709,7 +736,7 @@ void DefaultValueAnalysis::ComputeInterBlockDefaultValues() if (varDsc->lvIsParam || varDsc->lvIsOSRLocal) { - VarSetOps::AddElemD(m_compiler, m_nonDefaultAtEntry, varDsc->lvVarIndex); + VarSetOps::AddElemD(m_compiler, m_mutatedAtEntry, varDsc->lvVarIndex); } } @@ -717,30 +744,30 @@ void DefaultValueAnalysis::ComputeInterBlockDefaultValues() DataFlow flow(m_compiler); flow.ForwardAnalysis(callback); - JITDUMP("Default value analysis: per-block default vars on entry\n"); - DBEXEC(m_compiler->verbose, DumpDefaultVarsIn()); + JITDUMP("Default value analysis: per-block mutated vars on entry\n"); + DBEXEC(m_compiler->verbose, DumpMutatedVarsIn()); } #ifdef DEBUG //------------------------------------------------------------------------ -// DefaultValueAnalysis::DumpDefaultVarsIn: -// Debug helper to print the per-block default value sets. +// DefaultValueAnalysis::DumpMutatedVarsIn: +// Debug helper to print the per-block mutated-on-entry variable sets. // -void DefaultValueAnalysis::DumpDefaultVarsIn() +void DefaultValueAnalysis::DumpMutatedVarsIn() { const FlowGraphDfsTree* dfsTree = m_compiler->m_dfsTree; for (unsigned i = dfsTree->GetPostOrderCount(); i > 0; i--) { BasicBlock* block = dfsTree->GetPostOrder(i - 1); - printf(" " FMT_BB " default: ", block->bbNum); + printf(" " FMT_BB " mutated on entry: ", block->bbNum); - if (VarSetOps::IsEmpty(m_compiler, m_defaultVarsIn[block->bbNum])) + if (VarSetOps::IsEmpty(m_compiler, m_mutatedVarsIn[block->bbNum])) { printf(""); } else { - VarSetOps::Iter iter(m_compiler, m_defaultVarsIn[block->bbNum]); + VarSetOps::Iter iter(m_compiler, m_mutatedVarsIn[block->bbNum]); unsigned varIndex = 0; const char* sep = ""; while (iter.NextElem(&varIndex)) @@ -794,7 +821,8 @@ class AsyncLiveness void AsyncLiveness::StartBlock(BasicBlock* block) { VarSetOps::Assign(m_compiler, m_compiler->compCurLife, block->bbLiveIn); - VarSetOps::Assign(m_compiler, m_defaultValues, *m_defaultValueAnalysis.GetDefaultVarsIn(block)); + VarSetOps::AssignNoCopy(m_compiler, m_defaultValues, VarSetOps::MakeFull(m_compiler)); + VarSetOps::DiffD(m_compiler, m_defaultValues, m_defaultValueAnalysis.GetMutatedVarsIn(block)); } //------------------------------------------------------------------------ From 8e1a2a009b8456a5ef58d717a3f7ea38210912cd Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 16:58:04 +0100 Subject: [PATCH 08/22] More negation --- src/coreclr/jit/async.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 550837e39997fd..e8f7a3d3d35def 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -788,7 +788,7 @@ class AsyncLiveness TreeLifeUpdater m_updater; unsigned m_numVars; DefaultValueAnalysis& m_defaultValueAnalysis; - VARSET_TP m_defaultValues; + VARSET_TP m_mutatedValues; public: AsyncLiveness(Compiler* comp, DefaultValueAnalysis& defaultValueAnalysis) @@ -796,7 +796,7 @@ class AsyncLiveness , m_updater(comp) , m_numVars(comp->lvaCount) , m_defaultValueAnalysis(defaultValueAnalysis) - , m_defaultValues(VarSetOps::MakeEmpty(comp)) + , m_mutatedValues(VarSetOps::MakeEmpty(comp)) { } @@ -821,8 +821,7 @@ class AsyncLiveness void AsyncLiveness::StartBlock(BasicBlock* block) { VarSetOps::Assign(m_compiler, m_compiler->compCurLife, block->bbLiveIn); - VarSetOps::AssignNoCopy(m_compiler, m_defaultValues, VarSetOps::MakeFull(m_compiler)); - VarSetOps::DiffD(m_compiler, m_defaultValues, m_defaultValueAnalysis.GetMutatedVarsIn(block)); + VarSetOps::Assign(m_compiler, m_mutatedValues, m_defaultValueAnalysis.GetMutatedVarsIn(block)); } //------------------------------------------------------------------------ @@ -842,7 +841,7 @@ void AsyncLiveness::Update(GenTree* node) LclVarDsc* dsc = m_compiler->lvaGetDesc(node->AsLclVarCommon()); if (dsc->lvTracked) { - VarSetOps::RemoveElemD(m_compiler, m_defaultValues, dsc->lvVarIndex); + VarSetOps::AddElemD(m_compiler, m_mutatedValues, dsc->lvVarIndex); } return; @@ -853,7 +852,7 @@ void AsyncLiveness::Update(GenTree* node) LclVarDsc* dsc = m_compiler->lvaGetDesc(node->AsLclVarCommon()); if (dsc->lvTracked && !IsDefaultValue(node->AsLclVarCommon()->Data())) { - VarSetOps::RemoveElemD(m_compiler, m_defaultValues, dsc->lvVarIndex); + VarSetOps::AddElemD(m_compiler, m_mutatedValues, dsc->lvVarIndex); } } } @@ -978,7 +977,7 @@ bool AsyncLiveness::IsLive(unsigned lclNum) LclVarDsc* fieldDsc = m_compiler->lvaGetDesc(dsc->lvFieldLclStart + i); anyLive |= !fieldDsc->lvTracked || VarSetOps::IsMember(m_compiler, m_compiler->compCurLife, fieldDsc->lvVarIndex); - allDefault &= fieldDsc->lvTracked && VarSetOps::IsMember(m_compiler, m_defaultValues, fieldDsc->lvVarIndex); + allDefault &= fieldDsc->lvTracked && !VarSetOps::IsMember(m_compiler, m_mutatedValues, fieldDsc->lvVarIndex); } return anyLive && !allDefault; @@ -999,7 +998,7 @@ bool AsyncLiveness::IsLive(unsigned lclNum) return false; } - if (VarSetOps::IsMember(m_compiler, m_defaultValues, dsc->lvVarIndex)) + if (!VarSetOps::IsMember(m_compiler, m_mutatedValues, dsc->lvVarIndex)) { return false; } From d08d6f70fb7781e348286604795136ded4661c4b Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 17:02:48 +0100 Subject: [PATCH 09/22] Add a helper --- src/coreclr/jit/async.cpp | 93 +++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 52 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index e8f7a3d3d35def..43e553fe8dd73b 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -614,6 +614,45 @@ const VARSET_TP& DefaultValueAnalysis::GetMutatedVarsIn(BasicBlock* block) const return m_mutatedVarsIn[block->bbNum]; } +//------------------------------------------------------------------------ +//------------------------------------------------------------------------ +// UpdateMutatedLocal: +// If the given node is a local store or LCL_ADDR, and the local is tracked, +// mark it as mutated in the provided set. Stores of a default (zero) value +// are not considered mutations. +// +// Parameters: +// compiler - The compiler instance. +// node - The IR node to check. +// mutated - [in/out] The set to update. +// +static void UpdateMutatedLocal(Compiler* compiler, GenTree* node, VARSET_TP& mutated) +{ + if (!node->OperIsLocalStore() && !node->OperIs(GT_LCL_ADDR)) + { + return; + } + + LclVarDsc* varDsc = compiler->lvaGetDesc(node->AsLclVarCommon()); + if (!varDsc->lvTracked) + { + return; + } + + if (node->OperIs(GT_LCL_ADDR)) + { + // Address taken — we cannot know how the address will be used + // so conservatively treat this as a non-default mutation. + VarSetOps::AddElemD(compiler, mutated, varDsc->lvVarIndex); + return; + } + + if (!IsDefaultValue(node->AsLclVarCommon()->Data())) + { + VarSetOps::AddElemD(compiler, mutated, varDsc->lvVarIndex); + } +} + //------------------------------------------------------------------------ // DefaultValueAnalysis::ComputePerBlockMutatedVars: // Phase 1: For each reachable basic block compute the set of tracked locals @@ -641,38 +680,7 @@ void DefaultValueAnalysis::ComputePerBlockMutatedVars() for (GenTree* node : LIR::AsRange(block)) { - if (!node->OperIsLocalStore() && !node->OperIs(GT_LCL_ADDR)) - { - continue; - } - - GenTreeLclVarCommon* lclNode = node->AsLclVarCommon(); - unsigned lclNum = lclNode->GetLclNum(); - LclVarDsc* varDsc = m_compiler->lvaGetDesc(lclNum); - - if (!varDsc->lvTracked) - { - continue; - } - - unsigned varIndex = varDsc->lvVarIndex; - - if (node->OperIs(GT_LCL_ADDR)) - { - // Address taken — we cannot know how the address will be used - // so conservatively treat this as a non-default mutation. - VarSetOps::AddElemD(m_compiler, mutated, varIndex); - continue; - } - - if (node->OperIsLocalStore()) - { - GenTree* data = lclNode->Data(); - if (!IsDefaultValue(data)) - { - VarSetOps::AddElemD(m_compiler, mutated, varIndex); - } - } + UpdateMutatedLocal(m_compiler, node, mutated); } } @@ -835,26 +843,7 @@ void AsyncLiveness::StartBlock(BasicBlock* block) void AsyncLiveness::Update(GenTree* node) { m_updater.UpdateLife(node); - - if (node->OperIs(GT_LCL_ADDR)) - { - LclVarDsc* dsc = m_compiler->lvaGetDesc(node->AsLclVarCommon()); - if (dsc->lvTracked) - { - VarSetOps::AddElemD(m_compiler, m_mutatedValues, dsc->lvVarIndex); - } - - return; - } - - if (node->OperIsLocalStore()) - { - LclVarDsc* dsc = m_compiler->lvaGetDesc(node->AsLclVarCommon()); - if (dsc->lvTracked && !IsDefaultValue(node->AsLclVarCommon()->Data())) - { - VarSetOps::AddElemD(m_compiler, m_mutatedValues, dsc->lvVarIndex); - } - } + UpdateMutatedLocal(m_compiler, node, m_mutatedValues); } //------------------------------------------------------------------------ From fc97454d6da72dcbee1d1917cb7fef9cc091c334 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 17:07:10 +0100 Subject: [PATCH 10/22] Handle dependently promoted locals --- src/coreclr/jit/async.cpp | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 43e553fe8dd73b..da5e0573e21aa1 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -633,23 +633,31 @@ static void UpdateMutatedLocal(Compiler* compiler, GenTree* node, VARSET_TP& mut return; } - LclVarDsc* varDsc = compiler->lvaGetDesc(node->AsLclVarCommon()); - if (!varDsc->lvTracked) - { - return; - } + LclVarDsc* varDsc = compiler->lvaGetDesc(node->AsLclVarCommon()); + bool isMutated = node->OperIs(GT_LCL_ADDR) || !IsDefaultValue(node->AsLclVarCommon()->Data()); - if (node->OperIs(GT_LCL_ADDR)) + if (varDsc->lvTracked) { - // Address taken — we cannot know how the address will be used - // so conservatively treat this as a non-default mutation. - VarSetOps::AddElemD(compiler, mutated, varDsc->lvVarIndex); + if (isMutated) + { + VarSetOps::AddElemD(compiler, mutated, varDsc->lvVarIndex); + } return; } - if (!IsDefaultValue(node->AsLclVarCommon()->Data())) + // For dependently promoted structs the parent is not tracked but the + // field locals are. When the parent is mutated, all tracked fields must + // be marked as mutated as well. + if (isMutated && (compiler->lvaGetPromotionType(varDsc) == Compiler::PROMOTION_TYPE_DEPENDENT)) { - VarSetOps::AddElemD(compiler, mutated, varDsc->lvVarIndex); + for (unsigned i = 0; i < varDsc->lvFieldCnt; i++) + { + LclVarDsc* fieldDsc = compiler->lvaGetDesc(varDsc->lvFieldLclStart + i); + if (fieldDsc->lvTracked) + { + VarSetOps::AddElemD(compiler, mutated, fieldDsc->lvVarIndex); + } + } } } From e5b6f83ca03b7d26da755f5816dce8559be0ee01 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 17:11:16 +0100 Subject: [PATCH 11/22] Refactor --- src/coreclr/jit/async.cpp | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index da5e0573e21aa1..4d0bb81371f239 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -628,27 +628,23 @@ const VARSET_TP& DefaultValueAnalysis::GetMutatedVarsIn(BasicBlock* block) const // static void UpdateMutatedLocal(Compiler* compiler, GenTree* node, VARSET_TP& mutated) { - if (!node->OperIsLocalStore() && !node->OperIs(GT_LCL_ADDR)) + if ((!node->OperIsLocalStore() || IsDefaultValue(node->AsLclVarCommon()->Data())) && !node->OperIs(GT_LCL_ADDR)) { return; } - LclVarDsc* varDsc = compiler->lvaGetDesc(node->AsLclVarCommon()); - bool isMutated = node->OperIs(GT_LCL_ADDR) || !IsDefaultValue(node->AsLclVarCommon()->Data()); + LclVarDsc* varDsc = compiler->lvaGetDesc(node->AsLclVarCommon()); if (varDsc->lvTracked) { - if (isMutated) - { - VarSetOps::AddElemD(compiler, mutated, varDsc->lvVarIndex); - } + VarSetOps::AddElemD(compiler, mutated, varDsc->lvVarIndex); return; } - // For dependently promoted structs the parent is not tracked but the - // field locals are. When the parent is mutated, all tracked fields must - // be marked as mutated as well. - if (isMutated && (compiler->lvaGetPromotionType(varDsc) == Compiler::PROMOTION_TYPE_DEPENDENT)) + // For promoted structs the parent may not be tracked but the field locals + // are. When the parent is mutated, all tracked fields must be marked as + // mutated as well. + if (varDsc->lvPromoted) { for (unsigned i = 0; i < varDsc->lvFieldCnt; i++) { From bccdfa1ba3026cf72679a6a23c8f25d6ff55cc29 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 17:13:38 +0100 Subject: [PATCH 12/22] Clean up --- src/coreclr/jit/async.cpp | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 4d0bb81371f239..6a7b0c4bb2ead9 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -539,7 +539,6 @@ class DefaultValueAnalysis } void Run(); - bool HasDefaultValue(BasicBlock* block, unsigned varIndex) const; const VARSET_TP& GetMutatedVarsIn(BasicBlock* block) const; private: @@ -578,24 +577,6 @@ void DefaultValueAnalysis::Run() ComputeInterBlockDefaultValues(); } -//------------------------------------------------------------------------ -// DefaultValueAnalysis::HasDefaultValue: -// Check if a tracked local has its default value at the entry of the -// specified block. -// -// Parameters: -// block - The basic block. -// varIndex - The tracking index of the local variable. -// -// Returns: -// True if the local is guaranteed to have its default value at block entry. -// -bool DefaultValueAnalysis::HasDefaultValue(BasicBlock* block, unsigned varIndex) const -{ - assert(m_mutatedVarsIn != nullptr); - return !VarSetOps::IsMember(m_compiler, m_mutatedVarsIn[block->bbNum], varIndex); -} - //------------------------------------------------------------------------ // DefaultValueAnalysis::GetMutatedVarsIn: // Get the set of tracked locals that have been mutated to a non-default @@ -964,16 +945,16 @@ bool AsyncLiveness::IsLive(unsigned lclNum) // A dependently promoted struct is live if any of its fields are live. bool anyLive = false; - bool allDefault = true; + bool anyMutated = false; for (unsigned i = 0; i < dsc->lvFieldCnt; i++) { LclVarDsc* fieldDsc = m_compiler->lvaGetDesc(dsc->lvFieldLclStart + i); anyLive |= !fieldDsc->lvTracked || VarSetOps::IsMember(m_compiler, m_compiler->compCurLife, fieldDsc->lvVarIndex); - allDefault &= fieldDsc->lvTracked && !VarSetOps::IsMember(m_compiler, m_mutatedValues, fieldDsc->lvVarIndex); + anyMutated |= !fieldDsc->lvTracked || VarSetOps::IsMember(m_compiler, m_mutatedValues, fieldDsc->lvVarIndex); } - return anyLive && !allDefault; + return anyLive && anyMutated; } if (dsc->lvIsStructField && (m_compiler->lvaGetParentPromotionType(dsc) == Compiler::PROMOTION_TYPE_DEPENDENT)) From 55c5df88f0676049f834612c97629402ef50e5af Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 17:15:37 +0100 Subject: [PATCH 13/22] Run jit-format --- src/coreclr/jit/async.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 6a7b0c4bb2ead9..497871fde7443c 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -454,9 +454,9 @@ static bool IsDefaultValue(GenTree* node) class DefaultValueAnalysis { Compiler* m_compiler; - VARSET_TP* m_mutatedVars; // Per-block set of locals mutated to non-default. - VARSET_TP* m_mutatedVarsIn; // Per-block set of locals mutated to non-default on entry. - VARSET_TP m_mutatedAtEntry; // Locals that are mutated at method entry (params, OSR locals). + VARSET_TP* m_mutatedVars; // Per-block set of locals mutated to non-default. + VARSET_TP* m_mutatedVarsIn; // Per-block set of locals mutated to non-default on entry. + VARSET_TP m_mutatedAtEntry; // Locals that are mutated at method entry (params, OSR locals). // DataFlow::ForwardAnalysis callback used in Phase 2. class DataFlowCallback @@ -505,8 +505,7 @@ class DefaultValueAnalysis // entry or mutated anywhere within the try region. VARSET_TP tryMutated(VarSetOps::MakeCopy(m_compiler, m_analysis.m_mutatedVarsIn[firstTryBlock->bbNum])); - for (BasicBlock* tryBlock = firstTryBlock; tryBlock != lastTryBlock->Next(); - tryBlock = tryBlock->Next()) + for (BasicBlock* tryBlock = firstTryBlock; tryBlock != lastTryBlock->Next(); tryBlock = tryBlock->Next()) { VarSetOps::UnionD(m_compiler, tryMutated, m_analysis.m_mutatedVars[tryBlock->bbNum]); } @@ -521,8 +520,7 @@ class DefaultValueAnalysis { // No predecessors (entry block or unreachable). Parameters // and OSR locals are considered mutated at method entry. - VarSetOps::Assign(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], - m_analysis.m_mutatedAtEntry); + VarSetOps::Assign(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], m_analysis.m_mutatedAtEntry); } return !VarSetOps::Equal(m_compiler, m_preMergeIn, m_analysis.m_mutatedVarsIn[block->bbNum]); @@ -538,7 +536,7 @@ class DefaultValueAnalysis { } - void Run(); + void Run(); const VARSET_TP& GetMutatedVarsIn(BasicBlock* block) const; private: @@ -951,7 +949,8 @@ bool AsyncLiveness::IsLive(unsigned lclNum) LclVarDsc* fieldDsc = m_compiler->lvaGetDesc(dsc->lvFieldLclStart + i); anyLive |= !fieldDsc->lvTracked || VarSetOps::IsMember(m_compiler, m_compiler->compCurLife, fieldDsc->lvVarIndex); - anyMutated |= !fieldDsc->lvTracked || VarSetOps::IsMember(m_compiler, m_mutatedValues, fieldDsc->lvVarIndex); + anyMutated |= + !fieldDsc->lvTracked || VarSetOps::IsMember(m_compiler, m_mutatedValues, fieldDsc->lvVarIndex); } return anyLive && anyMutated; From 6b15438751b2aa9d2451fdf1d7ebd2dc9a90902e Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 19 Feb 2026 14:44:48 +0100 Subject: [PATCH 14/22] Run jit-format --- src/coreclr/jit/async.cpp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 5ae9d776db0529..56ba8a2166ace2 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -607,7 +607,24 @@ const VARSET_TP& DefaultValueAnalysis::GetMutatedVarsIn(BasicBlock* block) const // static void UpdateMutatedLocal(Compiler* compiler, GenTree* node, VARSET_TP& mutated) { - if ((!node->OperIsLocalStore() || IsDefaultValue(node->AsLclVarCommon()->Data())) && !node->OperIs(GT_LCL_ADDR)) + if (node->OperIsLocalStore()) + { + // If this is a zero initialization then we do not need to consider it + // mutated if we know the prolog will zero it anyway (otherwise we + // could be skipping this explicit zero init on resumption). + // We could improve this a bit by still skipping it but inserting + // explicit zero init on resumption, but these cases seem to be rare. + if (IsDefaultValue(node->AsLclVarCommon()->Data()) && + !compiler->fgVarNeedsExplicitZeroInit(node->AsLclVarCommon()->GetLclNum(), false, false)) + { + return; + } + } + else if (node->OperIs(GT_LCL_ADDR)) + { + // Fall through + } + else { return; } From 1f5f6c905f86f26d0702ad9febb49c1f1e2d710a Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 19 Feb 2026 14:50:42 +0100 Subject: [PATCH 15/22] Small cleanup --- src/coreclr/jit/async.cpp | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 56ba8a2166ace2..86703004383555 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -422,22 +422,6 @@ BasicBlock* Compiler::CreateReturnBB(unsigned* mergedReturnLcl) return newReturnBB; } -//------------------------------------------------------------------------ -// IsDefaultValue: -// Check if a node represents a default (zero) value. -// -// Parameters: -// node - The node to check. -// -// Returns: -// True if the node is a constant zero value (integral, floating-point, or -// vector). -// -static bool IsDefaultValue(GenTree* node) -{ - return node->IsIntegralConst(0) || node->IsFloatPositiveZero() || node->IsVectorZero(); -} - //------------------------------------------------------------------------ // DefaultValueAnalysis: // Computes which tracked locals have their default (zero) value at each @@ -593,6 +577,22 @@ const VARSET_TP& DefaultValueAnalysis::GetMutatedVarsIn(BasicBlock* block) const return m_mutatedVarsIn[block->bbNum]; } +//------------------------------------------------------------------------ +// IsDefaultValue: +// Check if a node represents a default (zero) value. +// +// Parameters: +// node - The node to check. +// +// Returns: +// True if the node is a constant zero value (integral, floating-point, or +// vector). +// +static bool IsDefaultValue(GenTree* node) +{ + return node->IsIntegralConst(0) || node->IsFloatPositiveZero() || node->IsVectorZero(); +} + //------------------------------------------------------------------------ //------------------------------------------------------------------------ // UpdateMutatedLocal: From 4b4dc817289f20c9c3b07080b6f81ca90d88f25d Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 19 Feb 2026 14:59:32 +0100 Subject: [PATCH 16/22] More clean up --- src/coreclr/jit/async.cpp | 89 +++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 51 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 86703004383555..5b0135d5f320f1 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -447,14 +447,12 @@ class DefaultValueAnalysis { DefaultValueAnalysis& m_analysis; Compiler* m_compiler; - bool m_isFirstPred; VARSET_TP m_preMergeIn; public: DataFlowCallback(DefaultValueAnalysis& analysis, Compiler* compiler) : m_analysis(analysis) , m_compiler(compiler) - , m_isFirstPred(true) , m_preMergeIn(VarSetOps::MakeEmpty(compiler)) { } @@ -467,19 +465,13 @@ class DefaultValueAnalysis // Optimistically assume no locals have been mutated; Merge will // grow via union. VarSetOps::ClearD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum]); - m_isFirstPred = true; } void Merge(BasicBlock* block, BasicBlock* predBlock, unsigned dupCount) { // The out set of a predecessor is its in set plus the locals // mutated in that block: mutatedOut = mutatedIn | mutated. - VARSET_TP predOut(VarSetOps::MakeCopy(m_compiler, m_analysis.m_mutatedVarsIn[predBlock->bbNum])); - VarSetOps::UnionD(m_compiler, predOut, m_analysis.m_mutatedVars[predBlock->bbNum]); - - // Union: a local is mutated if it is mutated on any incoming path. - VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], predOut); - m_isFirstPred = false; + VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], m_analysis.m_mutatedVars[predBlock->bbNum]); } void MergeHandler(BasicBlock* block, BasicBlock* firstTryBlock, BasicBlock* lastTryBlock) @@ -487,24 +479,19 @@ class DefaultValueAnalysis // A handler can be reached from any point in the try region. // A local is mutated at handler entry if it was mutated at try // entry or mutated anywhere within the try region. - VARSET_TP tryMutated(VarSetOps::MakeCopy(m_compiler, m_analysis.m_mutatedVarsIn[firstTryBlock->bbNum])); - for (BasicBlock* tryBlock = firstTryBlock; tryBlock != lastTryBlock->Next(); tryBlock = tryBlock->Next()) { - VarSetOps::UnionD(m_compiler, tryMutated, m_analysis.m_mutatedVars[tryBlock->bbNum]); + VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], m_analysis.m_mutatedVars[tryBlock->bbNum]); } - - VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], tryMutated); - m_isFirstPred = false; } bool EndMerge(BasicBlock* block) { - if (m_isFirstPred) + if (block == m_compiler->fgFirstBB) { - // No predecessors (entry block or unreachable). Parameters - // and OSR locals are considered mutated at method entry. - VarSetOps::Assign(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], m_analysis.m_mutatedAtEntry); + // Parameters and OSR locals are considered mutated at method + // entry. + VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], m_analysis.m_mutatedAtEntry); } return !VarSetOps::Equal(m_compiler, m_preMergeIn, m_analysis.m_mutatedVarsIn[block->bbNum]); @@ -527,8 +514,10 @@ class DefaultValueAnalysis void ComputePerBlockMutatedVars(); void ComputeInterBlockDefaultValues(); - INDEBUG(void DumpMutatedVars()); - INDEBUG(void DumpMutatedVarsIn()); +#ifdef DEBUG + void DumpMutatedVars(); + void DumpMutatedVarsIn(); +#endif }; //------------------------------------------------------------------------ @@ -688,35 +677,6 @@ void DefaultValueAnalysis::ComputePerBlockMutatedVars() DBEXEC(m_compiler->verbose, DumpMutatedVars()); } -#ifdef DEBUG -//------------------------------------------------------------------------ -// DefaultValueAnalysis::DumpMutatedVars: -// Debug helper to print the per-block mutated variable sets. -// -void DefaultValueAnalysis::DumpMutatedVars() -{ - const FlowGraphDfsTree* dfsTree = m_compiler->m_dfsTree; - for (unsigned i = 0; i < dfsTree->GetPostOrderCount(); i++) - { - BasicBlock* block = dfsTree->GetPostOrder(i); - if (!VarSetOps::IsEmpty(m_compiler, m_mutatedVars[block->bbNum])) - { - printf(" " FMT_BB " mutated: ", block->bbNum); - VarSetOps::Iter iter(m_compiler, m_mutatedVars[block->bbNum]); - unsigned varIndex = 0; - const char* sep = ""; - while (iter.NextElem(&varIndex)) - { - unsigned lclNum = m_compiler->lvaTrackedToVarNum[varIndex]; - printf("%sV%02u", sep, lclNum); - sep = ", "; - } - printf("\n"); - } - } -} -#endif - //------------------------------------------------------------------------ // DefaultValueAnalysis::ComputeInterBlockDefaultValues: // Phase 2: Forward dataflow to compute for each block the set of tracked @@ -757,6 +717,33 @@ void DefaultValueAnalysis::ComputeInterBlockDefaultValues() } #ifdef DEBUG +//------------------------------------------------------------------------ +// DefaultValueAnalysis::DumpMutatedVars: +// Debug helper to print the per-block mutated variable sets. +// +void DefaultValueAnalysis::DumpMutatedVars() +{ + const FlowGraphDfsTree* dfsTree = m_compiler->m_dfsTree; + for (unsigned i = 0; i < dfsTree->GetPostOrderCount(); i++) + { + BasicBlock* block = dfsTree->GetPostOrder(i); + if (!VarSetOps::IsEmpty(m_compiler, m_mutatedVars[block->bbNum])) + { + printf(" " FMT_BB " mutated: ", block->bbNum); + VarSetOps::Iter iter(m_compiler, m_mutatedVars[block->bbNum]); + unsigned varIndex = 0; + const char* sep = ""; + while (iter.NextElem(&varIndex)) + { + unsigned lclNum = m_compiler->lvaTrackedToVarNum[varIndex]; + printf("%sV%02u", sep, lclNum); + sep = " "; + } + printf("\n"); + } + } +} + //------------------------------------------------------------------------ // DefaultValueAnalysis::DumpMutatedVarsIn: // Debug helper to print the per-block mutated-on-entry variable sets. @@ -782,7 +769,7 @@ void DefaultValueAnalysis::DumpMutatedVarsIn() { unsigned lclNum = m_compiler->lvaTrackedToVarNum[varIndex]; printf("%sV%02u", sep, lclNum); - sep = ", "; + sep = " "; } } printf("\n"); From 8530e7871d55b10306363e29c4307a250be5782d Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 19 Feb 2026 15:00:47 +0100 Subject: [PATCH 17/22] Clean up and optimize --- src/coreclr/jit/async.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 5b0135d5f320f1..eb8f6664da7788 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -432,8 +432,8 @@ BasicBlock* Compiler::CreateReturnBB(unsigned* mergedReturnLcl) // 1. Per-block: compute which tracked locals are mutated (assigned a // non-default value or have their address taken) in each block. // 2. Inter-block: forward dataflow to propagate default value information -// across blocks. At merge points the sets are intersected (a local has -// default value only if it has default value on all incoming paths). +// across blocks. At merge points the sets are union (a local is mutated +// if it is mutated on any incoming path). // class DefaultValueAnalysis { @@ -471,7 +471,8 @@ class DefaultValueAnalysis { // The out set of a predecessor is its in set plus the locals // mutated in that block: mutatedOut = mutatedIn | mutated. - VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], m_analysis.m_mutatedVars[predBlock->bbNum]); + VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], + m_analysis.m_mutatedVars[predBlock->bbNum]); } void MergeHandler(BasicBlock* block, BasicBlock* firstTryBlock, BasicBlock* lastTryBlock) @@ -481,7 +482,8 @@ class DefaultValueAnalysis // entry or mutated anywhere within the try region. for (BasicBlock* tryBlock = firstTryBlock; tryBlock != lastTryBlock->Next(); tryBlock = tryBlock->Next()) { - VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], m_analysis.m_mutatedVars[tryBlock->bbNum]); + VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], + m_analysis.m_mutatedVars[tryBlock->bbNum]); } } From e068e72659bc64688187307d0866d667e0a65f74 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 19 Feb 2026 15:03:33 +0100 Subject: [PATCH 18/22] Fix comment --- src/coreclr/jit/async.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index eb8f6664da7788..a82a7ec8a0b20a 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -432,7 +432,7 @@ BasicBlock* Compiler::CreateReturnBB(unsigned* mergedReturnLcl) // 1. Per-block: compute which tracked locals are mutated (assigned a // non-default value or have their address taken) in each block. // 2. Inter-block: forward dataflow to propagate default value information -// across blocks. At merge points the sets are union (a local is mutated +// across blocks. At merge points the sets are unioned (a local is mutated // if it is mutated on any incoming path). // class DefaultValueAnalysis From 91236b609fec15e5cedf1acca97206229e6869d2 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 19 Feb 2026 15:06:57 +0100 Subject: [PATCH 19/22] Nit --- src/coreclr/jit/async.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index a82a7ec8a0b20a..da6614a05e8289 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -604,9 +604,11 @@ static void UpdateMutatedLocal(Compiler* compiler, GenTree* node, VARSET_TP& mut // mutated if we know the prolog will zero it anyway (otherwise we // could be skipping this explicit zero init on resumption). // We could improve this a bit by still skipping it but inserting - // explicit zero init on resumption, but these cases seem to be rare. + // explicit zero init on resumption, but these cases seem to be rare + // and that would require tracking additional information. if (IsDefaultValue(node->AsLclVarCommon()->Data()) && - !compiler->fgVarNeedsExplicitZeroInit(node->AsLclVarCommon()->GetLclNum(), false, false)) + !compiler->fgVarNeedsExplicitZeroInit(node->AsLclVarCommon()->GetLclNum(), /* bbInALoop */ false, + /* bbIsReturn */ false)) { return; } From e99c0df148cc4835f8c93c0d0e7ae46dd7febf0c Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 19 Feb 2026 15:21:20 +0100 Subject: [PATCH 20/22] Few more fixes --- src/coreclr/jit/async.cpp | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index da6614a05e8289..7fc2e02fa4b4a7 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -438,9 +438,8 @@ BasicBlock* Compiler::CreateReturnBB(unsigned* mergedReturnLcl) class DefaultValueAnalysis { Compiler* m_compiler; - VARSET_TP* m_mutatedVars; // Per-block set of locals mutated to non-default. - VARSET_TP* m_mutatedVarsIn; // Per-block set of locals mutated to non-default on entry. - VARSET_TP m_mutatedAtEntry; // Locals that are mutated at method entry (params, OSR locals). + VARSET_TP* m_mutatedVars; // Per-block set of locals mutated to non-default. + VARSET_TP* m_mutatedVarsIn; // Per-block set of locals mutated to non-default on entry. // DataFlow::ForwardAnalysis callback used in Phase 2. class DataFlowCallback @@ -453,7 +452,7 @@ class DefaultValueAnalysis DataFlowCallback(DefaultValueAnalysis& analysis, Compiler* compiler) : m_analysis(analysis) , m_compiler(compiler) - , m_preMergeIn(VarSetOps::MakeEmpty(compiler)) + , m_preMergeIn(VarSetOps::UninitVal()) { } @@ -471,6 +470,8 @@ class DefaultValueAnalysis { // The out set of a predecessor is its in set plus the locals // mutated in that block: mutatedOut = mutatedIn | mutated. + VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], + m_analysis.m_mutatedVarsIn[predBlock->bbNum]); VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], m_analysis.m_mutatedVars[predBlock->bbNum]); } @@ -482,6 +483,8 @@ class DefaultValueAnalysis // entry or mutated anywhere within the try region. for (BasicBlock* tryBlock = firstTryBlock; tryBlock != lastTryBlock->Next(); tryBlock = tryBlock->Next()) { + VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], + m_analysis.m_mutatedVarsIn[tryBlock->bbNum]); VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], m_analysis.m_mutatedVars[tryBlock->bbNum]); } @@ -489,13 +492,6 @@ class DefaultValueAnalysis bool EndMerge(BasicBlock* block) { - if (block == m_compiler->fgFirstBB) - { - // Parameters and OSR locals are considered mutated at method - // entry. - VarSetOps::UnionD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum], m_analysis.m_mutatedAtEntry); - } - return !VarSetOps::Equal(m_compiler, m_preMergeIn, m_analysis.m_mutatedVarsIn[block->bbNum]); } }; @@ -708,7 +704,7 @@ void DefaultValueAnalysis::ComputeInterBlockDefaultValues() if (varDsc->lvIsParam || varDsc->lvIsOSRLocal) { - VarSetOps::AddElemD(m_compiler, m_mutatedAtEntry, varDsc->lvVarIndex); + VarSetOps::AddElemD(m_compiler, m_mutatedVarsIn[m_compiler->fgFirstBB->bbNum], varDsc->lvVarIndex); } } From 713dcc18924a270eed27d707b9a85007418bf2eb Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 19 Feb 2026 15:22:04 +0100 Subject: [PATCH 21/22] Fix build --- src/coreclr/jit/async.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 7fc2e02fa4b4a7..05684e0c0f9561 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -501,7 +501,6 @@ class DefaultValueAnalysis : m_compiler(compiler) , m_mutatedVars(nullptr) , m_mutatedVarsIn(nullptr) - , m_mutatedAtEntry(VarSetOps::MakeEmpty(compiler)) { } From ff8794aed0b6d07468c4dca7dd85826858f3ec77 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 19 Feb 2026 15:46:28 +0100 Subject: [PATCH 22/22] Fix --- src/coreclr/jit/async.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 05684e0c0f9561..9f79a026b6c5ab 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -460,10 +460,6 @@ class DefaultValueAnalysis { // Save the current in set for change detection later. VarSetOps::Assign(m_compiler, m_preMergeIn, m_analysis.m_mutatedVarsIn[block->bbNum]); - - // Optimistically assume no locals have been mutated; Merge will - // grow via union. - VarSetOps::ClearD(m_compiler, m_analysis.m_mutatedVarsIn[block->bbNum]); } void Merge(BasicBlock* block, BasicBlock* predBlock, unsigned dupCount) @@ -695,7 +691,7 @@ void DefaultValueAnalysis::ComputeInterBlockDefaultValues() VarSetOps::AssignNoCopy(m_compiler, m_mutatedVarsIn[i], VarSetOps::MakeEmpty(m_compiler)); } - // Parameters and OSR locals do not have default values at method entry. + // Parameters and OSR locals are considered mutated at method entry. for (unsigned i = 0; i < m_compiler->lvaTrackedCount; i++) { unsigned lclNum = m_compiler->lvaTrackedToVarNum[i];