Skip to content
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

Merged
merged 7 commits into from
Jan 8, 2025

Conversation

AndyAyersMS
Copy link
Member

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.

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.
@AndyAyersMS AndyAyersMS marked this pull request as ready for review January 6, 2025 15:43
@AndyAyersMS
Copy link
Member Author

@jakobbotsch PTAL
cc @dotnet/jit-contrib

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.

Comment on lines 2762 to 2770
// 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);
}
Copy link
Member

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.

Copy link
Member

@jakobbotsch jakobbotsch Jan 6, 2025

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.

Copy link
Member Author

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.

Copy link
Member Author

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).

@jakobbotsch
Copy link
Member

Does this allow us to assert in fgDebugCheckLoops that no loop-header is a try-begin?

@AndyAyersMS
Copy link
Member Author

Does this allow us to assert in fgDebugCheckLoops that no loop-header is a try-begin?

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.

@jakobbotsch
Copy link
Member

Does this allow us to assert in fgDebugCheckLoops that no loop-header is a try-begin?

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

// * All loop blocks are dominated by the header block, i.e. the header block
// is guaranteed to be entered on every iteration. Note that in the prescence
// of exceptional flow the header might not fully execute on every iteration.
? I believe we have a stronger guarantee after this canonicalization has happened, namely that the header is always exited, i.e. the same as the normal dominance relation we get from the dominator tree.

I also think this comment can be deleted (and the loop below potentially simplified, as the ContainsBlock check should be always true now, but I'm ok with leaving the code alone):

// Note that there is a mismatch between the dominator tree dominance
// and loop header dominance; the dominator tree dominance relation
// guarantees that a block A that dominates B was exited before B is
// entered, meaning it could not possibly have thrown an exception. On
// the other hand loop finding guarantees only that the header was
// entered before other blocks in the loop. If the header is a
// try-begin then blocks inside the catch may not necessarily be fully
// dominated by the header, but may still be part of the loop.
//

@AndyAyersMS
Copy link
Member Author

Current diffs -- looks like we are missing some collections.

@AndyAyersMS
Copy link
Member Author

AndyAyersMS commented Jan 7, 2025

? I believe we have a stronger guarantee after this canonicalization has happened, namely that the header is always exited, i.e. the same as the normal dominance relation we get from the dominator tree.

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).

@AndyAyersMS
Copy link
Member Author

AndyAyersMS commented Jan 7, 2025

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.

Comment on lines 3004 to 3007
while ((splitBefore != stopStmt) && (splitBefore->GetRootNode()->gtFlags & GTF_EXCEPT) == 0)
{
splitBefore = splitBefore->GetNextStmt();
}
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

@jakobbotsch jakobbotsch Jan 7, 2025

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.

Copy link
Member

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.

Copy link
Member Author

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))
Copy link
Member

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.

Copy link
Member Author

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).

Copy link
Member

@jakobbotsch jakobbotsch left a 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.

@jakobbotsch
Copy link
Member

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).

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.

@jakobbotsch
Copy link
Member

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.

@AndyAyersMS
Copy link
Member Author

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...?)

@AndyAyersMS
Copy link
Member Author

spmi failure is a missing mch ...

@AndyAyersMS
Copy link
Member Author

/azp run runtime-coreclr jitstress

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@AndyAyersMS AndyAyersMS merged commit 5f3aa0c into dotnet:main Jan 8, 2025
124 of 126 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
Projects
None yet
Development

Successfully merging this pull request may close these issues.

JIT: Consider canonicalizing loops whose header is the beginning of a try
2 participants