You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
JIT: Rarely run exception block is not placed at the end, regression #117577
In the Get function the rarely run exception throwing block is not placed after the vector access causing a jmp on every call. This is a regression to .NET 9. In GetFixed I rewrote the if statement which fixes it. https://godbolt.org/z/M4dvTv1ah
staticfloatGet(Vector2vec,intindex){if(index>1||index<0){ThrowOutOfRangeException("You tried to access this vector at index: {0}",index);}return*(&vec.X+index);}staticfloatGetFixed(Vector2vec,intindex){if((uint)index>1){ThrowOutOfRangeException("You tried to access this vector at index: {0}",index);}return*(&vec.X+index);}
As for why we biased the edge likelihoods toward the throw block, in the absence of profile data, we frequently come up with 50/50 likelihood splits, and because the throw statement is hidden in ThrowOutOfRangeException, we don't know that BB03 throws until after inlining runs. So we initially come up with 50/50 splits for each condition in the if-statement (bool opts later collapses this into one check, with 25/75 splits). It is only after morph that we learn BB03 throws. I added a phase recently that adjusts the likelihoods of edges into throw blocks once we know they throw, but it only runs for PGO scenarios; I suppose it has similar value in non-PGO scenarios.
@BoyBaykiller out of curiosity, are you running your program with tiered compilation disabled? If not, I'd be interested in seeing a JIT dump of this method from a local run to see why these edges aren't being adjusted. Thanks!
Block layout relies on edge weights rather than block weights to drive layout decisions, so the incorrect weight on BB03 doesn't trip it up. At some point (maybe .NET 11), I'd like to enable all the new profile maintenance code for non-PGO scenarios too so we can get this right, but I suspect having non-zero weights on throw blocks isn't all that detrimental, since many (most?) throw blocks contain little more than the throw statement.
@amanasifkhalid but this is still basically a .NET 10.0 regression, isn't?
It is, and I suspect #111253 is to blame. In Compiler::fgConvertBBToThrowBB, we used to always mark throw blocks as rarely-run, without tweaking the edge likelihoods into the throw block. This inconsistency didn't pose a problem in .NET 9, because we didn't run profile repair anywhere after morph. Now in PGO scenarios, we can rely on the post-morph edge likelihood adjustment phase to reduce flow into throw blocks, and for profile repair to update throw blocks' weights accordingly. In non-PGO scenarios, these throw blocks are left with whatever incoming flow they already had. If this flow isn't miniscule, block layout may not push the block to the end of the method.
Ideally, we'd rely on the new edge likelihood adjustment phase to fix this problem, but this isn't the most targeted fix. We could also mark new throw blocks as rarely-run in Compiler::fgConvertBBToThrowBB like we used to, but only when we don't have profile data. That should return us to .NET 9's layout while (maybe) having little impact on other phases.
out of curiosity, are you running your program with tiered compilation disabled? If not, I'd be interested in seeing a JIT dump of this method from a local run to see why these edges aren't being adjusted
There isn't at the moment. My plan is for it to always run when optimizing, regardless of whether TC is enabled or not, but that probably won't make it in .NET 10.
In .NET 10, we refactored several front-end phases to ensure the profile data fed into the JIT is maintained throughout transformations -- for example, if a block no longer jumps to another block, the weight into the latter block needs to be reduced accordingly. The phase I mentioned above is part of that work, and all of this work currently applies to PGO scenarios. When the JIT doesn't have PGO data for a method (for example, if TC is disabled and the user didn't give us static PGO), this profile manipulation doesn't kick in. Part of the reason we're conservative at the moment is because we aren't all that confident in the execution counts the JIT makes up for such scenarios. This is something I want to address in .NET 11: We have plans to implement smarter heuristics for creating fake profile data in non-PGO scenarios, and turning on all the profile synthesis enhancements we made in .NET 10 for all scenarios. For .NET 10, that's not the fix I want to make (see below).
Is #111253 the reason why the throw block gets bbWeight=0.0 in .NET 9 and bbWeight=0.5 on main?
Yes, sort of. That change is why bbWeight is no longer set to zero for the throw block; the 0.5 value is just left over from whatever the JIT's early block weight heuristics came up with. Those heuristics run before morph, so they're operating on limited information; for example, we don't know if ThrowOutOfRangeException throws an exception until it's inlined, or if it isn't inlined, until morph notices it doesn't return and converts the calling block into a throw block.
I think the solution I want to proceed with is to bring back the morph heuristic for setting throw blocks' weights to zero, but only in non-PGO scenarios. When we don't have PGO data, we're making the profile data up as we go, so I don't see an issue with doing the same here. I'd expect most of the churn from this change to be from throw blocks being moved out-of-line in the code layout, which is largely a return to .NET 9's behavior.
Activity
dotnet-policy-service commentedon Jul 13, 2025
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.
EgorBo commentedon Jul 14, 2025
@amanasifkhalid could it be regressed by some layout work?
amanasifkhalid commentedon Jul 14, 2025
I can reproduce this with tiering disabled. Based on the edge likelihoods we're coming up with, block layout is doing the "right" thing:
As for why we biased the edge likelihoods toward the throw block, in the absence of profile data, we frequently come up with 50/50 likelihood splits, and because the throw statement is hidden in
ThrowOutOfRangeException, we don't know thatBB03throws until after inlining runs. So we initially come up with 50/50 splits for each condition in the if-statement (bool opts later collapses this into one check, with 25/75 splits). It is only after morph that we learnBB03throws. I added a phase recently that adjusts the likelihoods of edges into throw blocks once we know they throw, but it only runs for PGO scenarios; I suppose it has similar value in non-PGO scenarios.@BoyBaykiller out of curiosity, are you running your program with tiered compilation disabled? If not, I'd be interested in seeing a JIT dump of this method from a local run to see why these edges aren't being adjusted. Thanks!
amanasifkhalid commentedon Jul 14, 2025
I can confirm that running the new edge likelihood adjustment phase when tiering is disabled fixes the layout for the above program:
Block layout relies on edge weights rather than block weights to drive layout decisions, so the incorrect weight on
BB03doesn't trip it up. At some point (maybe .NET 11), I'd like to enable all the new profile maintenance code for non-PGO scenarios too so we can get this right, but I suspect having non-zero weights on throw blocks isn't all that detrimental, since many (most?) throw blocks contain little more than the throw statement.EgorBo commentedon Jul 14, 2025
@amanasifkhalid but this is still basically a .NET 10.0 regression, isn't? For e.g. NativeAOT since that one typically doesn't have any profile data
amanasifkhalid commentedon Jul 14, 2025
It is, and I suspect #111253 is to blame. In
Compiler::fgConvertBBToThrowBB, we used to always mark throw blocks as rarely-run, without tweaking the edge likelihoods into the throw block. This inconsistency didn't pose a problem in .NET 9, because we didn't run profile repair anywhere after morph. Now in PGO scenarios, we can rely on the post-morph edge likelihood adjustment phase to reduce flow into throw blocks, and for profile repair to update throw blocks' weights accordingly. In non-PGO scenarios, these throw blocks are left with whatever incoming flow they already had. If this flow isn't miniscule, block layout may not push the block to the end of the method.Ideally, we'd rely on the new edge likelihood adjustment phase to fix this problem, but this isn't the most targeted fix. We could also mark new throw blocks as rarely-run in
Compiler::fgConvertBBToThrowBBlike we used to, but only when we don't have profile data. That should return us to .NET 9's layout while (maybe) having little impact on other phases.BoyBaykiller commentedon Jul 14, 2025
I am. With TC I get the correct ordering in Tier1: https://gist.github.com/BoyBaykiller/f486cdf2ffd0703041bc019c3ffe4c53
Is there some env var to enable this new phase?
Is #111253 the reason why the throw block gets
bbWeight=0.0in .NET 9 andbbWeight=0.5on main?amanasifkhalid commentedon Jul 15, 2025
Thanks for sharing the JIT dumps!
There isn't at the moment. My plan is for it to always run when optimizing, regardless of whether TC is enabled or not, but that probably won't make it in .NET 10.
In .NET 10, we refactored several front-end phases to ensure the profile data fed into the JIT is maintained throughout transformations -- for example, if a block no longer jumps to another block, the weight into the latter block needs to be reduced accordingly. The phase I mentioned above is part of that work, and all of this work currently applies to PGO scenarios. When the JIT doesn't have PGO data for a method (for example, if TC is disabled and the user didn't give us static PGO), this profile manipulation doesn't kick in. Part of the reason we're conservative at the moment is because we aren't all that confident in the execution counts the JIT makes up for such scenarios. This is something I want to address in .NET 11: We have plans to implement smarter heuristics for creating fake profile data in non-PGO scenarios, and turning on all the profile synthesis enhancements we made in .NET 10 for all scenarios. For .NET 10, that's not the fix I want to make (see below).
Yes, sort of. That change is why
bbWeightis no longer set to zero for the throw block; the0.5value is just left over from whatever the JIT's early block weight heuristics came up with. Those heuristics run before morph, so they're operating on limited information; for example, we don't know ifThrowOutOfRangeExceptionthrows an exception until it's inlined, or if it isn't inlined, until morph notices it doesn't return and converts the calling block into a throw block.I think the solution I want to proceed with is to bring back the morph heuristic for setting throw blocks' weights to zero, but only in non-PGO scenarios. When we don't have PGO data, we're making the profile data up as we go, so I don't see an issue with doing the same here. I'd expect most of the churn from this change to be from throw blocks being moved out-of-line in the code layout, which is largely a return to .NET 9's behavior.
amanasifkhalid commentedon Jul 16, 2025
With the changes in #117692, the layout is fixed for me:
amanasifkhalid commentedon Jul 16, 2025
@BoyBaykiller you should see this scenario fixed in Preview 7 and later. Please reopen this issue if you don't. Thank you for trying .NET 10!
JIT: Mark throw blocks with heuristic-derived weights as rarely run d…