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

Enable devirtualization in more scenarios on crossgen2 #53567

Merged
merged 25 commits into from
Jun 11, 2021

Conversation

davidwrighton
Copy link
Member

@davidwrighton davidwrighton commented Jun 2, 2021

Address deficiencies in current devirtualization infrastructure

  • Remove the responsibility of creating a CORINFO_RESOLVED_TOKEN structure from the JIT and make it a responsibility of the VM side of the jit interface.
    • This enables the component (crossgen2) which has deeper understanding of the requirements here to correctly handle scenarios that would otherwise require expressing crossgen2 specific details across the jit interface.
  • Add a new set of fixups (READYTORUN_FIXUP_Check_VirtualFunctionOverride and READYTORUN_FIXUP_Verify_VirtualFunctionOverride) these are used to validate that the behavior of the runtime and crossgen2 compiler is equivalent for a virtual resolution event
    • READYTORUN_FIXUP_Check_VirtualFunctionOverride will ensure that the virtual resolution decision is the same at crossgen2 time and runtime, and if the decision differs, any generated code affected by the decision will not be used.
    • READYTORUN_FIXUP_Verify_VirtualFunctionOverride will perform the same checks as READYTORUN_FIXUP_Check_VirtualFunctionOverride, but if it fails the check, the process will be terminated with a fail-fast. It is intended for use under the --verify-type-and-field-layout stress mode.
    • Currently only the READYTORUN_FIXUP_Verify_VirtualFunctionOverride is actually generated, and it is only generated when using the --verify-type-and-field-layout switch to crossgen2. Future work will identify if there are scenarios where we need to generate the READYTORUN_FIXUP_Check_VirtualFunctionOverride flag. One area of possible concern is around covariant returns, another is around handling of type equivalence.
  • In order to express the fixup signature for the VirtualFunctionOverride fixups, a new flag has been added to ReadyToRunMethodSigFlags. READYTORUN_METHOD_SIG_UpdateContext will allow the method signature to internally specify the assembly which is associated with the method token, instead of relying on the ambient context.
  • R2RDump and the ReadyToRun format documentation have been updated with the details of the new fixups/flags.
  • Update the rules for handling unboxing stubs
  • Adjust the rules for when it is legal to devirtualize and maintain version resiliency
    • The VersionsWithCode and VersionsWithType rules are unnecessarily restrictive.
    • Instead Validate that the metadata is safely checkable, and rely on the canInline logic to ensure that no IL that can't be handled is inlined.
    • This also involved adding a check that the chain of types from the implementation type to the declaration method table type is within the version bubble.
    • And changing the VersionsWithType check on the implementation type, to a new VersionsWithTypeReference check which can be used to validate that the type can be referred to, in combination with using VersionsWithType on the type definition.
    • By adjusting the way that the declMethod is referred to, it becomes possible to use the declMethod without checking the full method is VersionsWithCode, and it just needs a relationship to version matching code.
  • In resolveVirtualMethod generate the CORINFO_RESOLVED_TOKEN structures for the jit
    • In particular we are now able to resolve to methods where the decl method is the resolution result but is not within the version bubble itself. This can happen if we can prove that the decl method is the only method which can possibly implement a virtual.
  • Add support for devirtualization reasons to crossgen2
  • Port all devirtualization abort conditions to crossgen2 from runtime that were not already present
  • Fix devirtualization from a canonical virtual method when the actual implementation is more exact
  • Fix variant interface override scenario where there is an interface that requires implementation of the variant interface as well as the variant interface itself.

MichalStrehovsky and others added 11 commits June 7, 2021 15:22
This is a difference between CoreCLR and the managed type system - the result of interface method resolution is going to be a canonical method on the CoreCLR side, but exact on the managed side.

So this implementation is slightly different from how it looks like on the VM side. We slightly touched on the difference in dotnet#45744 but there was more to it.
…ion override behavior

- Enable use of these fixups for devirtualization scenarios
- when --verify-type-and-field-layout is specified, the verification variant of the fixup is used
- Update documentation and r2rdump utility
…that we won't devirtualize outside of the version bubble
… ok though, as I think type forwarders can make this R2R unsafe
…mentation and instantiated tables as the owner type is sufficient for now.

- Also, set resolvedTokenDevirtualizedUnboxedMethod correctly
@davidwrighton davidwrighton force-pushed the fix_devirt_inst_param_issues branch from cced08c to e923066 Compare June 7, 2021 22:29
…pe signature generation, use it in the InstrumentationDataTableNode
…de on non-locally defined interfaces.

- decl vs impl check done on exact result, not on canonicalized result
…e are implemented in crossgen2

- Provide devirtualization details for all possible ways devirt can fail in crossgen2 (This is more detailed than the crossgen output)
@davidwrighton davidwrighton marked this pull request as ready for review June 9, 2021 15:50
@davidwrighton
Copy link
Member Author

@dotnet/crossgen-contrib

Copy link
Member

@AndyAyersMS AndyAyersMS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked over the jit changes, which generally look good, and skimmed the rest, much of which I'm not going to be able to review effectively.

This will need a new jit guid.

There will need to be some updates to superpmi's handling of resolveVirtualMethod; you can likely use the existing helpers for resolved tokens (eg SpmiRecordsHelper::StoreAgnostic_CORINFO_RESOLVED_TOKENout) Seems like you might need a novel combo of the in and out helpers though, as in this case all the fields of the resolved token are outputs...?

@@ -731,7 +731,7 @@ Compiler::fgWalkResult Compiler::fgLateDevirtualization(GenTree** pTree, fgWalkD
CORINFO_CONTEXT_HANDLE context = nullptr;
const bool isLateDevirtualization = true;
bool explicitTailCall = (call->AsCall()->gtCallMoreFlags & GTF_CALL_M_EXPLICIT_TAILCALL) != 0;
comp->impDevirtualizeCall(call, &method, &methodFlags, &context, nullptr, isLateDevirtualization,
comp->impDevirtualizeCall(call, NULL, &method, &methodFlags, &context, nullptr, isLateDevirtualization,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
comp->impDevirtualizeCall(call, NULL, &method, &methodFlags, &context, nullptr, isLateDevirtualization,
comp->impDevirtualizeCall(call, nullptr, &method, &methodFlags, &context, nullptr, isLateDevirtualization,

@@ -20683,6 +20684,7 @@ bool Compiler::IsMathIntrinsic(GenTree* tree)
// is not exactly known, and there is a plausible guess for the type.
//
void Compiler::impDevirtualizeCall(GenTreeCall* call,
CORINFO_RESOLVED_TOKEN* pResolvedToken,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment for the new arg to the block just above?

dvInfo.objClass = likelyClass;
dvInfo.context = *pContextHandle;
dvInfo.exactContext = *pContextHandle;
dvInfo.pResolvedTokenVirtualMethod = NULL;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dvInfo.pResolvedTokenVirtualMethod = NULL;
dvInfo.pResolvedTokenVirtualMethod = nullptr;

// jit interface logic after the logic that executes here.
//
// ImplType checking
// 1. At all times the metadata definition of the implmenetation type must version with the application.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 1. At all times the metadata definition of the implmenetation type must version with the application.
// 1. At all times the metadata definition of the implementation type must version with the application.

Copy link
Member

@trylek trylek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks beautiful to me, thank you!

sb.Append(":");
sb.Append(nameMangler.GetMangledTypeName(_implType));
sb.Append(":");
_implMethod.AppendMangledName(nameMangler, sb);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we apply a null check before calling _implMethod.AppendMangledName?

if (result != 0)
return result;

return _implMethod.CompareTo(otherNode._implMethod, comparer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dtto - null check on _implMethod

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, one more fun here that _implMethod and otherNode._implMethod being both null is probably a valid match.


public bool Equals(VirtualResolutionFixupSignatureFixupKey other)
{
return FixupKind == other.FixupKind && DeclMethod.Equals(other.DeclMethod) && ImplType == other.ImplType && ImplMethod.Equals(other.ImplMethod);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the null check here as well?

@@ -135,6 +135,11 @@ private void CreateNodeCaches()
);
});

_virtualFunctionOverrideCache = new NodeCache<VirtualResolutionFixupSignature, ISymbolNode>(key =>
{
return new PrecodeHelperImport(_codegenNodeFactory,key);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit - missing space after the comma

@@ -399,6 +416,93 @@ private bool ComputeTypeVersionsWithCode(TypeDesc type)
return ComputeInstantiationVersionsWithCode(type.Instantiation, type);
}

private bool ComputeTypeReferenceVersionsWithCode(TypeDesc type)
{
switch(type.Category)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use type.IsPrimitive here like we do below in ComputeInstantiationVersionsWithCode? In fact, if this smallest set of "absolutely non-versionable types" means the same thing in both methods, could we consider making a helper method for it, something like IsPrimitiveObjectOrString?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not quite the same as ComputeInstantiationVersionsWithCode. This one includes Void, but I've used the IsPrimitive and IsVoid predicates, and added a comment.

@@ -247,6 +247,24 @@ public void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb)
sb.Append("; UNBOXING");
}

public override string ToString()
{
StringBuilder debuggingName = new StringBuilder();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we implement ToString by just calling AppendMangledName on a local Utf8StringBuilder and returning its ToString - i.o.w. is there any fundamental reason for the duplication?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not quite the same, in particular, ToString() doesn't have access to a NameMangler.

if (fixupType == ReadyToRunFixupKind.Check_TypeLayout)
builder.Append(" (Check_VirtualFunctionOverride)");
else
builder.Append(" (Verify_VirtualFunctionOverride)");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit - today R2RDump uses the "macro" casing for fixup type names. I don't have a strong opinion about this, perhaps it's better to stick with the constant names but at some point we should probably convert the rest to be consistent.

}

SString fatalErrorString;
fatalErrorString.Printf(W("Verify_VirtualFunctionOverride Decl Method '%s' on type '%s' is '%s'(actual) instead of expected '%s'(from compiler)"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beautiful, thank you for providing the rich diagnostic info upon failure, that's super useful for analyzing lab failures!

uint32_t updatedModuleIndex;
IfFailThrow(sig.GetData(&updatedModuleIndex));
#ifdef FEATURE_MULTICOREJIT
if (pZapSigContext->externalTokens == ZapSig::MulticoreJitTokens)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for my education - what is this used for? Is that something we should take into account in Crossgen2 itself?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MulticoreJitTokens is used for the MulticoreJit feature, which is an odd profile guided optimization feature that works on a local machine. We don't need to worry about it in crossgen2.

public int _v1;
public int _v2;

public GenericClass()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can find the original issue but it would be extremely helpful if you could add at least a three-line comment describing the way the test is supposed to work and how it detects bad behavior - in my recent work on fixing the various regression tests with Crossgen2 I've always had to spend quite a bit of time poring over the particular test to understand what it's supposed to be doing especially when it's exercising some subtle behavioral differences.

- Fix variant interface dispatch bug where the variant interface is implemented by another interface associated with the type.
- Handle default interface methods
  - Except for variant dispatch, detect those, and fail the devirtualization
{
/// <summary>
/// Gets the method whose body this <see cref="MethodIL"/> represents.
/// Gets the method whose body this <see cref=""/> represents.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Gets the method whose body this <see cref=""/> represents.
/// Gets the method whose body this <see cref="MethodILScope"/> represents.

using Internal.TypeSystem;

using CORINFO_DEVIRTUALIZATION_DETAIL = Internal.JitInterface.CORINFO_DEVIRTUALIZATION_DETAIL;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically a layering violation since DevirtualizationManager is the general purpose component that can be used with different codegen backends.

But I don't know if I really want to ask you to create a separate enum.

Fortunately this will not be a build break in NativeAOT because we already violated the layering (the ObjWriter uses JitInterface constants for stuff).

@@ -53,16 +54,16 @@ public virtual bool IsEffectivelySealed(MethodDesc method)
/// Note that if <paramref name="implType"/> is a value type, the result of the resolution
/// might have to be treated as an unboxing thunk by the caller.
/// </remarks>
public MethodDesc ResolveVirtualMethod(MethodDesc declMethod, TypeDesc implType)
public MethodDesc ResolveVirtualMethod(MethodDesc declMethod, TypeDesc implType, ref CORINFO_DEVIRTUALIZATION_DETAIL devirtualizationDetail)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the new parameter be out instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Turns out not doing this was masking a bug in the devirtualilzation algorithm

{
// If we devirtualized into a default interface method on a generic type, we should actually return an
// instantiating stub but this is not happening.
// Making this work is tracked by https://github.com/dotnet/runtime/issues/9588
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will potentially be a problem because we don't have instantiating stubs in the general type system, same as we don't have unboxing stubs.

We have JitInterface-local unboxing stubs, and I think that will be the solution we'll want to use for instantiating stubs as well, but we will not have access to those from here (we're not in JitInterface).

I don't have a solution, just pointing out if you have thoughts in that area.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, its the same problem as devirtualizing to a struct method, which is something that @AndyAyersMS has actually made work fairly recently. I suspect that the comment is somewhat out of date, but I felt it was useful to have a matching comments in both crossgen2 and coreclr vm.

result.tokenContext = jitInterface.contextFromMethod(method);
result.token = methodWithToken.Token.Token;
if (methodWithToken.Token.TokenType != CorTokenType.mdtMethodDef)
throw new NotSupportedException();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will tear down the compiler process. Do we want that to happen? Consider RequiresRuntimeJitException (not fatal) or CodeGenerationFailedException (that one can be fatal if we're not in the resilient mode).

@@ -11,7 +11,10 @@

namespace Internal.IL
{
public sealed partial class EcmaMethodIL : MethodIL
// Marker interface implemented by EcmaMethodIL and EcmaMethodILScope
public interface IEcmaMethodIL { }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: might be faster to just do the two is checks instead of testing for the interface. Both EcmaMethodIL and EcmaMethodILScope are sealed so an is is quick.

…anonicallly compatible vtable devirtualization, and go back to normal yml for the crossgen2 outerloop
@davidwrighton davidwrighton merged commit d07f911 into dotnet:main Jun 11, 2021
BruceForstall added a commit to BruceForstall/runtime that referenced this pull request Jun 17, 2021
Change dotnet#53567 introduced new functionality to the JIT-EE
resolveVirtualMethod() API. This fixes the SuperPMI implementation
in the case where the API returns `false`, where some data fields
are not initialized.

Fixes crossgen2 SuperPMI collections.

Fixes dotnet#54310
BruceForstall added a commit that referenced this pull request Jun 17, 2021
Change #53567 introduced new functionality to the JIT-EE
resolveVirtualMethod() API. This fixes the SuperPMI implementation
in the case where the API returns `false`, where some data fields
are not initialized.

Fixes crossgen2 SuperPMI collections.

Fixes #54310
@ghost ghost locked as resolved and limited conversation to collaborators Jul 11, 2021
@davidwrighton davidwrighton deleted the fix_devirt_inst_param_issues branch April 13, 2023 18:47
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants