-
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: Allow helper calls that always throw to be marked as no-return #100900
Conversation
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
cc @dotnet/jit-contrib, @AndyAyersMS PTAL. Nice size improvements. |
I think I'd prefer to see the inlining aspects get handled in the same way we handle other cases of inlinee->root propagation: runtime/src/coreclr/jit/fginline.cpp Lines 1522 to 1610 in eb4bf70
So inlinee would transfer its no return call info to root only if the inline happens. |
if ((call->AsCall()->gtCallMoreFlags & GTF_CALL_M_DOES_NOT_RETURN) != 0 || | ||
((call->AsCall()->gtCallType == CT_HELPER) && | ||
Compiler::s_helperCallProperties.AlwaysThrow(call->AsCall()->GetHelperNum()))) | ||
if (call->AsCall()->IsNoReturn()) |
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.
Not related to this change, but I was wondering - why do we do (call->gtOper == GT_CALL)
?
Is it possible at this point for a BBJ_THROW to end with anything other than a call?
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 am mostly concerned about a possibility that the throwing call is somewhere inside the block and followed by some unreachable junk. Then we either emit the junk unnecessarily, or, worse, emitter may skip/optimize it and we end up with the same state we are trying to prevent here.
So I wonder - is there GT_SOMETHING
that is not a call, and yet it throws and can conclude a BBJ_THROW
?
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 tested this out by asserting the last node is a call if the block has any IR, and the assert did hit. Here's some example IR it hit for:
------------ BB04 [0003] [024..02A) (throw), preds={BB03} succs={}
N069 (???,???) [000043] ----------- IL_OFFSET void INLRT @ 0x024[E-] REG NA
N071 ( 3, 10) [000046] Hc--------- t46 = CNS_INT(h) long 0x7ffc04235008 ftn REG NA
/--* t46 long
N073 ( 5, 12) [000047] nc--G------ t47 = * IND long REG NA
/--* t47 long control expr
N075 ( 14, 5) [000020] --CXG------ t20 = * CALL int Microsoft.FSharp.Core.Operators+OperatorIntrinsics:alreadyFinished[int]():int REG rax $c4
/--* t20 int
N077 ( 15, 6) [000022] --CXG------ * RETURN int REG NA $VN.Void
That call is marked as not returning, but there's still a GT_RETURN
after it; this seems to confirm your concerns. Perhaps we need a dead code elimination check somewhere that deletes any IR after no-return calls, though a quick-and-dirty fix might be to always emit the breakpoint after no-return calls (regardless of whether they're the last node or not) in CodeGen::genCall
, or somewhere similar.
This method in particular expects an int to be return, so removing the GT_RETURN
might trip up the JIT if we do it too early...
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.
Morph has an exemption for tail calls that are no return, and will not prune the IR afterwards. So likely the method above is a tail call? Also there is this note:
runtime/src/coreclr/jit/morph.cpp
Lines 7825 to 7844 in 543b2cc
if (call->IsNoReturn()) | |
{ | |
// | |
// If we know that the call does not return then we can set fgRemoveRestOfBlock | |
// to remove all subsequent statements and change the call's basic block to BBJ_THROW. | |
// As a result the compiler won't need to preserve live registers across the call. | |
// | |
// This isn't need for tail calls as there shouldn't be any code after the call anyway. | |
// Besides, the tail call code is part of the epilog and converting the block to | |
// BBJ_THROW would result in the tail call being dropped as the epilog is generated | |
// only for BBJ_RETURN blocks. | |
// | |
if (!call->IsTailCall()) | |
{ | |
fgRemoveRestOfBlock = true; | |
} | |
} | |
return call; |
which the IR dump above seems to contradict, since it looks from the dump that the block kind is BBJ_THROW. I wonder if the comment is now stale?
Earlier on morph will reject implicit tail calls to no return methods, I wonder if we should just do the same for explicit tail calls, even though technically we'd be violating the intent of the .tail
prefix. Then we would not have these special cases.
a quick-and-dirty fix might be to always emit the breakpoint after no-return calls
There's also a concern that with profiler-driven rejitting a method that might always throw when the JIT looked at it could be updated to not always throw. I don't know if there is any good reason to support this sort of rejitting, but we should make sure there's an obvious failure if someone does it since the jitted code may depend on the old behavior. So I think in general we will want to put a breakpoint afterwards.
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.
Morph has an exemption for tail calls that are no return, and will not prune the IR afterwards. So likely the method above is a tail call?
In the above case, the call is an implicit tail call to a no-return method, so fgMorphPotentialTailCall
rejected it, and fgMorphCall
set fgRemoveRestOfBlock = true
. The GT_CALL
node is an operand of a GT_RETURN
node, which is in the last statement of the block, so fgRemoveRestOfBlock
didn't actually remove any IR, but it still converted the block into a BBJ_THROW
. Perhaps we could add a check that prevents converting the block into a BBJ_THROW
if it ends with a GT_RETURN
node, so that we can always expect the last node in a BBJ_THROW
to be a GT_CALL
? I'm not sure if we lose any benefits by doing this -- maybe we could still mark the block as rarely run?
If we decide to make any changes to this, I'll open a follow-up PR.
Can you update the PR/initial comment above to reflect the latest? |
Sure thing. |
CI failures are unrelated |
/ba-g unrelated failures with tracking issues |
…otnet#100900) Fixes dotnet#100458 by addressing two issues: When flagging a call node as no-return with GTF_CALL_M_DOES_NOT_RETURN, we should always increment Compiler::optNoReturnCallCount to avoid asserts in Compiler::fgTailMergeThrows. Previously, we weren't doing this in a unified place, which seemed error-prone. When incrementing the optNoReturnCallCount member of an inlinee compiler, ensure this information is propagated to the inlinee's parent compiler. In a similar vein, if we try to inline a call, and the inlinee compiler determines it does not return, make sure we increment optNoReturnCallCount in the parent compiler object if the inline fails -- we've kept the call, and we now know it doesn't return. With these changes, I can now mark helper calls that always throw as no-return; this unblocks morph to convert BBJ_ALWAYS blocks with helper calls that throw into BBJ_THROW blocks, and has the nice side effect of improving the efficacy of throw merging. Since I was touching relevant code, I decided to improve our usage of GenTreeCall::IsHelperCall, too.
Fixes #100458 by addressing two issues:
GTF_CALL_M_DOES_NOT_RETURN
, we should always incrementCompiler::optNoReturnCallCount
to avoid asserts inCompiler::fgTailMergeThrows
. Previously, we weren't doing this in a unified place, which seemed error-prone.optNoReturnCallCount
member of an inlinee compiler, ensure this information is propagated to the inlinee's parent compiler. In a similar vein, if we try to inline a call, and the inlinee compiler determines it does not return, make sure we incrementoptNoReturnCallCount
in the parent compiler object if the inline fails -- we've kept the call, and we now know it doesn't return.With these changes, I can now mark helper calls that always throw as no-return; this unblocks morph to convert
BBJ_ALWAYS
blocks with helper calls that throw intoBBJ_THROW
blocks, and has the nice side effect of improving the efficacy of throw merging. Since I was touching relevant code, I decided to improve our usage ofGenTreeCall::IsHelperCall
, too.Diffs -- quite extensive in libraries tests; not too surprising as they do a lot of exceptional case validation.