Repository navigation
On Stack Replacement Next Steps #33658
Copy link
Copy link
Open
Labels
area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMICLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
Milestone
Description
Possible next steps now that #32969 is merged, in rough order of priority.
- implement a regularly scheduled test run that enables OSR for pri1 tests on x64 windows and x64 Linux (both default OSR and with "OSR stress") (done, Add pipeline setup for jit offbydefault extra testing #33709)
- fix issues with OSR creating side entries into try regions (JIT\opt\OSR\tailrecursetry\tailrecursetry.cmd fails with COMPlus_FastTailCalls=0 #35687, also seen during Initial support for partially jitted methods #34522). PR: JIT: better support for patchpoints in try regions #59784
- fix failures in jit-experimental runs with OSR (OSR Test Failures #43534, OSR method may require kept-alive this in cases where base method didn't #47532, JIT: fix osr gc info this reporting #51057)
- fix interaction of OSR and PGO (JIT: resolve issues with OSR and PGO #47942 fixed via PR Update how OSR and PGO interact #61453; PR JIT: handle interaction of OSR, PGO, and tail calls #62263)
- run tests with OSR and some GC and JIT stress modes (PR Enable QJFL and OSR by default for x64 #61934)
- investigate assert seen in the weekly OSR tests
Assert failure(PID 7028 [0x00001b74], Thread: 7084 [0x1bac]): ppInfo->m_osrMethodCode == NULL-- likely the logic guarding against threads racing to build the patchpoint method needs adjusting (likely fixed by A couple of small OSR fixes #38165) - partial method compilation (eg don't initially jit exceptional paths, prototyped in Initial support for partially jitted methods #34522; PR Initial support for partially jitted methods #60791)
- support OSR from synchronous methods (JIT: support OSR for synchronized methods #61712)
- don't do OSR in methods that are asking for debug codegen (fine as is, since such methods aren't eligible for tiering)
- look at how OSR performs for powershell startup ([which uses QJFL=1]) (see note 4 below)
- implement variant of QJFL that bails out of methods that can't be OSR'd (see note 1 below) (JIT: refactor to allow OSR to switch to optimized #61851)
- run ASP.NET perf tests and verify startup improvements are as expected / no steady state losses
-
look at how debuggers handle OSR frames; if the double-RBP restore is too confusing, think about relying on the original method's RBP (will still need split save areas). On further thought, it seems like (for x64) we can pass the tier0 method caller's RBP to the osr method and just have one unwind restore. This is what I'm doing for arm64 and it seems to be working out ok.(new plan is to revise arm64 to conform with how x64 will work, see below) - arm64 platform support (see note 7 below). (PR: OSR support for Arm64 #62831)
- sort out complications with OSR and ALTJIT (see note 6 below). Addressed Arm64 PR.
- Exclude CI tests of crossgen2 from using OSR as it is run via a 6.0 runtime and so hits some old OSR bugs (Suppress OSR for crossgen2 execution #62968).
- Fix issues uncovered by OSR stress (JIT: yet another OSR stress mode #62980, enable osr stress tests for libraries and add fixes #64116)
- fix problem with broken epilog unwind on x64 (see note) JIT: revise approach for x64 OSR epilogs #65609
- re-enable struct promotion JIT: OSR jitstress fixes; enable struct promotion #65903
- Track OSR impact on Techempower (data now available on the PGO tab) add osr to pgo modes aspnet/Benchmarks#1726
- Run debugger tests
- Ensure BenchmarkDotNet optimizes key auto-generated methods to avoid holding on to GC references (see notes), also Ensure WorkloadActionUnroll and similar are optimized if possible BenchmarkDotNet#1934 and Fix optimization of action methods for coreclr BenchmarkDotNet#1935
- Update dotnet/performance with new BDN version and verify everything's ok. Some of the tests I expected to improve did, others did not -- see note below.
- Look more closely at interaction of OSR and loop optimizer (see notes below) JIT: add OSR patchpoint strategy, inhibit tail duplication #66208
- Update perfview / traceevent to properly parse new jit type (data there already) Add support for OSR jitted code microsoft/perfview#1584
- Update DAC/SOS to properly understand new native code versions for methods (see notes) Add OSR to DacpTieredVersionData diagnostics#2928 and Add OSR to DacpTieredVersionData #66507
- Fix ARM64 issue with large OSR funclet frames on arm64 Assertion failed 'genFuncletInfo.fiSpDelta1 >= -240' during 'Generate code' #65996 (via JIT: handle large OSR funclet frame initial SP delta for arm64 type 5… #66124)
- Update BDN iteration strategy for long running benchmarks. Revise heuristic for initial jitting. BenchmarkDotNet#1949 and Bump BDN to 0.13.1.1728, to pick up fixes for wasm/aot runs performance#2323
- Fix bad interaction of OSR and the more general loop cloning introduced in Cloning improvements #66257 (see note) Fix loop cloning for OSR #67067
- Fix stress failure OSR stress failure in System.Diagnostics.DiagnosticsSource.Tests #67078 (via JIT: block some struct promotion for OSR #67131)
- run perf test suite with OSR and investigate any regressions versus current default (see notes, more notes). We have temporarily co-opted the regularly scheduled "no pgo" perf lab runs for windows x64 to actually run OSR. Results here. And also enabled autofiling so OSR perf results that differ from the old no pgo perf are reported as regressions/improvements. Example of regressions.
- Investigate test failure Test failure JIT/opt/OSR/tailrecurse/tailrecurse.sh #67215 (likely unrelated, see Test failure JIT/opt/Devirtualization/structreturningstruct/structreturningstruct.sh #66924)
- Fix stress failure OSR failure in System.Text.Json.Tests #67152 (JIT: Use small register types for some enregisterable locals #67274)
- Enable QJFL and OSR by default for x64/arm64
Enable QJFL and OSR by default for x64 #61934Enable QJFL and OSR by default for x64 and arm64 #63642Enable QJFL and OSR by default for x64 and arm64 #65675 - Enable use of sparse edge instrumentation in OSR methods (JIT: resolve issues with OSR and PGO #47942). JIT: Enable edge based profiles for all scenarios #80481
- Import entire method initially and trim unneeded parts once we are done with morph. This fixes lingering issues with computing local exposure: JIT: import entire method for OSR, prune unneeded parts later #83910
- Ensure Tier0-exposed locals are normalize on load in the OSR method: JIT: small OSR locals must be normalize on load #84000
- Run diagnostic tests (blocked; they're not yet updated for the .NET 7 branch)
Issues and fixes after OSR was enabled
- Assertion failed 'codeGen->isFramePointerUsed() || varDsc->GetStackOffset() >= 0' #67488 (fixed by JIT: fix OSR handling for pinned locals #67680)
- OSR variants may not report pinned locals as pinned #67668 (fixed by JIT: Fix OSR local detection for implicit byref promotion #67678)
- Test failure DataContractSerializerTests.DCS_MyPersonSurrogate_Stress #67410 (fixed by JIT: fix interaction of OSR and shadowed parameters #67884)
- Test failure JIT\\opt\\OSR\\largefuncletframe\\largefuncletframe.cmd #68003 (fixed by JIT: fix assert seen in some OSR cases #68048)
- Assertion failed 'isValidGeneralDatasize(size)' during 'Generate code' #68170 (fixed by JIT: fix issue with invalid operand size in OSR prolog on arm64 #68198)
- OSR gets wrong offset for memory arg on arm64 OSX #68194 (fixed by JIT: fix patchpoint offset encoding #68202)
- Test failure: JIT\Regression\JitBlue\GitHub_21990\GitHub_21990\GitHub_21990.cmd #70263 (fixed by JIT: arm64 osr funclet frame probing #70916)
- [libraries-pgo] arm64 failure in System.Tests.DoubleTests.ParsePatterns #71005 (fixed by JIT: fix double reporting of some GC frame slots #71245)
- [libraries-pgo] Issues related to overlapped IO #75828 (fixed by Preserve last error for patchpoint helpers #75922)
- JIT: OSR is not conservative enough about exposing struct locals #83783 (fixed by JIT: import entire method for OSR, prune unneeded parts later #83910)
Performance Regressions
- Regressions due to QJFL and OSR #67594
- The frame times of my toy ray tracer increased from 24ms to 30ms #78127
- Performance of my Maze Generator went from 2.1 seconds to 3.8 after upgrading to .NET 7.0 #78110
- QuickJitForLoops causing regressions #80210
- I accidentaly found performance reggresion using"mandelbrot algorithm" #80757
Other ideas: enhancements or optimizations
- Update arm64 to use the same split callee-save technique we now use on x64, and pass Tier0 FP to the OSR method. This gives arm64 methods standard epilogs.
- Revise Arm64 frame layout to put PSPSym above callee-saves, so that OSR method can share Tier0 PSP, and OSR funclets don't need to pad their frames with the Tier0 frame (see notes starting here) and (more notes). Or, revise the OSR method so it shares the PSP slot with the TIer0 frame (requires split callee-save above). (PSP sym no longer exists: Abolish PSPSym from ABI #114630)
- look into enabling more independent promotion in OSR methods. Right now we use the Tier0 address exposure data and this is very conservative. Also see JIT: block some struct promotion for OSR #67131.
- Possibly addressed by JIT: import entire method for OSR, prune unneeded parts later #83910
- Possibly addressed by JIT: Add a (disabled) prototype for a generalized promotion pass #83388
- support OSR in methods with stackalloc (see note 2 below) and [further notes]
- support OSR for reverse pinvoke methods (see note 3 below)
- support OSR from methods that make explicit tail calls (see note 5 below)
- implement aggressive frame trimming (reduce original method frame to just live extent)
- look into viability of backpatching the patchpoint call with a jump to the OSR method instead
- look into how to support limited Tier0 opts with OSR
- look into emitting more compact patchpoint code sequences
- look into emitting more compact patchpoint info blobs
- think about asynchronous creation of OSR methods
- look into the feasibility of having one OSR method cover all the patchpoints
- look into using the "mutator" tool in jitutils to inject loops into methods that don't have them, so that we can trigger OSR in more cases.
- Note that random patchpoint placement and fast OSR triggers can accomplish something similar without needing to alter tests. There's nothing saying a patchpoint has to be within a loop. JIT: yet another OSR stress mode #62980
- OSR + GS -- perhaps OSR method should have its own cookie (if needed) in addition to the Tier0 cookie, and check them both on exit? Currently we just check the Tier0 cookie, but if the OSR frame holds saved LR/FP we might miss an overrun. Note: not needed if we move arm64 to the new x64 plan, as there's just one save area that gets restored, and it is in the Tier0 frame.
- update runtime strategy to support "slow" OSR method creation, but quick transitions when OSR methods exist
- support for mid-block patchpoints (where IL stack is empty). Among other things, this would let us do "source" patchpoint targeting more often.
- support for patchpoints at non-stack empty IL offsets. Would require custom per-site patchpoint descriptors and more.
- defer altering control flow for OSR until much later. Currently we do it very early and need to protect the original method entry specially in case we want to branch there during morph (see JIT: Improve local assertion prop throughput #94597 (comment)).
cc @dotnet/jit-contrib
category:cq
theme:osr
skill-level:expert
cost:extra-large
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMICLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
Activity
AndyAyersMS commentedon Mar 17, 2020
And if you want to try this out (x64 only):
By default a transition requires ~10,000 executions of a patchpoint. Unlike tiered compilation, transitions are currently synchronous, so there's no lag/delay once this threshold is reached.
To enable "aggressive OSR" -- where an OSR method is created the first time a patchpoint is encountered, also set
To see OSR log messages from the runtime, also add
AndyAyersMS commentedon Sep 16, 2021
Design sketch for better supporting patchpoints within
tryregionsAs noted above and elsewhere (#35687) the jit has a strong assumption that control can only enter a
tryregion by branching to the first block in the try. If there's an OSR patchpoint within the try we currently branch directly from the prolog to the middle of the try, breaking this assumption.The idea here is to add extra control flow to the starts of any try region that encloses the patchpoint, to conditionally transfer control from outside the try to the appropriate point. So all control flow still enters the try at the first block. If we're coming from method entry, we jump to the next enclosing try or the patchpoint as appropriate.
For example, suppose we have the following (
Snare arbitrary blocks of code):The
forwill inspire a patchpoint that is nested within twotryregions. We would transform this as follows:Because of the conditional control flow we'll end up importing some
Snblocks that we currently skip. If it turns out the variousSnare unreachable (which should be common) we should be able to optimize them away.Since we can detect the try-nesting of a patchpoint when we're asked to create an OSR method we won't need extra info passed in the patchpoint records from Tier0. We just apply this transformation automatically if we see the patchpoint is within a try.
Note patchpoints within handers are still problematic, largely because of funclets. So if we see loops in catches or finallies we might still want to have the method bypass Tier0. These should be rare.
AndyAyersMS commentedon Sep 28, 2021
Started working on the try region fix noted just above. Seems like we ought to wait until after the importer has done its "transitive closure" to add the OSR inspired try region "step blocks" -- if we add them before importation then we will make all the code within tries reachable and this may be hard to undo.
If after importation an enclosing try's entry block is unreachable then the step block can unconditionally transfer to the enclosed try entry and/or actual entry point.
Still checking if it's ok for a try entry to have multiple preds or if we actually expect there to be a unique fall through pred.
AndyAyersMS commentedon Sep 28, 2021
Here's an example from the tailrecursetry test case with my current changes main..FixMidTryPatchpoint.
This is adding the new flow pre-importation; as noted above that might not be the best approach overall. Seems like perhaps we should defer this until
fgRemoveEmptyBlocksto avoid wrangling with the importer, and then we can introduce conditional flow only if needed.JIT: better support for patchpoints in try regions
JIT: better support for patchpoints in try regions (#59784)
AndyAyersMS commentedon Nov 12, 2021
If there's an OSR method that has multiple patchpoints and multiple active threads we can trigger an "OSR storm" of sorts
This seems kind of unfortunate, but not sure there's any easy fix.
One could imagine, say, deferring OSR generation for a method if there's already an OSR method being built at any patchpoint in the method, with the hope that other threads eventually hit and can use the one OSR method being created. But there's no guarantee that any of these threads will ever hit other patchpoints.
AndyAyersMS commentedon Nov 15, 2021
(1) Been looking into what would be required to enable QJFL=1 via OSR. There are still some methods that OSR can't handle, and some loops within methods that OSR can't handle.
So the proposal is that with QJFL=1 and OSR=1 (which would become the default on x64, say) and provided the JIT could guarantee escape the Tier0 method via OSR, we'd jit at Tier0. If the JIT can't guarantee this then we instead optimize. So a much larger set of methods would jit at Tier0.
We could reserve QJFL=2 say to mean "always jit at Tier0" regardless, if we wanted such capability.
Bailing out of Tier0+OSR because of the "method" checks this is easy enough as it can use the same pattern we already use for QJFL=0. But for the per-loop failures, it's not as clear how to handle the fallback -- as we need to import to detect the problem.
If these cases are rare enough, it could work like we do for minopts fallback, failing the Tier0 jit request with OSR set; then the driver could request a new jit with OSR not set.
Or we could try and cleanly architect a jit reset that bails from mid-import back to pre-import somehow, but there's a lot of ambient jit state around....
AndyAyersMS commentedon Nov 17, 2021
(2) For OSR + {stack,loc}alloc, we'd need to have the jit and gc info support the idea of having 3 pointers into the frame. One would point at the original method's frame base (to access the OSR state), one at the OSR method's frame base (to access any state it introduces, eg spilled temps), and one to refer to the outgoing args area. The separation between these 3 would vary depending on how much allocation was done by the base method before triggering OSR, and how much is done by the OSR method.
Unfortunately the idea that there are at most two frame pointers (FP and SP) is baked in everywhere, including the GC encoding.
Fixing this is straightforward in principle, but also a fair amount of work. And while there are potentially other uses for three frame pointers (eg using the 3rd to reduce the need for large frame-relative offsets), at least initially this support would be exclusive to OSR, and within OSR exclusive to that subset of methods using localloc. So not faring well in terms of cost/benefit.
So thinking now that the right move is to initially exclude these methods from OSR by detecting this as one of the cases we can't handle, and bailing to optimized code (like we do currently for QJFL=0).
38 remaining items
AndyAyersMS commentedon Mar 18, 2022
Have an interim comparison report showing OSR vs MAIN. The top dozen or so regressions are the
ssl: truetests, these all should be "fixed" by the next BDN update in the performance repo: dotnet/performance#2323Overall this report shows 395 regressions and 591 improvements. The vast majority of these are noise. Drew is going to get me an updated comparison report that takes history into account; when that shows up I'll post an update.
In the meantime looking through what's there for actual regressions,
Benchstone.BenchF.NewtR.Testis slower with OSR. This benchmark runs 4 iterations per, so ends up executing OSR for about 1/3 of its workload iterations. The OSR codegen is similar but the inner loop has a couple of extra loads because of an OSR exposed local.I actually get somewhat inconsistent results locally. And not clear yet why we can't hoist/cse this load. Need to look more closely what's going on.
In the Tier0 method,
x0is address exposed, so the OSR method has to reload it from memory each time.The exact impact of this depends on when the Tier1 method shows up -- in the above you can see it kicked in around iteration 7.
AndyAyersMS commentedon Mar 18, 2022
x0is modified in the inner loop. But it's unaliased over that scope. So presumably could be enregistered and spilled back to its stack home on loop exit. We don't have the technology to handle cases like this (scalar promotion or similar).CSE does not consider these trees despite knowing that the two loads (at least) will return the same value.
presumably this runs into:
runtime/src/coreclr/jit/optcse.cpp
Lines 3576 to 3577 in 798d52b
which seems a bit short sighted? We know (as in this case) we have some invariant/expensive local var loads, why not try CSEing them if we have the room.
Will experiment.
AndyAyersMS commentedon Mar 18, 2022
Seems viable, though simply turning it on (even requiring
cost >= IND_COST_EX) leads to a lot of code churn and a few asserts about incompatible types during CSE.EG for benchmarks (via SPMI)
I recall a while back I was touting something like this as an alternative to EH Write Through but never pushed on it.
Anyways, probably something too radical to land in a timely manner.
AndyAyersMS commentedon Mar 23, 2022
Latest update to BDN has made its way to perf repo and OSR runs. This should fix the
ssl: true"regressions" where OSR consistently fared worse than main.AndyAyersMS commentedon Mar 23, 2022
Seeing a new issue now in general OSR testing, presumably from the interaction of OSR and #66257.
AndyAyersMS commentedon Mar 23, 2022
The jit is trying to clone the outer loop here:
AndyAyersMS commentedon Mar 23, 2022
Suspect the fix is simple? Here we have BB01->bbNext == BB04. So we decide not to create a new block for
h2here:runtime/src/coreclr/jit/loopcloning.cpp
Lines 1903 to 1928 in ea4ebaa
but BB01 does not transfer control to BB04.
BruceForstall commentedon Mar 23, 2022
Doesn't BB01->bbNext == BB02? What is the actual lexical block order?
If you set
COMPlus_JitDumpFgConstrained=1what does it look like?AndyAyersMS commentedon Mar 23, 2022
Yeah, sorry, I was off base. As you noticed in #67067 we get into this code but weren't setting up the right
h2.Constrained view of the flowgraph:
AndyAyersMS commentedon Mar 25, 2022
There's one more libraries issue I want to investigate... on ubuntu x64
AndyAyersMS commentedon Mar 29, 2022
Issue for above #67152 -- fix in PR at #67274.
AndyAyersMS commentedon Mar 30, 2023
#83910 improved a couple of the microbenchmarks, notably
and might also fix some of the reported regressions -- taking a look.
AndyAyersMS commentedon Apr 24, 2023
Seeing some benchmark cases where there are methods with stackalloc + loop that bypass tiering: #84264 (comment) and hence also bypass PGO.
In particular
runtime/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.TryGetProperty.cs
Lines 135 to 150 in f2a55e2
Not sure how common this is but something to keep an eye on. Supporting stackalloc in its full generality with OSR would be hard, because we potentially would need to track 3 addressable segments of the stack, but it's not impossible.
It might be easier to revise the BCL so this doesn't happen in places where we care about perf. The proposed mitigation would be to split the method into a caller that stackallocs and a callee that loops. These parts can be reunited (if deemed profitable) via normal inlining, or the callee marked with
AggressiveInlining.FYI @stephentoub -- possible pattern to avoid since it creates methods that can't benefit from Dynamic PGO.
Forked this off as #85548
hez2010 commentedon May 10, 2023
I think it is common because many developers (who cares about allocations and performance) are writing code like below nowadays.