-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Conversation
Marking no merge for now since this is work in progress. This impacts ~267 methods in System.Private.CoreLib,. 453 sites are devirtualized. No hits in other assemblys in jit-diff, presumably because of R2R restrictions, though I have yet to dig in deeper. Simple jitted test cases show expected results. Particular areas for review: implementation of the jit interface call and the fragile way that call related info is updated. @jkotas, @JosephTremoulet PTAL Related issues: #1166, #8772 |
src/jit/importer.cpp
Outdated
|
||
// If we have a virtual call, is the class of 'this' a subtype of | ||
// the class of the method? If so perhaps we can devirtualize. | ||
if ((call->gtFlags & GTF_CALL_VIRT_KIND_MASK) == GTF_CALL_VIRT_VTABLE) |
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.
You should also do this for CORINFO_VIRTUALCALL_STUB
. It is used for interface calls under JIT and fragile NGen, and for all virtual calls under R2R. Thus, you should see some hits for R2R once you do that.
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.
Have this sort of working, and yes I'm seeing it kick in for R2R code.
} | ||
|
||
// If the objClass is sealed (final), then we may be able to devirtualize. | ||
const DWORD objClassAttribs = info.compCompHnd->getClassAttribs(objClass); |
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 should be cheaper (less VM/EE calls) and cleaner to let the VM do all these checks that are pre-reqs for devirtualization.
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.
Agreed. I'll refactor to consolidate stuff on the VM side after I get a bit more functionality in place.
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.
Are you still planning to do this?
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.
Thinking of leaving things as they are now -- a few checks (eg the one for __Canon) were moved to the VM side, but a number remain on the jit side -- notably the attribute checks for final/sealed methods and classes.
Seemed logical to me to keep the policy decisions on the jit side and have the VM deal with correctness/servicing, which more or less dictates the current setup. Otherwise the jit has to pass in a bit more state (for instance, is the type known exactly) and get back more state (whether the target is known exactly).
Some of the jit-side checks will be removed in future work, eg interface call devirtualization.
src/vm/jitinterface.cpp
Outdated
{ | ||
CORINFO_METHOD_HANDLE result; | ||
|
||
JIT_TO_EE_TRANSITION(); |
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.
You do not need this if you just forwarding to other JIT/EE interface method.
CORINFO_CLASS_HANDLE implementingClass | ||
); | ||
|
||
CORINFO_METHOD_HANDLE resolveVirtualMethodExact( |
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 be useful to have some comments what these methods do. (In particular, what's the difference between them.)
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.
Sure. This is the shim layer header for SPMI, so I'll put something cursory here and something more detailed over in corinfo.h
src/vm/jitinterface.cpp
Outdated
// MethodDescs returned to JIT at runtime are always fully loaded. Verify that it is the case. | ||
_ASSERTE(pMT->IsRestored() && pMT->IsFullyLoaded()); | ||
|
||
MethodDesc* pMD = pMT->GetMethodDescForSlot(slot); |
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 R2R, this will also need the version bubble check.
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.
Are you saying the current approach (where the jit calls canInline on the resulting method handle) is insufficient?
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.
canInline should work, but it is too pessimistic check and makes the optimization order dependent because of the noinlining hint is cached. I do not think you should be using canInline to gate it.
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.
Ah, right. I have comments about the unfortunate impact of the noinline hint; this would be a good way to get past it.
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.
Seems like I can maybe use getMethodBeingCompiled()
for this? Something like:
// Allow devirtialization if jitting, or if prejitting and the
// method being jitted and the devirtualized method are in the
// same versioning bubble.
if (!IsCompilingForNGen() || IsInSameVersionBubble(getMethodBeingCompiled(), pMD))
{
result = (CORINFO_METHOD_HANDLE) pMD;
}
Otherwise I need to pass in another method handle.
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.
Yes, this should work.
src/vm/jitinterface.cpp
Outdated
JIT_TO_EE_TRANSITION(); | ||
|
||
result = resolveVirtualMethodExact(methodHnd, derivedClass); | ||
|
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.
Why do we have two methods here?
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 matching jit interface changes from .Net Native. It looks like I may not need both versions.
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.
On UTC the resolveVirtualMethod canonicalize's its result in most cases, but resolveVirtualMethodExact does not. This is used to handle the inlining of generic dictionary cell usage into the outer method. Given that RyuJit has a different model for handling shared generics that can't do the inlining anyways, this is probably ok. I would suggest only implementing the resolveVirtualMethod api, and not implementing resolveVirtualMethodExact. (This is simply done, as the implementation below is effectively the same as resolveVirtualMethod should be returning, but isn't quite the same as what resolveVirtualMethodExact would return in ProjectN.)
Could not repro release crossgen failure at b334a7f; wonder if I need to rebase to hit it...? |
Failures in 56f702a are missing null checks; looks like at least some of the vcall stubs do implicit null checking, which will now need to be made explicit. Not sure if I should enable this for all the stub devirts or just a subset; for now I'll turn it on for all. |
Same tests failed again in 0d17370; serves me right for jumping to the conclusion that it was stub null checks at fault. Will debug locally this time... |
src/jit/importer.cpp
Outdated
return; | ||
} | ||
|
||
// Do we know anything about the type of the 'this'? |
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.
Have you looked at what we do with (constrained) callvirt on value types? Not sure what shape those are in when they hit this code, but presumably they'd make the exact type easier to find...
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.
Yes, we will (always?) see an upstream Box and can work out the type from there.
However I will probably leave this as future work since it would also be nice to optimize away the boxing at the same time.
src/jit/importer.cpp
Outdated
} | ||
|
||
// Fetch attributes and do more vetting of the method. In general, | ||
// if we can't inline, we won't devirtualize. |
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 clear how the comment relates to the code. Did you mean to check for some inline-blocking attributes here?
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 is a stale comment. I used to call canInline
but have since moved the relevant checks to the VM side.
src/jit/importer.cpp
Outdated
|
||
if (objClassIsFinal || derivedMethodIsFinal) | ||
{ | ||
JITDUMP("!!! Inlining ok, no restrictions, final; can devirtualize\n"); |
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 confused again by the reference to inlining.
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 a stale comment.
src/jit/importer.cpp
Outdated
} | ||
#endif | ||
|
||
// Need to update call info too. This is fragile |
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.
Do you think we could run these checks upstream, at the point where we're calling getCallInfo
today? Then the callInfo
wouldn't be mutating on us, and importCall
could set these other flags on the call node as usual.
In fact, since you're already intending to push a number of these checks over to the VM, I'm wondering if we could end up with an interface where getCallInfo
just takes an additional parameter which is the most-derived "thisPtr" type that the jit can determine...
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 is one area I'm still experimenting with.
I don't like the idea of patching the call info or the various context handles, and it seems like the patching may get more complex when extending this to support shared generics.
But I am not sure we can anticipate all the info we might need early.
For instance, I'd like to call back into the devirtualization code during inlining or even later on during optimization, as we potentially learn more about types.
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.
Right now you have it coded up as
- (if we can) get an updated
CORINFO_METHOD_HANDLE
(else abort) - update the
GenTreeCall
node to the new target (returned from 1) and flags (inferred from change to nonvirtual call) - update the
CORINFO_CALL_INFO
I think you might be happier to switch the second two:
- (if we can) get an updated
CORINFO_METHOD_HANDLE
(else abort) - update the
CORINFO_CALL_INFO
- update the
GenTreeCall
node to the new target (returned from 1) and flags (returned from 2)
because
- You can code the changes to the
GenTreeCall
as a function of theCORINFO_CALL_INFO
, so you may be able to share that code with the importer upstream and optimizations downstream - You can separate the concerns of determining what the new info/attributes should be (which would be the part computing the new
CORINFO_CALL_INFO
) and determining which fields on aGenTreeCall
need to change in response to devirtualization (which could be pretty mechanical). Currently you have a mix of those things in your second and third steps. - It would line up with the desire to move some of this to the VM side, since we're used to the VM populating the
CORINFO_CALL_INFO
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.
Wanted to get the post-inline case implemented so whatever approach I finally settle on handles both importer and inliner variants smoothly.
So with latest updates the jit can devirtualize based on actual return types. For example, in the "sealed default" pattern from #9276: using System;
public class Base
{
public virtual int Foo() { return 33; }
static BaseSealed s_Default = new BaseSealed();
public static Base Default => s_Default;
}
sealed class BaseSealed : Base {}
public class Test
{
public static int Main()
{
Base b = Base.Default;
int x = b.Foo();
return (x == 33 ? 100 : -1);
}
} the jit will devirtualize the call to Inlining |
Thinking those ARM64 failures may be related to #9375. |
Latest change ends up triggering a lot of non-crossgen fatal R2R errors of the form:
suspect something is off either with the devirt update to R2R calls or the dependence tracking. (Update: this was from a private change I never pushed to the fork, so disregard the details here. R2R methods are still failing to encode in some cases, see below). |
Still working through this. Was thrown off by the fact that in R2R mode crossgen will throw/catch for "expected" failures. |
With the 68af9f2 changes we lose 68 methods in R2R images, running into the "Method entrypoint cannot be encoded" notimpl in I haven't been able to find a way to re-invoke @jkotas any ideas? |
src/vm/jitinterface.cpp
Outdated
Assembly* pCallerAssembly = pCallerModule->GetAssembly(); | ||
Assembly* pDevirtAssembly = pDevirtModule->GetAssembly(); | ||
|
||
if (pCallerAssembly == pDevirtAssembly) |
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 should be just (edited):
if (IsReadyToRunCompilation())
{
Assembly * pCallerAssembly = m_pMethodBeingCompiled->GetModule()->GetAssembly();
allowDevirt = IsInSameVersionBubble(pCallerAssembly , pDevirtModule->GetModule()->GetAssembly()) &&
IsInSameVersionBubble(pCallerAssembly , pMT->GetAssembly());
}
else
{
allowDevirt = true;
}
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.
Just to make sure I understand: this extra check is for the case where the class the jit initially identifies as the object type is a subclass of the class that introduces the method, right?
If so, I though I had that covered over on the jit side by mapping back from the devirtualized method to the introducing class, and passing the implementing class to resolveVirtual.
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 extra check is for the case where the class the jit initially identifies as the object type is a subclass of the class that introduces the method, right?
Right.
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 have not realized that it may be handled in the JIT side. It may be good idea to do the version bubble check defensively here anyway.
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.
Agreed, will update.
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 just to be clear -- no version bubble checks for ngen?
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.
Right. Fragile NGen is like JIT.
You can remove the TODO+THROW from |
Removing the throw leads to an AV in |
@jkotas looks like for R2R it is happy if I reinvoke |
Snip from the latest jit-diff stats. It would be really nice if jit-diff would also report how many methods remained unchaged; I'm curious what % of methods this change impacts. The 41 methods that get lost are still a concern. Looks like most of then now bail out in R2R with: Runtime method access checks not supported. Could be a bug in devirt, since overrides should be just as accessible as the base methods. Will dig in deeper...
|
16705f1
to
5dde62b
Compare
Rebased and pushed a few updates. Still pretty rough around the edges. Jit can now spot "exact" types from Fixed issue with context update on the call for cases where the base method is from a non-generic class and the derived class is a shared generic. R2R dropout issues still hit in 41 methods, mostly in Roslyn, still unsure as to what exactly is going wrong. In those cases, the zapper |
@dotnet-bot test Windows_NT_x64 perf |
@dotnet-bot test Windows_NT arm64 Cross Debug Build |
25112a4
to
398bc6e
Compare
Rebased, squashed commits down again into two: one for runtime side and one for jit side. Incorporate some feedback: remove the unused interface method, add the check for __Canon over to the runtime side and remove the overly general check on the jit side. |
Think I cleared up the problems that were causing loss of some R2R methods. No losses seen in the current jit-diff framework set. @dotnet-bot test Windows_NT Checked r2r |
ac18d9a
to
59136f8
Compare
Pushed an update so the jit interface should be in final form for merging. New GUID and all. @JosephTremoulet PTAL FYI this will require a coordinated update for desktop. I'll get that ready and kick off desktop testing. |
2328df3
to
40b1841
Compare
Another forced update to fix formatting, polish the commit text, and remove an unused declaration. |
@AndyAyersMS this still true at current iteration? i.e. is it worth reactivating that PR? |
Yes, it will devirtualize (but not inline, yet) -- provided you use the property directly when you make calls. |
CentOS failure (log) was in a PAL test; added note to possibly related #8321. Will retry. @dotnet-bot retest CentOS7.1 x64 Debug Build and Test |
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.
Overall looks good, just a few questions/comments.
src/jit/flowgraph.cpp
Outdated
// If the parent of the GT_RET_EXPR is a virtual call, | ||
// devirtualization is attempted. This should only succeed in the | ||
// successful inline case, when the inlinee's return value | ||
// expression provides a better type then the declared type of the |
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.
typo: then -> than
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.
Fixed and reworded some neighboring text....
lvaInitVarDsc(varDsc, varNum, strip(corInfoType), typeHnd, localsSig, &info.compMethodInfo->locals); | ||
|
||
varDsc->lvPinned = ((corInfoType & CORINFO_TYPE_MOD_PINNED) != 0); | ||
varDsc->lvOnFrame = true; // The final home for this local variable might be our local stack frame | ||
|
||
if (strip(corInfoType) == CORINFO_TYPE_CLASS) |
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 get the same CORINFO_CLASS_HANDLE
that the call to getArgType
just above got and passed to lvaInitVarDsc
? Is so, maybe move this logic down into lvaInitVarDsc
?
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.
getArgType
doesn't set the class handle for ref types. It is mainly used to establish the representation of the type and not the identity of a type.
@@ -550,6 +558,11 @@ void Compiler::lvaInitUserArgs(InitVarDscInfo* varDscInfo) | |||
|
|||
lvaInitVarDsc(varDsc, varDscInfo->varNum, strip(corInfoType), typeHnd, argLst, &info.compMethodInfo->args); | |||
|
|||
if (strip(corInfoType) == CORINFO_TYPE_CLASS) |
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.
Same as above regarding moving this into lvaInitVarDsc
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.
Same reason as above here.
src/jit/importer.cpp
Outdated
// Virtual calls in IL will always "invoke" the base class method. | ||
// | ||
// This transformation looks for evidence that the type of 'this' | ||
// in the call is a final class or would invoke a final method, and |
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.
nit: or is exact
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.
Fixed
src/jit/importer.cpp
Outdated
// if that and other safety checks pan out, modifies the call and | ||
// the call info to create a direct call. | ||
// | ||
// This transformation is done in the importer and not in some |
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 paragraph reads like we only call this before inline, but the same method gets called for late devirtualization, right?
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.
Yes, I need to update the comment.
} | ||
|
||
// If the objClass is sealed (final), then we may be able to devirtualize. | ||
const DWORD objClassAttribs = info.compCompHnd->getClassAttribs(objClass); |
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.
Are you still planning to do this?
src/jit/importer.cpp
Outdated
// Virtual calls include an implicit null check, which we may | ||
// now need to make explicit. Not sure yet if we can restrict | ||
// this to just a subset or need to do it for all cases, so | ||
// will do it for all unless we know the object is not null. |
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.
Supposing we could restrict this to a subset, would that be up in the code that's setting nullCheck
, or down here? If it would be down here, maybe better to invert the sense of the bool and name it knownNotNull
or something rather than nullCheck
.
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.
When I wrote this I was thinking there might be a locally identifiable subset of calls that would not need checks -- one that could be identified by just looking at the call and its properties. But I no longer believe this.
So, suppressing the check requires some sort of analysis of the 'obj' and doing anything nontrivial here seems risky since we are in the middle of importing.
The string case is trivial so we might as well keep it. I suppose we could also rely on newobj
not returning null.
Inverting the sense of the bool looks like a good suggestion since the common case is that we will add a null check.
Updated to address feedback. Added a 4.6 compat stub (needed for clean desktop builds). Removed the distribution table showing which operators lead to type loss. The information is stale and arguably should live somewhere else. |
Haven't hit any issues in desktop testing, though have only run a subset because the GUID/interface change makes getting the full test of tests running a challenge. |
Waiting for builds with #9869 to make their way over to CoreFx before merging this. Believe they should be in the beta25101-02 build of CoreCLR. |
Actually, watching dotnet/corefx#16574 |
Local CoreFx changes with these changes look good. |
LGTM |
Add new method to jit interface so the jit can determine what derived method might be called for a given base method, derived class pair. Implement support in the VM and in other places (zap, spmi).
Devirtualize calls where the this object type at a call site is a subtype of the type described at the call site. Currently learns types from local and arg references and a subset of other operators. Will devirtualize if either the class or method is final (sealed in C#), or if the type is known exactly (eg from a newobj). Devirtualization is run twice, once during importation, and again in a limited way after inlinining. Calls devirtualized during importation are are subsequently eligible for inlining. Calls devirtualized during inlining currently cannot be inlined.
Am going to push one more force update that will reorder things so the non-jit changes are back together in one commit and the jit changes in another. |
dd298a8
to
393ee92
Compare
Devirtualize calls where the this object type at a call site
is a subtype of the type described at the call site. Currently learns
types from local and arg references and a subset of other operators.
Will devirtualize if either the class or method is final (sealed in C#),
or if the type is known exactly (eg from a newobj).
Devirtualization is run twice, once during importation, and again in a
limited way after inlinining. Calls devirtualized during importation are
are subsequently eligible for inlining. Calls devirtualized during inlining
currently cannot be inlined.