Implement UnknownSizeFrame for locals with unknown size - #125491

Merged
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame
May 7, 2026
Merged

Implement UnknownSizeFrame for locals with unknown size#125491
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame

Conversation

@snickolls-arm

Copy link
Copy Markdown
Contributor

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are allocated to this space when lvaIsUnknownSizeLocal is true for the variable.

The frame is implemented on ARM64 as two homogenenous blocks containing either TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals in the block. Updates codegen for SVE memory transfer instructions to accept indices in multiples of the vector length (or VL / 8 for masks) instead of deriving them from the size of the local.

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are
allocated to this space when lvaIsUnknownSizeLocal is true for the variable.
The frame is implemented on ARM64 as two homogenenous blocks containing either
TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals
in the block. Updates codegen for SVE memory transfer instructions to accept
indices in multiples of the vector length (or VL / 8 for masks) instead of
deriving them from the size of the local.
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 12, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 12, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@jakobbotsch
jakobbotsch self-requested a review March 16, 2026 09:46
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Comment threadsrc/coreclr/jit/codegenarmarch.cpp
Comment threadsrc/coreclr/jit/compiler.hpp Outdated
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lsrabuild.cpp Outdated
Comment threadsrc/coreclr/jit/regset.cpp
* Add function header
* Create UnknownSizeFrame::GetAddressingOffset and revert changes to lvaFrameAddress
* Use rsSetRegsModified and remove kill ref position
@jakobbotsch

Copy link
Copy Markdown
Member

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

@jakobbotsch
jakobbotsch self-requested a review April 14, 2026 09:39
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

No problem, thanks for this. I suppose we see fewer locals on frame at higher optimization level so the impact isn't as strong?

UnknownSizeFrame
Adds lvaIsAllocatedOnUnknownSizeFrame with a stronger criteria for
what locals should or shouldn't be allocated in the unknown size frame.
Namely promoted struct fields that are address exposed should not be
allocated there, because the layout of the structure in memory needs to
be preserved.
Comment threadsrc/coreclr/jit/lclvars.cpp Outdated
…on the"
This reverts commit 0a448e16be8a929311243e3172a8b0c8f7793969.
These locals need to be treated specially as they allocated to a different part
of the frame. Adds some assertions to the original accessors to prevent use with
variable sized locals.
//
unsigned varNum = lclNum;

// Variable-sized locals reside in a different part of the stack frame.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This brings up OSR support for this kind of stack frame, which I hadn't yet run into. I suppose it's not possible to just skip over these kinds of variables. I would have to either disable OSR for the method, or add some support to allow for copying over the extra frame space as well?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OSR with unknown frame size runs into problems. Particularly how do you address locals from the tier0 frame? You will approximately have a frame that looks like:

Tier0 locals
Tier0 vectors/masks
OSR locals <- FP points here
OSR vectors/masks <- SP points here

It is not possible to address the tier0 locals via FP without some non-fixed offset encoding. You will need another frame pointer to do that.
I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

We may need to support this eventually, OSR is important for our PGO and tiering strategy. cc @AndyAyersMS, he has thought about this in relation to localloc a lot.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

You will need to predict whether we are going to end up with unknown size locals

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since OSR is only supported for JIT cases we can probably just compute the size of the "Tier0 vectors/masks" part based on the actual vector/mask size during JIT time. It seems like the most straightforward approach.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To answer your questions:

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

Note that you cannot move this data around after its initial allocation since there can be pointers pointing to it.
I do not think having reserved x19 makes it much easier. The codegen side of two frame pointers is probably not that hard, but the rest of the VM is not set up to handle the possibility of having to address locals via separate frame pointers.

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Yes, we currently only support switching very early:

if (compHasBackwardJump && (reason == nullptr) && (JitConfig.TC_OnStackReplacement() > 0))
{
bool canEscapeViaOSR = compCanHavePatchpoints(&reason);
#ifdef DEBUG
if (canEscapeViaOSR)
{
// Optionally disable OSR by method hash. This will force any
// method that might otherwise get trapped in Tier0 to be optimized.
//
static ConfigMethodRange JitEnableOsrRange;
JitEnableOsrRange.EnsureInit(JitConfig.JitEnableOsrRange());
constunsigned hash = impInlineRoot()->info.compMethodHash();
if (!JitEnableOsrRange.Contains(hash))
{
canEscapeViaOSR = false;
reason = "OSR disabled by JitEnableOsrRange";
}
}
#endif
if (canEscapeViaOSR)
{
JITDUMP("\nOSR enabled for this method\n");
if (compHasBackwardJump && !compTailPrefixSeen &&
opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR_IF_LOOPS) && opts.IsTier0())
{
assert((info.compFlags & CORINFO_FLG_DISABLE_TIER0_FOR_LOOPS) == 0);
opts.jitFlags->Set(JitFlags::JIT_FLAG_BBINSTR);
JITDUMP("\nEnabling instrumentation for this method so OSR'd version will have a profile.\n");
}
}
else
{
JITDUMP("\nOSR disabled for this method: %s\n", reason);
assert(reason != nullptr);
}
}
if (reason != nullptr)
{
fgSwitchToOptimized(reason);
}

We haven't even imported the IR at this point, we have only done basic setup of the basic blocks. As part of that we do look at the IL though, but I am not sure how feasible it would be to predict whether we are going to end up with unknown size locals at this point. Perhaps a strategy where we reimported once we saw one and then switched to optimize code would work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can determine the size of the 'unknown' size frame when jitting which should allow us to solve OSR in future. It should be as simple as reading the size of Vector<T> from the EE and multiplying it by the number of vectors in the frame, as the EE executes the instruction rdvl already and patches the method table with the result.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

I think so.

Eventually we will need to solve the "GC pointers at VL-offset dependent locations in the stack frame" problem.
I wonder if we can solve this problem and the fact that escape analysis wants something similar at the same time. IIRC you discussed the possibility of a separate dynamic stack with @davidwrighton before, maybe it would be the way to go here since we could allocate the unknown size frame there.

That would potentially also make the OSR case a bit more natural since now all fixed size locals are next to each other in the OSR method.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

An idea I've had for this is to add a noway_assert and have some sort of state on the compiler instance that contains suggested retry flags. Then the catch block in jitNativeCode can pull a new set of flags from the retry parameters and restart compilation at tier1.

If this sounds right then it could be a way to start. It would be better to support the frame in OSR, but if this system could be useful more widely then I could give it a try.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't spend time trying to create workarounds. Instead we should just include the size of the "unknown" part in the patchpoint info (maybe it can even be put directly in TotalFrameSize() and no additional fields will be needed).

@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

I'm not seeing any test failures, other than cancellations at the moment. Are these likely to be caused by the patch, or are they unrelated?

Comment threadsrc/coreclr/jit/codegenarmarch.cpp Outdated
Comment on lines +4872 to +4875
if (m_compiler->compUsesUnknownSizeFrame)
{
genUnknownSizeFrame();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is too early in the prolog. At this point we are still emitting prolog unwind info. I would not expect that we will want to emit any unwind for this adjustment.
This adjustment should happen after unwindEndProlog in genFnProlog.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Yes, I think I should take a similar approach to genPoisonFrame and iterate over this frame in genCodeForBlock. Possibly best to cover both in one patch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you open a tracking issue (or modify an existing one, if there is one) to make sure we don't lose track of these TODOs?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added bullets here for spill temps and initialization: #120599

@jakobbotsch

Copy link
Copy Markdown
Member

There are some x64 throughput regressions which is unexpected to me.
Can you try to fast-path lvaIsUnknownSizeLocal outside arm64 and see if they disappear?

* Move genUnknownSizeFrame call after generating unwind info
* Remove lvaIsUnknownSizeLocal body from builds from other architectures
@jakobbotsch
jakobbotsch self-requested a review May 7, 2026 11:15
Comment threadsrc/coreclr/jit/codegenarmarch.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@jakobbotsch
jakobbotsch merged commit 270715b into dotnet:mainMay 7, 2026
136 of 138 checks passed
jakobbotsch pushed a commit that referenced this pull request May 12, 2026
…28037)
Code introduced in #125491 asserts that addressing is always frame
pointer based, but it is possible for it to be stack pointer based.
Remove the assertion and select the correct register for codegen.
Fixes#127955
@a74nha74nh mentioned this pull request Jun 24, 2026
20 tasks
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 28, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIarm-sveWork related to arm64 SVE/SVE2 supportcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@snickolls-arm@jakobbotsch@AndyAyersMS@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Implement UnknownSizeFrame for locals with unknown size - #125491

Merged
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame
May 7, 2026
Merged

Implement UnknownSizeFrame for locals with unknown size#125491
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame

Conversation

@snickolls-arm

Copy link
Copy Markdown
Contributor

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are allocated to this space when lvaIsUnknownSizeLocal is true for the variable.

The frame is implemented on ARM64 as two homogenenous blocks containing either TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals in the block. Updates codegen for SVE memory transfer instructions to accept indices in multiples of the vector length (or VL / 8 for masks) instead of deriving them from the size of the local.

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are
allocated to this space when lvaIsUnknownSizeLocal is true for the variable.
The frame is implemented on ARM64 as two homogenenous blocks containing either
TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals
in the block. Updates codegen for SVE memory transfer instructions to accept
indices in multiples of the vector length (or VL / 8 for masks) instead of
deriving them from the size of the local.
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 12, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 12, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@jakobbotsch
jakobbotsch self-requested a review March 16, 2026 09:46
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Comment threadsrc/coreclr/jit/codegenarmarch.cpp
Comment threadsrc/coreclr/jit/compiler.hpp Outdated
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lsrabuild.cpp Outdated
Comment threadsrc/coreclr/jit/regset.cpp
* Add function header
* Create UnknownSizeFrame::GetAddressingOffset and revert changes to lvaFrameAddress
* Use rsSetRegsModified and remove kill ref position
@jakobbotsch

Copy link
Copy Markdown
Member

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

@jakobbotsch
jakobbotsch self-requested a review April 14, 2026 09:39
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

No problem, thanks for this. I suppose we see fewer locals on frame at higher optimization level so the impact isn't as strong?

UnknownSizeFrame
Adds lvaIsAllocatedOnUnknownSizeFrame with a stronger criteria for
what locals should or shouldn't be allocated in the unknown size frame.
Namely promoted struct fields that are address exposed should not be
allocated there, because the layout of the structure in memory needs to
be preserved.
Comment threadsrc/coreclr/jit/lclvars.cpp Outdated
…on the"
This reverts commit 0a448e16be8a929311243e3172a8b0c8f7793969.
These locals need to be treated specially as they allocated to a different part
of the frame. Adds some assertions to the original accessors to prevent use with
variable sized locals.
//
unsigned varNum = lclNum;

// Variable-sized locals reside in a different part of the stack frame.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This brings up OSR support for this kind of stack frame, which I hadn't yet run into. I suppose it's not possible to just skip over these kinds of variables. I would have to either disable OSR for the method, or add some support to allow for copying over the extra frame space as well?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OSR with unknown frame size runs into problems. Particularly how do you address locals from the tier0 frame? You will approximately have a frame that looks like:

Tier0 locals
Tier0 vectors/masks
OSR locals <- FP points here
OSR vectors/masks <- SP points here

It is not possible to address the tier0 locals via FP without some non-fixed offset encoding. You will need another frame pointer to do that.
I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

We may need to support this eventually, OSR is important for our PGO and tiering strategy. cc @AndyAyersMS, he has thought about this in relation to localloc a lot.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

You will need to predict whether we are going to end up with unknown size locals

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since OSR is only supported for JIT cases we can probably just compute the size of the "Tier0 vectors/masks" part based on the actual vector/mask size during JIT time. It seems like the most straightforward approach.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To answer your questions:

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

Note that you cannot move this data around after its initial allocation since there can be pointers pointing to it.
I do not think having reserved x19 makes it much easier. The codegen side of two frame pointers is probably not that hard, but the rest of the VM is not set up to handle the possibility of having to address locals via separate frame pointers.

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Yes, we currently only support switching very early:

if (compHasBackwardJump && (reason == nullptr) && (JitConfig.TC_OnStackReplacement() > 0))
{
bool canEscapeViaOSR = compCanHavePatchpoints(&reason);
#ifdef DEBUG
if (canEscapeViaOSR)
{
// Optionally disable OSR by method hash. This will force any
// method that might otherwise get trapped in Tier0 to be optimized.
//
static ConfigMethodRange JitEnableOsrRange;
JitEnableOsrRange.EnsureInit(JitConfig.JitEnableOsrRange());
constunsigned hash = impInlineRoot()->info.compMethodHash();
if (!JitEnableOsrRange.Contains(hash))
{
canEscapeViaOSR = false;
reason = "OSR disabled by JitEnableOsrRange";
}
}
#endif
if (canEscapeViaOSR)
{
JITDUMP("\nOSR enabled for this method\n");
if (compHasBackwardJump && !compTailPrefixSeen &&
opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR_IF_LOOPS) && opts.IsTier0())
{
assert((info.compFlags & CORINFO_FLG_DISABLE_TIER0_FOR_LOOPS) == 0);
opts.jitFlags->Set(JitFlags::JIT_FLAG_BBINSTR);
JITDUMP("\nEnabling instrumentation for this method so OSR'd version will have a profile.\n");
}
}
else
{
JITDUMP("\nOSR disabled for this method: %s\n", reason);
assert(reason != nullptr);
}
}
if (reason != nullptr)
{
fgSwitchToOptimized(reason);
}

We haven't even imported the IR at this point, we have only done basic setup of the basic blocks. As part of that we do look at the IL though, but I am not sure how feasible it would be to predict whether we are going to end up with unknown size locals at this point. Perhaps a strategy where we reimported once we saw one and then switched to optimize code would work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can determine the size of the 'unknown' size frame when jitting which should allow us to solve OSR in future. It should be as simple as reading the size of Vector<T> from the EE and multiplying it by the number of vectors in the frame, as the EE executes the instruction rdvl already and patches the method table with the result.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

I think so.

Eventually we will need to solve the "GC pointers at VL-offset dependent locations in the stack frame" problem.
I wonder if we can solve this problem and the fact that escape analysis wants something similar at the same time. IIRC you discussed the possibility of a separate dynamic stack with @davidwrighton before, maybe it would be the way to go here since we could allocate the unknown size frame there.

That would potentially also make the OSR case a bit more natural since now all fixed size locals are next to each other in the OSR method.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

An idea I've had for this is to add a noway_assert and have some sort of state on the compiler instance that contains suggested retry flags. Then the catch block in jitNativeCode can pull a new set of flags from the retry parameters and restart compilation at tier1.

If this sounds right then it could be a way to start. It would be better to support the frame in OSR, but if this system could be useful more widely then I could give it a try.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't spend time trying to create workarounds. Instead we should just include the size of the "unknown" part in the patchpoint info (maybe it can even be put directly in TotalFrameSize() and no additional fields will be needed).

@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

I'm not seeing any test failures, other than cancellations at the moment. Are these likely to be caused by the patch, or are they unrelated?

Comment threadsrc/coreclr/jit/codegenarmarch.cpp Outdated
Comment on lines +4872 to +4875
if (m_compiler->compUsesUnknownSizeFrame)
{
genUnknownSizeFrame();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is too early in the prolog. At this point we are still emitting prolog unwind info. I would not expect that we will want to emit any unwind for this adjustment.
This adjustment should happen after unwindEndProlog in genFnProlog.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Yes, I think I should take a similar approach to genPoisonFrame and iterate over this frame in genCodeForBlock. Possibly best to cover both in one patch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you open a tracking issue (or modify an existing one, if there is one) to make sure we don't lose track of these TODOs?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added bullets here for spill temps and initialization: #120599

@jakobbotsch

Copy link
Copy Markdown
Member

There are some x64 throughput regressions which is unexpected to me.
Can you try to fast-path lvaIsUnknownSizeLocal outside arm64 and see if they disappear?

* Move genUnknownSizeFrame call after generating unwind info
* Remove lvaIsUnknownSizeLocal body from builds from other architectures
@jakobbotsch
jakobbotsch self-requested a review May 7, 2026 11:15
Comment threadsrc/coreclr/jit/codegenarmarch.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@jakobbotsch
jakobbotsch merged commit 270715b into dotnet:mainMay 7, 2026
136 of 138 checks passed
jakobbotsch pushed a commit that referenced this pull request May 12, 2026
…28037)
Code introduced in #125491 asserts that addressing is always frame
pointer based, but it is possible for it to be stack pointer based.
Remove the assertion and select the correct register for codegen.
Fixes#127955
@a74nha74nh mentioned this pull request Jun 24, 2026
20 tasks
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 28, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIarm-sveWork related to arm64 SVE/SVE2 supportcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@snickolls-arm@jakobbotsch@AndyAyersMS@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Implement UnknownSizeFrame for locals with unknown size - #125491

Merged
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame
May 7, 2026
Merged

Implement UnknownSizeFrame for locals with unknown size#125491
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame

Conversation

@snickolls-arm

Copy link
Copy Markdown
Contributor

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are allocated to this space when lvaIsUnknownSizeLocal is true for the variable.

The frame is implemented on ARM64 as two homogenenous blocks containing either TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals in the block. Updates codegen for SVE memory transfer instructions to accept indices in multiples of the vector length (or VL / 8 for masks) instead of deriving them from the size of the local.

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are
allocated to this space when lvaIsUnknownSizeLocal is true for the variable.
The frame is implemented on ARM64 as two homogenenous blocks containing either
TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals
in the block. Updates codegen for SVE memory transfer instructions to accept
indices in multiples of the vector length (or VL / 8 for masks) instead of
deriving them from the size of the local.
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 12, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 12, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@jakobbotsch
jakobbotsch self-requested a review March 16, 2026 09:46
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Comment threadsrc/coreclr/jit/codegenarmarch.cpp
Comment threadsrc/coreclr/jit/compiler.hpp Outdated
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lsrabuild.cpp Outdated
Comment threadsrc/coreclr/jit/regset.cpp
* Add function header
* Create UnknownSizeFrame::GetAddressingOffset and revert changes to lvaFrameAddress
* Use rsSetRegsModified and remove kill ref position
@jakobbotsch

Copy link
Copy Markdown
Member

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

@jakobbotsch
jakobbotsch self-requested a review April 14, 2026 09:39
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

No problem, thanks for this. I suppose we see fewer locals on frame at higher optimization level so the impact isn't as strong?

UnknownSizeFrame
Adds lvaIsAllocatedOnUnknownSizeFrame with a stronger criteria for
what locals should or shouldn't be allocated in the unknown size frame.
Namely promoted struct fields that are address exposed should not be
allocated there, because the layout of the structure in memory needs to
be preserved.
Comment threadsrc/coreclr/jit/lclvars.cpp Outdated
…on the"
This reverts commit 0a448e16be8a929311243e3172a8b0c8f7793969.
These locals need to be treated specially as they allocated to a different part
of the frame. Adds some assertions to the original accessors to prevent use with
variable sized locals.
//
unsigned varNum = lclNum;

// Variable-sized locals reside in a different part of the stack frame.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This brings up OSR support for this kind of stack frame, which I hadn't yet run into. I suppose it's not possible to just skip over these kinds of variables. I would have to either disable OSR for the method, or add some support to allow for copying over the extra frame space as well?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OSR with unknown frame size runs into problems. Particularly how do you address locals from the tier0 frame? You will approximately have a frame that looks like:

Tier0 locals
Tier0 vectors/masks
OSR locals <- FP points here
OSR vectors/masks <- SP points here

It is not possible to address the tier0 locals via FP without some non-fixed offset encoding. You will need another frame pointer to do that.
I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

We may need to support this eventually, OSR is important for our PGO and tiering strategy. cc @AndyAyersMS, he has thought about this in relation to localloc a lot.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

You will need to predict whether we are going to end up with unknown size locals

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since OSR is only supported for JIT cases we can probably just compute the size of the "Tier0 vectors/masks" part based on the actual vector/mask size during JIT time. It seems like the most straightforward approach.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To answer your questions:

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

Note that you cannot move this data around after its initial allocation since there can be pointers pointing to it.
I do not think having reserved x19 makes it much easier. The codegen side of two frame pointers is probably not that hard, but the rest of the VM is not set up to handle the possibility of having to address locals via separate frame pointers.

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Yes, we currently only support switching very early:

if (compHasBackwardJump && (reason == nullptr) && (JitConfig.TC_OnStackReplacement() > 0))
{
bool canEscapeViaOSR = compCanHavePatchpoints(&reason);
#ifdef DEBUG
if (canEscapeViaOSR)
{
// Optionally disable OSR by method hash. This will force any
// method that might otherwise get trapped in Tier0 to be optimized.
//
static ConfigMethodRange JitEnableOsrRange;
JitEnableOsrRange.EnsureInit(JitConfig.JitEnableOsrRange());
constunsigned hash = impInlineRoot()->info.compMethodHash();
if (!JitEnableOsrRange.Contains(hash))
{
canEscapeViaOSR = false;
reason = "OSR disabled by JitEnableOsrRange";
}
}
#endif
if (canEscapeViaOSR)
{
JITDUMP("\nOSR enabled for this method\n");
if (compHasBackwardJump && !compTailPrefixSeen &&
opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR_IF_LOOPS) && opts.IsTier0())
{
assert((info.compFlags & CORINFO_FLG_DISABLE_TIER0_FOR_LOOPS) == 0);
opts.jitFlags->Set(JitFlags::JIT_FLAG_BBINSTR);
JITDUMP("\nEnabling instrumentation for this method so OSR'd version will have a profile.\n");
}
}
else
{
JITDUMP("\nOSR disabled for this method: %s\n", reason);
assert(reason != nullptr);
}
}
if (reason != nullptr)
{
fgSwitchToOptimized(reason);
}

We haven't even imported the IR at this point, we have only done basic setup of the basic blocks. As part of that we do look at the IL though, but I am not sure how feasible it would be to predict whether we are going to end up with unknown size locals at this point. Perhaps a strategy where we reimported once we saw one and then switched to optimize code would work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can determine the size of the 'unknown' size frame when jitting which should allow us to solve OSR in future. It should be as simple as reading the size of Vector<T> from the EE and multiplying it by the number of vectors in the frame, as the EE executes the instruction rdvl already and patches the method table with the result.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

I think so.

Eventually we will need to solve the "GC pointers at VL-offset dependent locations in the stack frame" problem.
I wonder if we can solve this problem and the fact that escape analysis wants something similar at the same time. IIRC you discussed the possibility of a separate dynamic stack with @davidwrighton before, maybe it would be the way to go here since we could allocate the unknown size frame there.

That would potentially also make the OSR case a bit more natural since now all fixed size locals are next to each other in the OSR method.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

An idea I've had for this is to add a noway_assert and have some sort of state on the compiler instance that contains suggested retry flags. Then the catch block in jitNativeCode can pull a new set of flags from the retry parameters and restart compilation at tier1.

If this sounds right then it could be a way to start. It would be better to support the frame in OSR, but if this system could be useful more widely then I could give it a try.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't spend time trying to create workarounds. Instead we should just include the size of the "unknown" part in the patchpoint info (maybe it can even be put directly in TotalFrameSize() and no additional fields will be needed).

@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

I'm not seeing any test failures, other than cancellations at the moment. Are these likely to be caused by the patch, or are they unrelated?

Comment threadsrc/coreclr/jit/codegenarmarch.cpp Outdated
Comment on lines +4872 to +4875
if (m_compiler->compUsesUnknownSizeFrame)
{
genUnknownSizeFrame();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is too early in the prolog. At this point we are still emitting prolog unwind info. I would not expect that we will want to emit any unwind for this adjustment.
This adjustment should happen after unwindEndProlog in genFnProlog.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Yes, I think I should take a similar approach to genPoisonFrame and iterate over this frame in genCodeForBlock. Possibly best to cover both in one patch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you open a tracking issue (or modify an existing one, if there is one) to make sure we don't lose track of these TODOs?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added bullets here for spill temps and initialization: #120599

@jakobbotsch

Copy link
Copy Markdown
Member

There are some x64 throughput regressions which is unexpected to me.
Can you try to fast-path lvaIsUnknownSizeLocal outside arm64 and see if they disappear?

* Move genUnknownSizeFrame call after generating unwind info
* Remove lvaIsUnknownSizeLocal body from builds from other architectures
@jakobbotsch
jakobbotsch self-requested a review May 7, 2026 11:15
Comment threadsrc/coreclr/jit/codegenarmarch.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@jakobbotsch
jakobbotsch merged commit 270715b into dotnet:mainMay 7, 2026
136 of 138 checks passed
jakobbotsch pushed a commit that referenced this pull request May 12, 2026
…28037)
Code introduced in #125491 asserts that addressing is always frame
pointer based, but it is possible for it to be stack pointer based.
Remove the assertion and select the correct register for codegen.
Fixes#127955
@a74nha74nh mentioned this pull request Jun 24, 2026
20 tasks
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 28, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIarm-sveWork related to arm64 SVE/SVE2 supportcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@snickolls-arm@jakobbotsch@AndyAyersMS@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Implement UnknownSizeFrame for locals with unknown size - #125491

Merged
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame
May 7, 2026
Merged

Implement UnknownSizeFrame for locals with unknown size#125491
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame

Conversation

@snickolls-arm

Copy link
Copy Markdown
Contributor

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are allocated to this space when lvaIsUnknownSizeLocal is true for the variable.

The frame is implemented on ARM64 as two homogenenous blocks containing either TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals in the block. Updates codegen for SVE memory transfer instructions to accept indices in multiples of the vector length (or VL / 8 for masks) instead of deriving them from the size of the local.

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are
allocated to this space when lvaIsUnknownSizeLocal is true for the variable.
The frame is implemented on ARM64 as two homogenenous blocks containing either
TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals
in the block. Updates codegen for SVE memory transfer instructions to accept
indices in multiples of the vector length (or VL / 8 for masks) instead of
deriving them from the size of the local.
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 12, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 12, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@jakobbotsch
jakobbotsch self-requested a review March 16, 2026 09:46
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Comment threadsrc/coreclr/jit/codegenarmarch.cpp
Comment threadsrc/coreclr/jit/compiler.hpp Outdated
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lsrabuild.cpp Outdated
Comment threadsrc/coreclr/jit/regset.cpp
* Add function header
* Create UnknownSizeFrame::GetAddressingOffset and revert changes to lvaFrameAddress
* Use rsSetRegsModified and remove kill ref position
@jakobbotsch

Copy link
Copy Markdown
Member

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

@jakobbotsch
jakobbotsch self-requested a review April 14, 2026 09:39
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

No problem, thanks for this. I suppose we see fewer locals on frame at higher optimization level so the impact isn't as strong?

UnknownSizeFrame
Adds lvaIsAllocatedOnUnknownSizeFrame with a stronger criteria for
what locals should or shouldn't be allocated in the unknown size frame.
Namely promoted struct fields that are address exposed should not be
allocated there, because the layout of the structure in memory needs to
be preserved.
Comment threadsrc/coreclr/jit/lclvars.cpp Outdated
…on the"
This reverts commit 0a448e16be8a929311243e3172a8b0c8f7793969.
These locals need to be treated specially as they allocated to a different part
of the frame. Adds some assertions to the original accessors to prevent use with
variable sized locals.
//
unsigned varNum = lclNum;

// Variable-sized locals reside in a different part of the stack frame.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This brings up OSR support for this kind of stack frame, which I hadn't yet run into. I suppose it's not possible to just skip over these kinds of variables. I would have to either disable OSR for the method, or add some support to allow for copying over the extra frame space as well?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OSR with unknown frame size runs into problems. Particularly how do you address locals from the tier0 frame? You will approximately have a frame that looks like:

Tier0 locals
Tier0 vectors/masks
OSR locals <- FP points here
OSR vectors/masks <- SP points here

It is not possible to address the tier0 locals via FP without some non-fixed offset encoding. You will need another frame pointer to do that.
I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

We may need to support this eventually, OSR is important for our PGO and tiering strategy. cc @AndyAyersMS, he has thought about this in relation to localloc a lot.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

You will need to predict whether we are going to end up with unknown size locals

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since OSR is only supported for JIT cases we can probably just compute the size of the "Tier0 vectors/masks" part based on the actual vector/mask size during JIT time. It seems like the most straightforward approach.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To answer your questions:

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

Note that you cannot move this data around after its initial allocation since there can be pointers pointing to it.
I do not think having reserved x19 makes it much easier. The codegen side of two frame pointers is probably not that hard, but the rest of the VM is not set up to handle the possibility of having to address locals via separate frame pointers.

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Yes, we currently only support switching very early:

if (compHasBackwardJump && (reason == nullptr) && (JitConfig.TC_OnStackReplacement() > 0))
{
bool canEscapeViaOSR = compCanHavePatchpoints(&reason);
#ifdef DEBUG
if (canEscapeViaOSR)
{
// Optionally disable OSR by method hash. This will force any
// method that might otherwise get trapped in Tier0 to be optimized.
//
static ConfigMethodRange JitEnableOsrRange;
JitEnableOsrRange.EnsureInit(JitConfig.JitEnableOsrRange());
constunsigned hash = impInlineRoot()->info.compMethodHash();
if (!JitEnableOsrRange.Contains(hash))
{
canEscapeViaOSR = false;
reason = "OSR disabled by JitEnableOsrRange";
}
}
#endif
if (canEscapeViaOSR)
{
JITDUMP("\nOSR enabled for this method\n");
if (compHasBackwardJump && !compTailPrefixSeen &&
opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR_IF_LOOPS) && opts.IsTier0())
{
assert((info.compFlags & CORINFO_FLG_DISABLE_TIER0_FOR_LOOPS) == 0);
opts.jitFlags->Set(JitFlags::JIT_FLAG_BBINSTR);
JITDUMP("\nEnabling instrumentation for this method so OSR'd version will have a profile.\n");
}
}
else
{
JITDUMP("\nOSR disabled for this method: %s\n", reason);
assert(reason != nullptr);
}
}
if (reason != nullptr)
{
fgSwitchToOptimized(reason);
}

We haven't even imported the IR at this point, we have only done basic setup of the basic blocks. As part of that we do look at the IL though, but I am not sure how feasible it would be to predict whether we are going to end up with unknown size locals at this point. Perhaps a strategy where we reimported once we saw one and then switched to optimize code would work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can determine the size of the 'unknown' size frame when jitting which should allow us to solve OSR in future. It should be as simple as reading the size of Vector<T> from the EE and multiplying it by the number of vectors in the frame, as the EE executes the instruction rdvl already and patches the method table with the result.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

I think so.

Eventually we will need to solve the "GC pointers at VL-offset dependent locations in the stack frame" problem.
I wonder if we can solve this problem and the fact that escape analysis wants something similar at the same time. IIRC you discussed the possibility of a separate dynamic stack with @davidwrighton before, maybe it would be the way to go here since we could allocate the unknown size frame there.

That would potentially also make the OSR case a bit more natural since now all fixed size locals are next to each other in the OSR method.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

An idea I've had for this is to add a noway_assert and have some sort of state on the compiler instance that contains suggested retry flags. Then the catch block in jitNativeCode can pull a new set of flags from the retry parameters and restart compilation at tier1.

If this sounds right then it could be a way to start. It would be better to support the frame in OSR, but if this system could be useful more widely then I could give it a try.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't spend time trying to create workarounds. Instead we should just include the size of the "unknown" part in the patchpoint info (maybe it can even be put directly in TotalFrameSize() and no additional fields will be needed).

@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

I'm not seeing any test failures, other than cancellations at the moment. Are these likely to be caused by the patch, or are they unrelated?

Comment threadsrc/coreclr/jit/codegenarmarch.cpp Outdated
Comment on lines +4872 to +4875
if (m_compiler->compUsesUnknownSizeFrame)
{
genUnknownSizeFrame();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is too early in the prolog. At this point we are still emitting prolog unwind info. I would not expect that we will want to emit any unwind for this adjustment.
This adjustment should happen after unwindEndProlog in genFnProlog.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Yes, I think I should take a similar approach to genPoisonFrame and iterate over this frame in genCodeForBlock. Possibly best to cover both in one patch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you open a tracking issue (or modify an existing one, if there is one) to make sure we don't lose track of these TODOs?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added bullets here for spill temps and initialization: #120599

@jakobbotsch

Copy link
Copy Markdown
Member

There are some x64 throughput regressions which is unexpected to me.
Can you try to fast-path lvaIsUnknownSizeLocal outside arm64 and see if they disappear?

* Move genUnknownSizeFrame call after generating unwind info
* Remove lvaIsUnknownSizeLocal body from builds from other architectures
@jakobbotsch
jakobbotsch self-requested a review May 7, 2026 11:15
Comment threadsrc/coreclr/jit/codegenarmarch.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@jakobbotsch
jakobbotsch merged commit 270715b into dotnet:mainMay 7, 2026
136 of 138 checks passed
jakobbotsch pushed a commit that referenced this pull request May 12, 2026
…28037)
Code introduced in #125491 asserts that addressing is always frame
pointer based, but it is possible for it to be stack pointer based.
Remove the assertion and select the correct register for codegen.
Fixes#127955
@a74nha74nh mentioned this pull request Jun 24, 2026
20 tasks
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 28, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIarm-sveWork related to arm64 SVE/SVE2 supportcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@snickolls-arm@jakobbotsch@AndyAyersMS@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Implement UnknownSizeFrame for locals with unknown size - #125491

Merged
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame
May 7, 2026
Merged

Implement UnknownSizeFrame for locals with unknown size#125491
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame

Conversation

@snickolls-arm

Copy link
Copy Markdown
Contributor

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are allocated to this space when lvaIsUnknownSizeLocal is true for the variable.

The frame is implemented on ARM64 as two homogenenous blocks containing either TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals in the block. Updates codegen for SVE memory transfer instructions to accept indices in multiples of the vector length (or VL / 8 for masks) instead of deriving them from the size of the local.

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are
allocated to this space when lvaIsUnknownSizeLocal is true for the variable.
The frame is implemented on ARM64 as two homogenenous blocks containing either
TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals
in the block. Updates codegen for SVE memory transfer instructions to accept
indices in multiples of the vector length (or VL / 8 for masks) instead of
deriving them from the size of the local.
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 12, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 12, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@jakobbotsch
jakobbotsch self-requested a review March 16, 2026 09:46
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Comment threadsrc/coreclr/jit/codegenarmarch.cpp
Comment threadsrc/coreclr/jit/compiler.hpp Outdated
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lsrabuild.cpp Outdated
Comment threadsrc/coreclr/jit/regset.cpp
* Add function header
* Create UnknownSizeFrame::GetAddressingOffset and revert changes to lvaFrameAddress
* Use rsSetRegsModified and remove kill ref position
@jakobbotsch

Copy link
Copy Markdown
Member

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

@jakobbotsch
jakobbotsch self-requested a review April 14, 2026 09:39
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

No problem, thanks for this. I suppose we see fewer locals on frame at higher optimization level so the impact isn't as strong?

UnknownSizeFrame
Adds lvaIsAllocatedOnUnknownSizeFrame with a stronger criteria for
what locals should or shouldn't be allocated in the unknown size frame.
Namely promoted struct fields that are address exposed should not be
allocated there, because the layout of the structure in memory needs to
be preserved.
Comment threadsrc/coreclr/jit/lclvars.cpp Outdated
…on the"
This reverts commit 0a448e16be8a929311243e3172a8b0c8f7793969.
These locals need to be treated specially as they allocated to a different part
of the frame. Adds some assertions to the original accessors to prevent use with
variable sized locals.
//
unsigned varNum = lclNum;

// Variable-sized locals reside in a different part of the stack frame.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This brings up OSR support for this kind of stack frame, which I hadn't yet run into. I suppose it's not possible to just skip over these kinds of variables. I would have to either disable OSR for the method, or add some support to allow for copying over the extra frame space as well?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OSR with unknown frame size runs into problems. Particularly how do you address locals from the tier0 frame? You will approximately have a frame that looks like:

Tier0 locals
Tier0 vectors/masks
OSR locals <- FP points here
OSR vectors/masks <- SP points here

It is not possible to address the tier0 locals via FP without some non-fixed offset encoding. You will need another frame pointer to do that.
I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

We may need to support this eventually, OSR is important for our PGO and tiering strategy. cc @AndyAyersMS, he has thought about this in relation to localloc a lot.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

You will need to predict whether we are going to end up with unknown size locals

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since OSR is only supported for JIT cases we can probably just compute the size of the "Tier0 vectors/masks" part based on the actual vector/mask size during JIT time. It seems like the most straightforward approach.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To answer your questions:

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

Note that you cannot move this data around after its initial allocation since there can be pointers pointing to it.
I do not think having reserved x19 makes it much easier. The codegen side of two frame pointers is probably not that hard, but the rest of the VM is not set up to handle the possibility of having to address locals via separate frame pointers.

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Yes, we currently only support switching very early:

if (compHasBackwardJump && (reason == nullptr) && (JitConfig.TC_OnStackReplacement() > 0))
{
bool canEscapeViaOSR = compCanHavePatchpoints(&reason);
#ifdef DEBUG
if (canEscapeViaOSR)
{
// Optionally disable OSR by method hash. This will force any
// method that might otherwise get trapped in Tier0 to be optimized.
//
static ConfigMethodRange JitEnableOsrRange;
JitEnableOsrRange.EnsureInit(JitConfig.JitEnableOsrRange());
constunsigned hash = impInlineRoot()->info.compMethodHash();
if (!JitEnableOsrRange.Contains(hash))
{
canEscapeViaOSR = false;
reason = "OSR disabled by JitEnableOsrRange";
}
}
#endif
if (canEscapeViaOSR)
{
JITDUMP("\nOSR enabled for this method\n");
if (compHasBackwardJump && !compTailPrefixSeen &&
opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR_IF_LOOPS) && opts.IsTier0())
{
assert((info.compFlags & CORINFO_FLG_DISABLE_TIER0_FOR_LOOPS) == 0);
opts.jitFlags->Set(JitFlags::JIT_FLAG_BBINSTR);
JITDUMP("\nEnabling instrumentation for this method so OSR'd version will have a profile.\n");
}
}
else
{
JITDUMP("\nOSR disabled for this method: %s\n", reason);
assert(reason != nullptr);
}
}
if (reason != nullptr)
{
fgSwitchToOptimized(reason);
}

We haven't even imported the IR at this point, we have only done basic setup of the basic blocks. As part of that we do look at the IL though, but I am not sure how feasible it would be to predict whether we are going to end up with unknown size locals at this point. Perhaps a strategy where we reimported once we saw one and then switched to optimize code would work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can determine the size of the 'unknown' size frame when jitting which should allow us to solve OSR in future. It should be as simple as reading the size of Vector<T> from the EE and multiplying it by the number of vectors in the frame, as the EE executes the instruction rdvl already and patches the method table with the result.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

I think so.

Eventually we will need to solve the "GC pointers at VL-offset dependent locations in the stack frame" problem.
I wonder if we can solve this problem and the fact that escape analysis wants something similar at the same time. IIRC you discussed the possibility of a separate dynamic stack with @davidwrighton before, maybe it would be the way to go here since we could allocate the unknown size frame there.

That would potentially also make the OSR case a bit more natural since now all fixed size locals are next to each other in the OSR method.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

An idea I've had for this is to add a noway_assert and have some sort of state on the compiler instance that contains suggested retry flags. Then the catch block in jitNativeCode can pull a new set of flags from the retry parameters and restart compilation at tier1.

If this sounds right then it could be a way to start. It would be better to support the frame in OSR, but if this system could be useful more widely then I could give it a try.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't spend time trying to create workarounds. Instead we should just include the size of the "unknown" part in the patchpoint info (maybe it can even be put directly in TotalFrameSize() and no additional fields will be needed).

@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

I'm not seeing any test failures, other than cancellations at the moment. Are these likely to be caused by the patch, or are they unrelated?

Comment threadsrc/coreclr/jit/codegenarmarch.cpp Outdated
Comment on lines +4872 to +4875
if (m_compiler->compUsesUnknownSizeFrame)
{
genUnknownSizeFrame();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is too early in the prolog. At this point we are still emitting prolog unwind info. I would not expect that we will want to emit any unwind for this adjustment.
This adjustment should happen after unwindEndProlog in genFnProlog.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Yes, I think I should take a similar approach to genPoisonFrame and iterate over this frame in genCodeForBlock. Possibly best to cover both in one patch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you open a tracking issue (or modify an existing one, if there is one) to make sure we don't lose track of these TODOs?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added bullets here for spill temps and initialization: #120599

@jakobbotsch

Copy link
Copy Markdown
Member

There are some x64 throughput regressions which is unexpected to me.
Can you try to fast-path lvaIsUnknownSizeLocal outside arm64 and see if they disappear?

* Move genUnknownSizeFrame call after generating unwind info
* Remove lvaIsUnknownSizeLocal body from builds from other architectures
@jakobbotsch
jakobbotsch self-requested a review May 7, 2026 11:15
Comment threadsrc/coreclr/jit/codegenarmarch.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@jakobbotsch
jakobbotsch merged commit 270715b into dotnet:mainMay 7, 2026
136 of 138 checks passed
jakobbotsch pushed a commit that referenced this pull request May 12, 2026
…28037)
Code introduced in #125491 asserts that addressing is always frame
pointer based, but it is possible for it to be stack pointer based.
Remove the assertion and select the correct register for codegen.
Fixes#127955
@a74nha74nh mentioned this pull request Jun 24, 2026
20 tasks
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 28, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIarm-sveWork related to arm64 SVE/SVE2 supportcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@snickolls-arm@jakobbotsch@AndyAyersMS@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Implement UnknownSizeFrame for locals with unknown size - #125491

Merged
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame
May 7, 2026
Merged

Implement UnknownSizeFrame for locals with unknown size#125491
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame

Conversation

@snickolls-arm

Copy link
Copy Markdown
Contributor

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are allocated to this space when lvaIsUnknownSizeLocal is true for the variable.

The frame is implemented on ARM64 as two homogenenous blocks containing either TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals in the block. Updates codegen for SVE memory transfer instructions to accept indices in multiples of the vector length (or VL / 8 for masks) instead of deriving them from the size of the local.

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are
allocated to this space when lvaIsUnknownSizeLocal is true for the variable.
The frame is implemented on ARM64 as two homogenenous blocks containing either
TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals
in the block. Updates codegen for SVE memory transfer instructions to accept
indices in multiples of the vector length (or VL / 8 for masks) instead of
deriving them from the size of the local.
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 12, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 12, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@jakobbotsch
jakobbotsch self-requested a review March 16, 2026 09:46
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Comment threadsrc/coreclr/jit/codegenarmarch.cpp
Comment threadsrc/coreclr/jit/compiler.hpp Outdated
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lsrabuild.cpp Outdated
Comment threadsrc/coreclr/jit/regset.cpp
* Add function header
* Create UnknownSizeFrame::GetAddressingOffset and revert changes to lvaFrameAddress
* Use rsSetRegsModified and remove kill ref position
@jakobbotsch

Copy link
Copy Markdown
Member

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

@jakobbotsch
jakobbotsch self-requested a review April 14, 2026 09:39
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

No problem, thanks for this. I suppose we see fewer locals on frame at higher optimization level so the impact isn't as strong?

UnknownSizeFrame
Adds lvaIsAllocatedOnUnknownSizeFrame with a stronger criteria for
what locals should or shouldn't be allocated in the unknown size frame.
Namely promoted struct fields that are address exposed should not be
allocated there, because the layout of the structure in memory needs to
be preserved.
Comment threadsrc/coreclr/jit/lclvars.cpp Outdated
…on the"
This reverts commit 0a448e16be8a929311243e3172a8b0c8f7793969.
These locals need to be treated specially as they allocated to a different part
of the frame. Adds some assertions to the original accessors to prevent use with
variable sized locals.
//
unsigned varNum = lclNum;

// Variable-sized locals reside in a different part of the stack frame.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This brings up OSR support for this kind of stack frame, which I hadn't yet run into. I suppose it's not possible to just skip over these kinds of variables. I would have to either disable OSR for the method, or add some support to allow for copying over the extra frame space as well?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OSR with unknown frame size runs into problems. Particularly how do you address locals from the tier0 frame? You will approximately have a frame that looks like:

Tier0 locals
Tier0 vectors/masks
OSR locals <- FP points here
OSR vectors/masks <- SP points here

It is not possible to address the tier0 locals via FP without some non-fixed offset encoding. You will need another frame pointer to do that.
I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

We may need to support this eventually, OSR is important for our PGO and tiering strategy. cc @AndyAyersMS, he has thought about this in relation to localloc a lot.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

You will need to predict whether we are going to end up with unknown size locals

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since OSR is only supported for JIT cases we can probably just compute the size of the "Tier0 vectors/masks" part based on the actual vector/mask size during JIT time. It seems like the most straightforward approach.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To answer your questions:

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

Note that you cannot move this data around after its initial allocation since there can be pointers pointing to it.
I do not think having reserved x19 makes it much easier. The codegen side of two frame pointers is probably not that hard, but the rest of the VM is not set up to handle the possibility of having to address locals via separate frame pointers.

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Yes, we currently only support switching very early:

if (compHasBackwardJump && (reason == nullptr) && (JitConfig.TC_OnStackReplacement() > 0))
{
bool canEscapeViaOSR = compCanHavePatchpoints(&reason);
#ifdef DEBUG
if (canEscapeViaOSR)
{
// Optionally disable OSR by method hash. This will force any
// method that might otherwise get trapped in Tier0 to be optimized.
//
static ConfigMethodRange JitEnableOsrRange;
JitEnableOsrRange.EnsureInit(JitConfig.JitEnableOsrRange());
constunsigned hash = impInlineRoot()->info.compMethodHash();
if (!JitEnableOsrRange.Contains(hash))
{
canEscapeViaOSR = false;
reason = "OSR disabled by JitEnableOsrRange";
}
}
#endif
if (canEscapeViaOSR)
{
JITDUMP("\nOSR enabled for this method\n");
if (compHasBackwardJump && !compTailPrefixSeen &&
opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR_IF_LOOPS) && opts.IsTier0())
{
assert((info.compFlags & CORINFO_FLG_DISABLE_TIER0_FOR_LOOPS) == 0);
opts.jitFlags->Set(JitFlags::JIT_FLAG_BBINSTR);
JITDUMP("\nEnabling instrumentation for this method so OSR'd version will have a profile.\n");
}
}
else
{
JITDUMP("\nOSR disabled for this method: %s\n", reason);
assert(reason != nullptr);
}
}
if (reason != nullptr)
{
fgSwitchToOptimized(reason);
}

We haven't even imported the IR at this point, we have only done basic setup of the basic blocks. As part of that we do look at the IL though, but I am not sure how feasible it would be to predict whether we are going to end up with unknown size locals at this point. Perhaps a strategy where we reimported once we saw one and then switched to optimize code would work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can determine the size of the 'unknown' size frame when jitting which should allow us to solve OSR in future. It should be as simple as reading the size of Vector<T> from the EE and multiplying it by the number of vectors in the frame, as the EE executes the instruction rdvl already and patches the method table with the result.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

I think so.

Eventually we will need to solve the "GC pointers at VL-offset dependent locations in the stack frame" problem.
I wonder if we can solve this problem and the fact that escape analysis wants something similar at the same time. IIRC you discussed the possibility of a separate dynamic stack with @davidwrighton before, maybe it would be the way to go here since we could allocate the unknown size frame there.

That would potentially also make the OSR case a bit more natural since now all fixed size locals are next to each other in the OSR method.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

An idea I've had for this is to add a noway_assert and have some sort of state on the compiler instance that contains suggested retry flags. Then the catch block in jitNativeCode can pull a new set of flags from the retry parameters and restart compilation at tier1.

If this sounds right then it could be a way to start. It would be better to support the frame in OSR, but if this system could be useful more widely then I could give it a try.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't spend time trying to create workarounds. Instead we should just include the size of the "unknown" part in the patchpoint info (maybe it can even be put directly in TotalFrameSize() and no additional fields will be needed).

@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

I'm not seeing any test failures, other than cancellations at the moment. Are these likely to be caused by the patch, or are they unrelated?

Comment threadsrc/coreclr/jit/codegenarmarch.cpp Outdated
Comment on lines +4872 to +4875
if (m_compiler->compUsesUnknownSizeFrame)
{
genUnknownSizeFrame();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is too early in the prolog. At this point we are still emitting prolog unwind info. I would not expect that we will want to emit any unwind for this adjustment.
This adjustment should happen after unwindEndProlog in genFnProlog.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Yes, I think I should take a similar approach to genPoisonFrame and iterate over this frame in genCodeForBlock. Possibly best to cover both in one patch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you open a tracking issue (or modify an existing one, if there is one) to make sure we don't lose track of these TODOs?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added bullets here for spill temps and initialization: #120599

@jakobbotsch

Copy link
Copy Markdown
Member

There are some x64 throughput regressions which is unexpected to me.
Can you try to fast-path lvaIsUnknownSizeLocal outside arm64 and see if they disappear?

* Move genUnknownSizeFrame call after generating unwind info
* Remove lvaIsUnknownSizeLocal body from builds from other architectures
@jakobbotsch
jakobbotsch self-requested a review May 7, 2026 11:15
Comment threadsrc/coreclr/jit/codegenarmarch.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@jakobbotsch
jakobbotsch merged commit 270715b into dotnet:mainMay 7, 2026
136 of 138 checks passed
jakobbotsch pushed a commit that referenced this pull request May 12, 2026
…28037)
Code introduced in #125491 asserts that addressing is always frame
pointer based, but it is possible for it to be stack pointer based.
Remove the assertion and select the correct register for codegen.
Fixes#127955
@a74nha74nh mentioned this pull request Jun 24, 2026
20 tasks
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 28, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIarm-sveWork related to arm64 SVE/SVE2 supportcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@snickolls-arm@jakobbotsch@AndyAyersMS@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Implement UnknownSizeFrame for locals with unknown size - #125491

Merged
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame
May 7, 2026
Merged

Implement UnknownSizeFrame for locals with unknown size#125491
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame

Conversation

@snickolls-arm

Copy link
Copy Markdown
Contributor

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are allocated to this space when lvaIsUnknownSizeLocal is true for the variable.

The frame is implemented on ARM64 as two homogenenous blocks containing either TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals in the block. Updates codegen for SVE memory transfer instructions to accept indices in multiples of the vector length (or VL / 8 for masks) instead of deriving them from the size of the local.

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are
allocated to this space when lvaIsUnknownSizeLocal is true for the variable.
The frame is implemented on ARM64 as two homogenenous blocks containing either
TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals
in the block. Updates codegen for SVE memory transfer instructions to accept
indices in multiples of the vector length (or VL / 8 for masks) instead of
deriving them from the size of the local.
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 12, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 12, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@jakobbotsch
jakobbotsch self-requested a review March 16, 2026 09:46
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Comment threadsrc/coreclr/jit/codegenarmarch.cpp
Comment threadsrc/coreclr/jit/compiler.hpp Outdated
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lsrabuild.cpp Outdated
Comment threadsrc/coreclr/jit/regset.cpp
* Add function header
* Create UnknownSizeFrame::GetAddressingOffset and revert changes to lvaFrameAddress
* Use rsSetRegsModified and remove kill ref position
@jakobbotsch

Copy link
Copy Markdown
Member

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

@jakobbotsch
jakobbotsch self-requested a review April 14, 2026 09:39
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

No problem, thanks for this. I suppose we see fewer locals on frame at higher optimization level so the impact isn't as strong?

UnknownSizeFrame
Adds lvaIsAllocatedOnUnknownSizeFrame with a stronger criteria for
what locals should or shouldn't be allocated in the unknown size frame.
Namely promoted struct fields that are address exposed should not be
allocated there, because the layout of the structure in memory needs to
be preserved.
Comment threadsrc/coreclr/jit/lclvars.cpp Outdated
…on the"
This reverts commit 0a448e16be8a929311243e3172a8b0c8f7793969.
These locals need to be treated specially as they allocated to a different part
of the frame. Adds some assertions to the original accessors to prevent use with
variable sized locals.
//
unsigned varNum = lclNum;

// Variable-sized locals reside in a different part of the stack frame.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This brings up OSR support for this kind of stack frame, which I hadn't yet run into. I suppose it's not possible to just skip over these kinds of variables. I would have to either disable OSR for the method, or add some support to allow for copying over the extra frame space as well?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OSR with unknown frame size runs into problems. Particularly how do you address locals from the tier0 frame? You will approximately have a frame that looks like:

Tier0 locals
Tier0 vectors/masks
OSR locals <- FP points here
OSR vectors/masks <- SP points here

It is not possible to address the tier0 locals via FP without some non-fixed offset encoding. You will need another frame pointer to do that.
I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

We may need to support this eventually, OSR is important for our PGO and tiering strategy. cc @AndyAyersMS, he has thought about this in relation to localloc a lot.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

You will need to predict whether we are going to end up with unknown size locals

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since OSR is only supported for JIT cases we can probably just compute the size of the "Tier0 vectors/masks" part based on the actual vector/mask size during JIT time. It seems like the most straightforward approach.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To answer your questions:

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

Note that you cannot move this data around after its initial allocation since there can be pointers pointing to it.
I do not think having reserved x19 makes it much easier. The codegen side of two frame pointers is probably not that hard, but the rest of the VM is not set up to handle the possibility of having to address locals via separate frame pointers.

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Yes, we currently only support switching very early:

if (compHasBackwardJump && (reason == nullptr) && (JitConfig.TC_OnStackReplacement() > 0))
{
bool canEscapeViaOSR = compCanHavePatchpoints(&reason);
#ifdef DEBUG
if (canEscapeViaOSR)
{
// Optionally disable OSR by method hash. This will force any
// method that might otherwise get trapped in Tier0 to be optimized.
//
static ConfigMethodRange JitEnableOsrRange;
JitEnableOsrRange.EnsureInit(JitConfig.JitEnableOsrRange());
constunsigned hash = impInlineRoot()->info.compMethodHash();
if (!JitEnableOsrRange.Contains(hash))
{
canEscapeViaOSR = false;
reason = "OSR disabled by JitEnableOsrRange";
}
}
#endif
if (canEscapeViaOSR)
{
JITDUMP("\nOSR enabled for this method\n");
if (compHasBackwardJump && !compTailPrefixSeen &&
opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR_IF_LOOPS) && opts.IsTier0())
{
assert((info.compFlags & CORINFO_FLG_DISABLE_TIER0_FOR_LOOPS) == 0);
opts.jitFlags->Set(JitFlags::JIT_FLAG_BBINSTR);
JITDUMP("\nEnabling instrumentation for this method so OSR'd version will have a profile.\n");
}
}
else
{
JITDUMP("\nOSR disabled for this method: %s\n", reason);
assert(reason != nullptr);
}
}
if (reason != nullptr)
{
fgSwitchToOptimized(reason);
}

We haven't even imported the IR at this point, we have only done basic setup of the basic blocks. As part of that we do look at the IL though, but I am not sure how feasible it would be to predict whether we are going to end up with unknown size locals at this point. Perhaps a strategy where we reimported once we saw one and then switched to optimize code would work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can determine the size of the 'unknown' size frame when jitting which should allow us to solve OSR in future. It should be as simple as reading the size of Vector<T> from the EE and multiplying it by the number of vectors in the frame, as the EE executes the instruction rdvl already and patches the method table with the result.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

I think so.

Eventually we will need to solve the "GC pointers at VL-offset dependent locations in the stack frame" problem.
I wonder if we can solve this problem and the fact that escape analysis wants something similar at the same time. IIRC you discussed the possibility of a separate dynamic stack with @davidwrighton before, maybe it would be the way to go here since we could allocate the unknown size frame there.

That would potentially also make the OSR case a bit more natural since now all fixed size locals are next to each other in the OSR method.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

An idea I've had for this is to add a noway_assert and have some sort of state on the compiler instance that contains suggested retry flags. Then the catch block in jitNativeCode can pull a new set of flags from the retry parameters and restart compilation at tier1.

If this sounds right then it could be a way to start. It would be better to support the frame in OSR, but if this system could be useful more widely then I could give it a try.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't spend time trying to create workarounds. Instead we should just include the size of the "unknown" part in the patchpoint info (maybe it can even be put directly in TotalFrameSize() and no additional fields will be needed).

@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

I'm not seeing any test failures, other than cancellations at the moment. Are these likely to be caused by the patch, or are they unrelated?

Comment threadsrc/coreclr/jit/codegenarmarch.cpp Outdated
Comment on lines +4872 to +4875
if (m_compiler->compUsesUnknownSizeFrame)
{
genUnknownSizeFrame();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is too early in the prolog. At this point we are still emitting prolog unwind info. I would not expect that we will want to emit any unwind for this adjustment.
This adjustment should happen after unwindEndProlog in genFnProlog.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Yes, I think I should take a similar approach to genPoisonFrame and iterate over this frame in genCodeForBlock. Possibly best to cover both in one patch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you open a tracking issue (or modify an existing one, if there is one) to make sure we don't lose track of these TODOs?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added bullets here for spill temps and initialization: #120599

@jakobbotsch

Copy link
Copy Markdown
Member

There are some x64 throughput regressions which is unexpected to me.
Can you try to fast-path lvaIsUnknownSizeLocal outside arm64 and see if they disappear?

* Move genUnknownSizeFrame call after generating unwind info
* Remove lvaIsUnknownSizeLocal body from builds from other architectures
@jakobbotsch
jakobbotsch self-requested a review May 7, 2026 11:15
Comment threadsrc/coreclr/jit/codegenarmarch.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@jakobbotsch
jakobbotsch merged commit 270715b into dotnet:mainMay 7, 2026
136 of 138 checks passed
jakobbotsch pushed a commit that referenced this pull request May 12, 2026
…28037)
Code introduced in #125491 asserts that addressing is always frame
pointer based, but it is possible for it to be stack pointer based.
Remove the assertion and select the correct register for codegen.
Fixes#127955
@a74nha74nh mentioned this pull request Jun 24, 2026
20 tasks
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 28, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIarm-sveWork related to arm64 SVE/SVE2 supportcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@snickolls-arm@jakobbotsch@AndyAyersMS@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Implement UnknownSizeFrame for locals with unknown size - #125491

Merged
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame
May 7, 2026
Merged

Implement UnknownSizeFrame for locals with unknown size#125491
jakobbotsch merged 13 commits into
dotnet:mainfrom
snickolls-arm:stack-frame

Conversation

@snickolls-arm

Copy link
Copy Markdown
Contributor

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are allocated to this space when lvaIsUnknownSizeLocal is true for the variable.

The frame is implemented on ARM64 as two homogenenous blocks containing either TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals in the block. Updates codegen for SVE memory transfer instructions to accept indices in multiples of the vector length (or VL / 8 for masks) instead of deriving them from the size of the local.

Implements a simple bump allocator for TYP_SIMD and TYP_MASK. Locals are
allocated to this space when lvaIsUnknownSizeLocal is true for the variable.
The frame is implemented on ARM64 as two homogenenous blocks containing either
TYP_SIMD or TYP_MASK locals. The x19 register is reserved for addressing locals
in the block. Updates codegen for SVE memory transfer instructions to accept
indices in multiples of the vector length (or VL / 8 for masks) instead of
deriving them from the size of the local.
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 12, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 12, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@jakobbotsch
jakobbotsch self-requested a review March 16, 2026 09:46
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Comment threadsrc/coreclr/jit/codegenarmarch.cpp
Comment threadsrc/coreclr/jit/compiler.hpp Outdated
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lclvars.cpp
Comment threadsrc/coreclr/jit/lsrabuild.cpp Outdated
Comment threadsrc/coreclr/jit/regset.cpp
* Add function header
* Create UnknownSizeFrame::GetAddressingOffset and revert changes to lvaFrameAddress
* Use rsSetRegsModified and remove kill ref position
@jakobbotsch

Copy link
Copy Markdown
Member

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

@jakobbotsch
jakobbotsch self-requested a review April 14, 2026 09:39
@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

Looking at the throughput differences, the performance of lvaIsUnknownSizeLocal is probably not very good, maybe adding another property bit to LclVarDsc could help with this.

Can you post the detailed throughput analysis with per-function information?

Just checking which analysis you mean here? As the only detailed output I can find from tpdiff is in CSV form. Is there tooling available for processing this?

Sorry, I got my contributors confused. We have some tooling that can break throughput regressions/improvements down by JIT function, contributed by @SingleAccretion. However, it is x64-host only (based on Intel PIN).

I collected the data on benchmarks.run_pgo and it looks like this:

Base:86438410243, Diff:86528734455, +0.1045%33418435:+20.56%:36.23%:+0.0387%:public: void __cdecl Compiler::lvaAssignFrameOffsets(enumCompiler::FrameLayoutState) 15698295:+15.34%:17.02%:+0.0182%:protected: void __cdecl CodeGen::genFnProlog(void) 13341778:+8.18%:14.46%:+0.0154%:public: void __cdecl emitter::emitIns_R_S(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 11303530:+4.61%:12.25%:+0.0131%:public: void __cdecl Compiler::lvaAssignVirtualFrameOffsetsToLocals(void) 9485383:+11.28%:10.28%:+0.0110%:public: void __cdecl emitter::emitIns_S_R(enuminstruction, enumemitAttr, enum_regNumber_enum, int, int) 5717661:+7.00%:6.20%:+0.0066%:protected: void __cdecl CodeGen::genCheckUseBlockInit(void) 471445:+4.61%:0.51%:+0.0005%:protected: void __cdecl CodeGen::genFinalizeFrame(void) 441974:+0.28%:0.48%:+0.0005%:public: __cdecl Compiler::Compiler(classArenaAllocatorT<struct JitMemKindTraits> *, struct CORINFO_METHOD_STRUCT_*, classICorJitInfo*, struct CORINFO_METHOD_INFO*, struct InlineInfo*) 377156:+2.79%:0.41%:+0.0004%:protected: void __cdecl CodeGen::genPushCalleeSavedRegisters(enum_regNumber_enum, bool *) 282867:+4.91%:0.31%:+0.0003%:private: void __cdecl LinearScan::setFrameType(void) 246151:+4.35%:0.27%:+0.0003%:protected: void __cdecl CodeGen::genZeroInitFrame(int, int, enum_regNumber_enum, bool *) 92508:+0.04%:0.10%:+0.0001%:public: static void __cdecl BitSetOps<unsigned __int64 *, 1, class Compiler *, class TrackedVarBitSetTraits>::LivenessD(classCompiler*, unsigned __int64 *&, unsigned __int64 *const, unsigned __int64 *const, unsigned __int64 *const)
-96660:-0.09%:0.10%:-0.0001%:protected: void __cdecl JitExpandArray<unsigned char>::InitializeRange(unsigned int, unsigned int) -660023:-100.00%:0.72%:-0.0008%:public: void __cdecl Compiler::funSetCurrentFunc(unsigned int) 

These regressions are correspondingly larger in tier0 code where it matters more, but I think we can live with it and if we really care address it in a follow-up.

I pushed a merge to resolve the merge conflict.

No problem, thanks for this. I suppose we see fewer locals on frame at higher optimization level so the impact isn't as strong?

UnknownSizeFrame
Adds lvaIsAllocatedOnUnknownSizeFrame with a stronger criteria for
what locals should or shouldn't be allocated in the unknown size frame.
Namely promoted struct fields that are address exposed should not be
allocated there, because the layout of the structure in memory needs to
be preserved.
Comment threadsrc/coreclr/jit/lclvars.cpp Outdated
…on the"
This reverts commit 0a448e16be8a929311243e3172a8b0c8f7793969.
These locals need to be treated specially as they allocated to a different part
of the frame. Adds some assertions to the original accessors to prevent use with
variable sized locals.
//
unsigned varNum = lclNum;

// Variable-sized locals reside in a different part of the stack frame.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This brings up OSR support for this kind of stack frame, which I hadn't yet run into. I suppose it's not possible to just skip over these kinds of variables. I would have to either disable OSR for the method, or add some support to allow for copying over the extra frame space as well?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OSR with unknown frame size runs into problems. Particularly how do you address locals from the tier0 frame? You will approximately have a frame that looks like:

Tier0 locals
Tier0 vectors/masks
OSR locals <- FP points here
OSR vectors/masks <- SP points here

It is not possible to address the tier0 locals via FP without some non-fixed offset encoding. You will need another frame pointer to do that.
I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

We may need to support this eventually, OSR is important for our PGO and tiering strategy. cc @AndyAyersMS, he has thought about this in relation to localloc a lot.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

You will need to predict whether we are going to end up with unknown size locals

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since OSR is only supported for JIT cases we can probably just compute the size of the "Tier0 vectors/masks" part based on the actual vector/mask size during JIT time. It seems like the most straightforward approach.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To answer your questions:

I have reserved x19 for addressing the vectors/masks, will this make it easier to support OSR in future? So long as the compiler knows to copy the data and update x19 accordingly? The entire space is some N*VL in size from x19 --> sp.

Note that you cannot move this data around after its initial allocation since there can be pointers pointing to it.
I do not think having reserved x19 makes it much easier. The codegen side of two frame pointers is probably not that hard, but the rest of the VM is not set up to handle the possibility of having to address locals via separate frame pointers.

This sounds like it needs another pass over the IL, as the earliest we would know TYP_SIMD/TYP_MASK is used is on import of code. I am assuming this is too late to decide on whether OSR is possible?

Yes, we currently only support switching very early:

if (compHasBackwardJump && (reason == nullptr) && (JitConfig.TC_OnStackReplacement() > 0))
{
bool canEscapeViaOSR = compCanHavePatchpoints(&reason);
#ifdef DEBUG
if (canEscapeViaOSR)
{
// Optionally disable OSR by method hash. This will force any
// method that might otherwise get trapped in Tier0 to be optimized.
//
static ConfigMethodRange JitEnableOsrRange;
JitEnableOsrRange.EnsureInit(JitConfig.JitEnableOsrRange());
constunsigned hash = impInlineRoot()->info.compMethodHash();
if (!JitEnableOsrRange.Contains(hash))
{
canEscapeViaOSR = false;
reason = "OSR disabled by JitEnableOsrRange";
}
}
#endif
if (canEscapeViaOSR)
{
JITDUMP("\nOSR enabled for this method\n");
if (compHasBackwardJump && !compTailPrefixSeen &&
opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR_IF_LOOPS) && opts.IsTier0())
{
assert((info.compFlags & CORINFO_FLG_DISABLE_TIER0_FOR_LOOPS) == 0);
opts.jitFlags->Set(JitFlags::JIT_FLAG_BBINSTR);
JITDUMP("\nEnabling instrumentation for this method so OSR'd version will have a profile.\n");
}
}
else
{
JITDUMP("\nOSR disabled for this method: %s\n", reason);
assert(reason != nullptr);
}
}
if (reason != nullptr)
{
fgSwitchToOptimized(reason);
}

We haven't even imported the IR at this point, we have only done basic setup of the basic blocks. As part of that we do look at the IL though, but I am not sure how feasible it would be to predict whether we are going to end up with unknown size locals at this point. Perhaps a strategy where we reimported once we saw one and then switched to optimize code would work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can determine the size of the 'unknown' size frame when jitting which should allow us to solve OSR in future. It should be as simple as reading the size of Vector<T> from the EE and multiplying it by the number of vectors in the frame, as the EE executes the instruction rdvl already and patches the method table with the result.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to recap: when jitting we'll never have unknown sized frames from SVE, and OSR is only needed when jitting, so there is no problem to solve?

I think so.

Eventually we will need to solve the "GC pointers at VL-offset dependent locations in the stack frame" problem.
I wonder if we can solve this problem and the fact that escape analysis wants something similar at the same time. IIRC you discussed the possibility of a separate dynamic stack with @davidwrighton before, maybe it would be the way to go here since we could allocate the unknown size frame there.

That would potentially also make the OSR case a bit more natural since now all fixed size locals are next to each other in the OSR method.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is reasonable to disable OSR for now in these functions (meaning that they will be tier1 compiled immediately). You will need to predict whether we are going to end up with unknown size locals, which may not be trivial.

An idea I've had for this is to add a noway_assert and have some sort of state on the compiler instance that contains suggested retry flags. Then the catch block in jitNativeCode can pull a new set of flags from the retry parameters and restart compilation at tier1.

If this sounds right then it could be a way to start. It would be better to support the frame in OSR, but if this system could be useful more widely then I could give it a try.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't spend time trying to create workarounds. Instead we should just include the size of the "unknown" part in the patchpoint info (maybe it can even be put directly in TotalFrameSize() and no additional fields will be needed).

@snickolls-arm

Copy link
Copy Markdown
ContributorAuthor

I'm not seeing any test failures, other than cancellations at the moment. Are these likely to be caused by the patch, or are they unrelated?

Comment threadsrc/coreclr/jit/codegenarmarch.cpp Outdated
Comment on lines +4872 to +4875
if (m_compiler->compUsesUnknownSizeFrame)
{
genUnknownSizeFrame();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is too early in the prolog. At this point we are still emitting prolog unwind info. I would not expect that we will want to emit any unwind for this adjustment.
This adjustment should happen after unwindEndProlog in genFnProlog.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR does not seem to handle zeroing of these locals. Do you expect to do that in a follow-up?

Yes, I think I should take a similar approach to genPoisonFrame and iterate over this frame in genCodeForBlock. Possibly best to cover both in one patch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you open a tracking issue (or modify an existing one, if there is one) to make sure we don't lose track of these TODOs?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added bullets here for spill temps and initialization: #120599

@jakobbotsch

Copy link
Copy Markdown
Member

There are some x64 throughput regressions which is unexpected to me.
Can you try to fast-path lvaIsUnknownSizeLocal outside arm64 and see if they disappear?

* Move genUnknownSizeFrame call after generating unwind info
* Remove lvaIsUnknownSizeLocal body from builds from other architectures
@jakobbotsch
jakobbotsch self-requested a review May 7, 2026 11:15
Comment threadsrc/coreclr/jit/codegenarmarch.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@jakobbotsch
jakobbotsch merged commit 270715b into dotnet:mainMay 7, 2026
136 of 138 checks passed
jakobbotsch pushed a commit that referenced this pull request May 12, 2026
…28037)
Code introduced in #125491 asserts that addressing is always frame
pointer based, but it is possible for it to be stack pointer based.
Remove the assertion and select the correct register for codegen.
Fixes#127955
@a74nha74nh mentioned this pull request Jun 24, 2026
20 tasks
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 28, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIarm-sveWork related to arm64 SVE/SVE2 supportcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@snickolls-arm@jakobbotsch@AndyAyersMS@jkotas