-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
JIT: split loop headers that are also try entries #110880
Conversation
when there are backedges that have exited the try. This allows the preheader and header to be in the same EH region and may unblock other optimizations. This is done by splitting the header block and making the trailing portion be the try entry. Perform a mid (or end) block split if the initial (or all) statements in the header cannot throw. Fixes dotnet#96887. Enable cloning of loops with EH.
@jakobbotsch PTAL I don't remember what the diffs look like, and the old PR runs are gone, so will update with a summary once the new runs are in. |
src/coreclr/jit/optimizer.cpp
Outdated
// If we split any headers we must rebuild DFS/loops. | ||
// This should be relatively uncommon. | ||
// | ||
if (splitHeaders) | ||
{ | ||
fgInvalidateDfsTree(); | ||
m_dfsTree = fgComputeDfs(); | ||
m_loops = FlowGraphNaturalLoops::Find(m_dfsTree); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious why this is needed when we don't need it in the case where we create new preheaders. It seems like this should not be strictly required for similar reasons as the comment below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I suppose the issue is that splitting a header can change which blocks are the exiting ones.
I wonder if this could be a separate canonicalization step that happens after exit canonicalization, and that then splits the headers for all loops where the header is a try-begin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In earlier versions I didn't have this bit and ran into issues. Let me look into moving it later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe what we can do is (1) always create preheader in the same EH region as the header, then (2) canoncalize exits, then (3) split headers where the header is a try entry and not all backedges sources are within the try.
In (1) we can keep track of the loops that require (3).
Does this allow us to assert in |
Seems like it should -- either all backedges are from within the try, in which case the preheader becomes the new try entry, or some backedges are from an outer region, in which case the try entry is pushed down inside the loop. |
Can you also add a small note to runtime/src/coreclr/jit/compiler.h Lines 2063 to 2065 in 5824c47
I also think this comment can be deleted (and the loop below potentially simplified, as the runtime/src/coreclr/jit/optimizer.cpp Lines 3970 to 3978 in 5824c47
|
Current diffs -- looks like we are missing some collections. |
Not sure about this one, unless you are proposing we always split the header into an exception-free portion (possibly an empty block) and exception-possible portion. Currently we'd only do this split if there are backedges from sources that are not within the try. What is true after this is that no block in the loop is dominated by a block outside the try. Either the try entry is the preheader (so the preheader dominates), or the try entry is inside the loop (so the loop header dominates). |
I moved the header split to after exit edge processing. To make this work I needed to know the preheader and the simplest route seemed to be to update the entry edge list. I haven't done anything with the dominance guarantees; if we want to pursue this then we can change the "split if necessary" to always split loop headers in try regions (or perhaps just try regions whose handlers may resume in the loop). I suspect the empty header block that will sometimes be needed to provide this guarantee will often get merged into its successor, so in those cases the guarantee would be fragile. |
src/coreclr/jit/optimizer.cpp
Outdated
while ((splitBefore != stopStmt) && (splitBefore->GetRootNode()->gtFlags & GTF_EXCEPT) == 0) | ||
{ | ||
splitBefore = splitBefore->GetNextStmt(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to check for GTF_CALL
too? I think your recent try-removal passes did that to be safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're post-morph I would hope GTF_EXCEPT
is sufficient -- those other phases run very early where we're not yet doing flags validation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm mostly worried that certain classes of exceptions that we sometimes allow reordering (like OutOfMemoryException
) would suddenly end up being moved out of a try-region. There may be some cases where we intentionally set only GTF_CALL
but not GTF_EXCEPT
, but I'm not sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does look like allocation helpers are considered "no throw" and only get GTF_CALL
, so we probably do need to check for that here too. Seems like we are moving to a state where we need to start differentiating between "throws reorderable exceptions" and "truly does not throw", since only the latter can be moved out of a try region.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, added GTF_CALL.
BasicBlock* header = loop->GetHeader(); | ||
BasicBlock* preheader = loop->GetPreheader(); | ||
|
||
if (BasicBlock::sameTryRegion(header, preheader)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be sufficient to check bbIsTryBeg
directly here? Then we could avoid having to look for the preheader / the entry edge updating.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would but then I'd have to do a bit of work to find the proper EH region for the header (not a big deal, we already looked for it when we created the preheader).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great to me, thanks for doing this.
What I mean is: does this canonicalization make it so that all loop blocks are dominated by the loop header (for the traditional definition of dominance)? I think it does: if the header cannot be a try-begin anymore, then throwing an exception in the header will always exit the loop. Thus any time a loop block is entered, we know that we executed all of the header. |
That's probably not true actually, due to the special case where handlers are allowed to reenter their corresponding try-region at any point, not just at the beginning. So you could have odd loop shapes that start in the middle of a try region, throw an exception to enter the handler, and then jump back into the middle of the try region to continue the loop. |
Right, if the loop is inside the try and the try's catch can resume at the loop header then you can iterate the loop without fully executing the header (though possibly these all end up looking like irreducible cases...?) |
spmi failure is a missing mch ... |
/azp run runtime-coreclr jitstress |
Azure Pipelines successfully started running 1 pipeline(s). |
when there are backedges that have exited the try. This allows the preheader and header to be in the same EH region and may unblock other optimizations.
This is done by splitting the header block and making the trailing portion be the try entry. Perform a mid (or end) block split if the initial (or all) statements in the header cannot throw.
Fixes #96887.
Enable cloning of loops with EH.