-
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: Keep delegate object alive during invoke #105099
Conversation
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
src/coreclr/jit/lower.cpp
Outdated
// Keep delegate alive | ||
GenTree* baseClone = comp->gtCloneExpr(base); | ||
GenTree* keepBaseAlive = comp->gtNewKeepAliveNode(baseClone); | ||
BlockRange().InsertAfter(call, baseClone, keepBaseAlive); |
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.
Is this fix going to introduce CQ regressions? Ideally, the fix for this issue would be zero codegen diff on x86/x64.
We need to keep delegate alive only until we make the call. The delegate does not need to be alive after 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.
For my knowledge, will all the various stubs that we may go through on the way to the final target automatically keep the right loader allocator without the JIT at minimum putting the delegate instance in a callee-saved register?
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 think so. There are really only 3:
- Shuffle thunks: hand-emitted assembly so the GC cannot kick in while they are executing
- Multicast stubs: they keep the delegate thisptr alive. (Adding comment that this is important may be nice.)
- Marshalled function pointers: The delegate points to itself, so calling the method on the delegate will keep it alive.
It is possible that I may be missing a corner case somewhere, but I do not expect it to be hard to fix.
src/coreclr/jit/lower.cpp
Outdated
{ | ||
GenTree* baseClone = comp->gtCloneExpr(base); | ||
GenTree* keepBaseAlive = comp->gtNewKeepAliveNode(baseClone); | ||
BlockRange().InsertBefore(call, baseClone, keepBaseAlive); |
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.
This InsertBefore
only works due to implementation details of the backend (lazy liveness updates for GC information about registers), which is quite subtle. But of course the fully ideal solution requires adding a new operand to GenTreeCall
and special casing it throughout the backend as an invisible use, which is probably a bit overkill.
I wonder if we in the uncontained case wouldn't be better off just by inserting GT_START_NONGC
and creating a nogc region on the single call instruction. Given that we already have a call and hence a GC safe point this does not seem like it would be very harmful; in a less conservative JIT this basic block might have simply always been partially interruptible for that reason.
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.
hm.. interesting idea! applied
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.
The new diff for the snippet in the issue: https://www.diffchecker.com/CiuxVSBL/
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 think you will also need to add a STOP_NOGC
to turn it off again. Looks like we don't have that today.
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 think you will also need to add a
STOP_NOGC
to turn it off again. Looks like we don't have that today.
I guess it's not from a correctness stand point, just to reduce the nongc area if we have a lot more statements in the current block?
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 guess this one is tricky - we might end whatever was expected to be nongc even before we emitted the start_nogc
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's counted, so it doesn't turn off nogc until the count gets to zero. But regardless I wouldn't expect there to ever be another active nogc region in this case
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 guess it's not from a correctness stand point, just to reduce the nongc area if we have a lot more statements in the current block?
I'm not really sure how the VM behaves if the return address is in a nogc region – but in any case it's unnecessary. I think we only need it to be active from the load of the target address (which is the last use) to the call – should be just one instruction in the end
src/coreclr/jit/lower.cpp
Outdated
#if !defined(TARGET_XARCH) | ||
if (comp->GetInterruptible()) | ||
{ | ||
// If the target's backend doesn't support indirect calls with immediate operands (contained) | ||
// and the method is marked as interruptible, we need to insert a GT_START_NONGC before the call. | ||
// to keep the delegate object alive while we're obtaining the function pointer. | ||
GenTree* startNonGCNode = new (comp, GT_START_NONGC) GenTree(GT_START_NONGC, TYP_VOID); | ||
BlockRange().InsertAfter(thisArgNode, startNonGCNode); | ||
|
||
// We should try to end the non-GC region just before the actual call. | ||
*shouldEndNogcBeforeCall = true; | ||
} | ||
#endif | ||
|
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 think it would be cleaner to insert both START
and STOP
at the end of LowerCall
instead of doing half in here and half in LowerCall
.
Also, is inserting the stop node before the call node really correct?
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.
Also, is inserting the stop node before the call node really correct?
Why not?
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.
The GC is illegal when we are on the blr
instruction -- it is not illegal when we are on the ldr
instruction. https://www.diffchecker.com/NwHIxdIA/ looks like it makes the GC illegal on the ldr
instruction but not on the blr
instruction.
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.
can GC happen on call instructions before we entered the call?
If I need to extend the nogc past the call then it's easier for my PR, I just though that I should not add a call to nogc block (because GC may happens inside the 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.
Addressed
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.
can GC happen on call instructions before we entered the call?
Yes, I think that's the problem here. If GC happens on that call instruction nothing indicates the delegate is live and thus the target we are going to jump to may be collected.
I just though that I should not add a call to nogc block (because GC may happens inside the call)
Hmm, I'm not totally sure what happens, but the safepoint should logically be on the return address, not on the call, so I think we should be fine.
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 was just wondering if we have any debug mechanism that checks that all calls inside nogc blocks can't trigger GC even internally, but I presume we don't otherwise we'd catch the recent bug with BULKBARRIER earlier
src/coreclr/jit/lower.cpp
Outdated
#if !defined(TARGET_XARCH) | ||
if (comp->GetInterruptible()) | ||
{ | ||
// If the target's backend doesn't support indirect calls with immediate operands (contained) | ||
// and the method is marked as interruptible, we need to insert a GT_START_NONGC before the call. | ||
// to keep the delegate object alive while we're obtaining the function pointer. | ||
GenTree* startNonGCNode = new (comp, GT_START_NONGC) GenTree(GT_START_NONGC, TYP_VOID); | ||
GenTree* stopNonGCNode = new (comp, GT_STOP_NONGC) GenTree(GT_STOP_NONGC, TYP_VOID); | ||
BlockRange().InsertAfter(thisArgNode, startNonGCNode); | ||
BlockRange().InsertAfter(call, stopNonGCNode); | ||
} | ||
#endif |
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 would still do this inside LowerCall
since it's LowerCall
responsibility to insert the control expression returned by this function. This is adding an assumption about where LowerCall
will insert that node.
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 would still do this inside
LowerCall
since it'sLowerCall
responsibility to insert the control expression returned by this function. This is adding an assumption about whereLowerCall
will insert that node.
but that's the point, no? LowerDelegateInvoke
already inserts some trees and at the end of those we conservatively emit a START_NOGC so other functions like LowerCall
itself, LowerCFGCall
or LowerTailCallViaJitHelper
can insert whatever they want after our NOGC and before the call. Because, presumably, they can extend the dangerous area where GC can kick in and collect our delegate
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 yeah, that's a good point too. What I dislike here is just that it still relies on LowerCall
to insert the control expression after the START_NONGC
. To be conservatively correct we have to insert the START_NOGC
before the last use of the delegate, since emitDisableGC()
means something like "the next instruction I emit is going to make GC information wrong, so disable GC after that instruction". Hence inserting START_NONGC
after thisArgNode
is only correct because there ends up being another use inserted after that point by LowerCall
. The main idea behind moving the logic into LowerCall
is that it has the full overview of what nodes it is going to insert and where, so it can make the decision with more information. So for instance it could insert it before the gtControlExpr
in the delegate case, knowing that gtControlExpr
has the last use of the delegate instance in the common case.
Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it. |
/azp list |
/azp run runtime-coreclr outerloop, runtime-coreclr jitstress |
/azp run runtime-coreclr jitstress |
/azp run runtime-coreclr outerloop |
/azp run runtime-coreclr gcstress-extra, runtime-coreclr gcstress0x3-gcstress0xc |
/azp run runtime-coreclr gcstress-extra, runtime-coreclr gcstress0x3-gcstress0xc |
A quick fix for #105082, we just insert a KeepAlive node for the delegate object, diffWithGC for the snippet in the issue:
https://www.diffchecker.com/fhmuJnXa/https://www.diffchecker.com/CiuxVSBL/https://www.diffchecker.com/sLNK1aIS/