-
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
Crossgen2 support for static virtual method resolution #54063
Conversation
src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs
Outdated
Show resolved
Hide resolved
} | ||
} | ||
} | ||
|
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 logic that refers to Canon, and canonicalization is specific to actual compilation with utilizing a Canonical representation. I would either keep this in CorInfoImpl.ReadyToRun.cs or move move this to the DevirtualizationManager.
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.
Actually given some more thought, this should definitely be part of DevirtualizationManager, and we probably need to have a different result for the conditions of failure due to various canonical shenanigans, and failure due to the resolution algorithm failing.
Note, the canonicalization failure stuff should be in the Common version of DevirtualizationManager.
We should also take a very close look at the version resilience characteristics of all of this, and consider if there are scenarios where we need to abort the calculation, and fallback to a RequiresRuntimeJitException. (That logic would go in the R2R specific DevirtualizationManager.
The resolution algorithm failing should trigger a CodeGenerationException, or RequiresRuntimeJitException (and also probably an assert), and the canonical failure, should trigger the change to use CORINFO_CALL_POINTER.
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 moved the core logic to DevirtualizationManaged and I have added initial provisions for version bubble checks. I think they aren't perfect but they're better than nothing. I'll work on further refining them.
As per PR #52173, this will need updating to use a ConstrainedMethodEntrySlot when a call or ldftn instruction is in use. Refers to: src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs:1676 in 34429cc. [](commit_id = 34429cc199d3e4a76b1cb0089cd3b818339577f8, deletion_comment = False) |
After this initial setting of allowInstParam, you'll need to add the logic which will sometimes set the allowInstParam flag to false. Se PR #52173 for details. Refers to: src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs:1625 in 34429cc. [](commit_id = 34429cc199d3e4a76b1cb0089cd3b818339577f8, deletion_comment = False) |
...r/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/SignatureBuilder.cs
Outdated
Show resolved
Hide resolved
Also, this will need to set useInstantiatingStub to false when we are performing a CORINFO_CALL_CODE_POINTER to a static method on a generic type. In reply to: 859776971 Refers to: src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs:1676 in 34429cc. [](commit_id = 34429cc199d3e4a76b1cb0089cd3b818339577f8, deletion_comment = False) |
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.
🕐
2d70d77
to
0049f3d
Compare
src/coreclr/tools/Common/TypeSystem/Common/MetadataVirtualMethodAlgorithm.cs
Outdated
Show resolved
Hide resolved
if (methodImpl.Decl.OwningType == interfaceType && | ||
methodImpl.Decl == interfaceMethod) |
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 we actually need to pass the interfaceType
around? This condition can be simplified to if (methodImpl.Decl == interfaceMethod)
because the other part is redundant.
The CoreCLR type system is weird and needs to pass around both the method and the owning type separately, but we abstract that weirdness away in CorInfoImpl and don't import it into the general type system.
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.
Well, while I believe you're right that the outer method doesn't need the parameter, according to my understanding for TryResolveVirtualStaticMethodOnThisType
we may need to match the method on a variance matched interface - perhaps that rather means that the method reference comparison is incorrect and we need to perform a structural comparison involving its name and signature?
216b3a4
to
93940e8
Compare
I meant that we don't have a bool flag for the normal interface method resolution - whether we can similarly have a pair of methods to do the static interface method resolution. It would let us have the same structure. I'm indifferent about piping it through the existing API surface as done in 93940e8. |
{ | ||
directMethod = null; | ||
} | ||
forceUseRuntimeLookup = (directMethod == 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.
If we return null because we can't find a result (as there isn't one), and we're not compiling shared generic code, we should produce a CodeGenerationFailedException. I know the current logic in coreclr doesn't fail in the obvious way here, but it does actually reliably fail, and we do have a testcase for 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.
Hmm, why is it necessary to tear down compilation of the entire method? Wouldn't it be sufficient to just keep the constraint on a particular call and let it be resolved by the runtime?
/// </summary> | ||
/// <param name="currentType">Type constraint in the static virtual method </param> | ||
/// <param name="interfaceMethod">Static virtual interface method to resolve</param> | ||
public bool AllowCompileTimeStaticVirtualMethodResolution(TypeDesc currentType, MethodDesc interfaceMethod) |
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 shouldn't be just AllowCompileTimeStaticVirtualMethodResolution, we should make the call to ResolveVariantInterfaceMethodToVirtualMethodOnType from within the DevirtualizationManager, as the full and compilex R2R version resilience rules will depend on the result of resolving the virtual call. In particular, it isn't required that the entire base type chain be within the bubble, but that the chain up until a result is found is within the version bubble.
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, in such case it seems to me that we should rather include the version bubble edge logic directly in the recursive descent into base type in ResolveVirtualStaticMethod
as in contrast to the runtime we only need to succeed the resolution attempt when we stay within the version bubble.
It also seems to me that we only need the variant form of the resolution as the only place where we're using the non-variant form in the runtime is class validity checks and we don't need to be doing that.
1b76fb3
to
be0a176
Compare
I have updated the change according to PR feedback and the SVM tests are now passing; I retried an infrastructural failure on Windows arm64 and I'm running the Crossgen2 composite tests to increase my confidence in the change. @davidwrighton / @MichalStrehovsky, can you please take another look when you have a chance? Thanks Tomas |
@trylek When I run this logic against the GenericContextTest, in non-composite mode, I'm not pleased by the results. In particular if I run crossgen2 with the --verbose switch, one can see that in non-composite mode each and every method is not compiled, with a reason such as "Direct call to abstract method 'Void IFaceGeneric`1<System.String>.GenericMethod()' not allowed" This doesn't make sense to me. We should not be failing compilation for that reason, and I believe we should instead be generating a fixup to the constrained call. When I run the crossgen2 against TypeHierarchyTest, I see that many scenarios also report with a similar failure string. In that case, we don't have a version bubble to deal with, but we're still seeing compilation being deferred to runtime. That should not be happening. |
The genericscontexttest now fails with an assertion when compiled with crossgen2 (non-composite mode). (Windows X64 Debug)
|
/// <param name="interfaceMethod">Interface method to resolve</param> | ||
/// <param name="currentType">Type to attempt virtual static method resolution on</param> | ||
/// <returns>MethodDesc of the resolved virtual static method, null when not found (runtime lookup must be used)</returns> | ||
public static MethodDesc ResolveVariantInterfaceMethodToStaticVirtualMethodOnType(MethodDesc interfaceMethod, MetadataType currentType, Func<TypeDesc, bool> inVersionBubble) |
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 inVersionBubble
parameter should probably go under #if READYTORUN
.
We don't ifdef things in the type system, but also we don't put version bubble stuff here. So maybe an ifdef is less evil.
I do wonder why we were able to get by without a bool like this for the other virtual method resolution scenarios.
} | ||
|
||
// Attempt to resolve on variance matched interface | ||
MethodDesc runtimeInterfaceMethod = TryResolveInterfaceMethodOnVariantCompatibleInterface(runtimeInterfaceType, interfaceMethod); |
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 we actually want to do this:
MethodDesc runtimeInterfaceMethod = TryResolveInterfaceMethodOnVariantCompatibleInterface(runtimeInterfaceType, interfaceMethod); | |
MethodDesc runtimeInterfaceMethod = runtimeInterfaceType.FindMethodOnExactTypeWithMatchingTypicalMethod(interfaceMethod); |
// Attempt to resolve on variance matched interface | ||
MethodDesc runtimeInterfaceMethod = TryResolveInterfaceMethodOnVariantCompatibleInterface(runtimeInterfaceType, interfaceMethod); | ||
|
||
if (runtimeInterfaceMethod != 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.
Under what condition could this be null?
return resolvedMethodOnType; | ||
} | ||
|
||
// Variant interface dispatch |
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 static virtual methods don't do a two pass variance check? (First try to find an exact match and only then do a second pass looking for variant match.)
If so could you please model these APIs the same as the existing "ResolveVariantInterfaceMethodToVirtualMethodOnType" and "ResolveInterfaceMethodToVirtualMethodOnType"? (i.e. add "ResolveInterfaceMethodToStaticVirtualMethodOnType" that "ResolveVariantInterfaceMethodToStaticVirtualMethodOnType" can call into as the first thing).
{ | ||
ThrowHelper.ThrowMissingMethodException(constrainedType, resolvedMethodImpl.Name, resolvedMethodImpl.Signature); | ||
} | ||
if (interfaceMethod.HasInstantiation || methodImpl.Body.HasInstantiation || constrainedType.HasInstantiation) |
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.
if (interfaceMethod.HasInstantiation || methodImpl.Body.HasInstantiation || constrainedType.HasInstantiation) | |
if (interfaceMethod.HasInstantiation) |
If interfaceMethod.HasInstantiation != methodImpl.Body.HasInstantiation
, terrible things will happen below, so we might as well assume that and consider the Body check redundant.
I believe InstantiateSignature is going to be a no-op if the constrained type has an instantiation because that part is already instantiated.
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 in the latest commit, thanks for the suggestion.
} | ||
if (interfaceMethod.HasInstantiation || methodImpl.Body.HasInstantiation || constrainedType.HasInstantiation) | ||
{ | ||
resolvedMethodImpl = resolvedMethodImpl.InstantiateSignature(constrainedType.Instantiation, interfaceMethod.Instantiation); |
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.
resolvedMethodImpl = resolvedMethodImpl.InstantiateSignature(constrainedType.Instantiation, interfaceMethod.Instantiation); | |
resolvedMethodImpl = resolvedMethodImpl.MakeInstantiatedMethod(interfaceMethod.Instantiation); |
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 in the latest commit, thanks for the suggestion.
{ | ||
foreach (MethodImplRecord methodImpl in mdType.FindMethodsImplWithMatchingDeclName(interfaceMethod.Name) ?? Array.Empty<MethodImplRecord>()) | ||
{ | ||
if (methodImpl.Decl == interfaceMethod) |
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'll need to compare with the interfaceMethod.GetMethodDefinition()
. The code below assumes interfaceMethod
is already instantiated if it's generic but the MethodImpl records you get from the constrained type will not be instantiated.
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 in the latest commit, thanks for explaining!
516cf46
to
1c5207a
Compare
This is my initial attempt at porting the CoreCLR runtime code for SVM resolution to Crossgen2. The TypeHierarchyTest now fails at 66-th scenario. The biggest problems I'm hitting are around making sure that we drop the constraint from the actual entrypoint import cells, otherwise we end up with constructs like BaseScenario61.Method() @ DerivedScenario61 and that crashes the runtime. I guess that my treatment of generics is most likely still incorrect. Thanks Tomas
I'm adding this as a separate commit as it's basically just a projection of David's PR suggestions without additional algorithmic changes. I'll follow up with additional fixes for the remaining test failures. Thanks Tomas
I have removed the previously added special virtual method algorithm for static virtual method resolution and instead I added logic for SVM resolution to the existing methods ResolveInterfaceMethodToVirtualMethodOnType and ResolveVariantInterfaceMethodToVirtualMethodOnType I haven't made any functional changes in this commit. Thanks Tomas
With this change, more tests seem to be passing but there seems to be a runtime problem ending up as an assertion: for instance, in TypeHierarchyTests, Scenario 25, TryResolveConstraintMethodApprox resolves the constrained call to BaseScenario25<Func<String>>.Method() which subsequently gets wrapped into an instantiating stub and later crashes GC ref map check which seems to compare the GC ref map of the wrapped method with the original import cell for the constrained method. These naturally don't match as the wrapped canonical method requires the method dictionary argument. I continue investigating this but any insight into what exactly needs fixing here would be more than welcome. Thanks Tomas
1c5207a
to
c3248ec
Compare
@trylek, is svm support in crossgen2 planned for the next release? |
@kasperk81 - yes, support for static virtual methods in Crossgen2 is part of our .NET Core 7 plans. |
@trylek, is there an ETA on this? Are we going to see failures in CI without this if we start merging PRs that utilize generic math in the BCL and marking it non-preview: #60905 (comment) ? |
@tannergooding - I have yet to double-check with Manish and David but my gut feeling is that it would be good to get this in before Preview 4; later such a change becomes too risky due to insufficient bake time as it affects |
Thanks! I just wanted to double check as this was considered blocking other issues and preventing some work from going in. We're trying to target Preview 2/3 for a lot of the other changes. |
Took the type system changes from dotnet#54063 and cleaned them up, added unit tests. Hooked it up into JitInterface/ResolveConstraintMethodApprox. Using the pre-existing `ConstrainedMethodUseLookupResult` that wasn't currently getting emitted. We'll want to use it for its original purpose at some point, but I think we can make this work for both instance and static constrained calls. Missing things: * Support creating delegates to static virtual methods. This will need a RyuJIT/JitInterface change. * Type loader support. If `MakeGeneric` needs static virtuals at runtime, it will throw. But this is enough to get HttpClient working again. Fixes dotnet#65613. Contributes to dotnet/runtimelab#1665.
Took the type system changes from #54063 and cleaned them up, added unit tests. Hooked it up into JitInterface/ResolveConstraintMethodApprox. Using the pre-existing `ConstrainedMethodUseLookupResult` that wasn't currently getting emitted. We'll want to use it for its original purpose at some point, but I think we can make this work for both instance and static constrained calls. Missing things: * Support creating delegates to static virtual methods. This will need a RyuJIT/JitInterface change. * Type loader support. If `MakeGeneric` needs static virtuals at runtime, it will throw. But this is enough to get HttpClient working again. Fixes #65613. Contributes to dotnet/runtimelab#1665.
Stale PR |
Apologies mostly to @MichalStrehovsky, I originally hoped to be able to revive this PR in order to keep all the PR feedback in place but apparently it's no longer there so I'll need to create a new one. I have rebased the change against current main and I believe I have addressed all Michal's feedback I'm aware of, I'm about to start testing the new version of this change shortly. |
If you hit this in the future, I think you can recover this by clicking "Unlock conversation" on the PR (the link is at the bottom of the right column in the Github UI) |
This is my initial attempt at porting the CoreCLR runtime code
for SVM resolution to Crossgen2. The TypeHierarchyTest now fails
at 66-th scenario. The biggest problems I'm hitting are around
making sure that we drop the constraint from the actual entrypoint
import cells, otherwise we end up with constructs like
BaseScenario61.Method() @ DerivedScenario61
and that crashes the runtime. I guess that my treatment of generics
is most likely still incorrect.
Thanks
Tomas