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

Split generic virtual method slot use and impl tracking #82222

Merged
merged 3 commits into from
Feb 27, 2023

Conversation

MichalStrehovsky
Copy link
Member

I'm looking at generic virtual method again because of #80602.

The analysis of generic virtual methods within the compiler is an N * M algorithm where N is the number of unique generic virtual method instantiations called and M is the number of types that implement generic virtual methods.

We use dynamic dependencies within the dependency analysis engine to model this relationship.

It is important to try to limit the N and M. Looking at things, I realized the N we're currently operating on is bigger than it needs to be:

Foo f = new Bar();
f.Blah<int>();
f = new Baz();
f.Blah<double>();

class Foo { public virtual void Blah<T>() { } }
class Bar : Foo { public override void Blah<T> { } }
class Baz : Foo { public override void Blah<T> { } }

Previously, the analysis would see M = 3 and N = 6 because we would track each of the overrides as something that needs to be considered for each M. This changes the analysis to only look at the definition of the slot, i.e. N = 2 (one for int, other for double).

The result of the analysis will still be same, it will just take less time. The new GenericVirtualMethodImpl node responds false to HasDynamicDependencies and doesn't participate in expensive activities.

Cc @dotnet/ilc-contrib

I'm looking at generic virtual method again because of dotnet#80602.

The analysis of generic virtual methods within the compiler is an N * M algorithm where N is the number of unique generic virtual method instantiations called and M is the number of types that implement generic virtual methods.

We use dynamic dependencies within the dependency analysis engine to model this relationship.

It is important to try to limit the N and M. Looking at things, I realized the N we're currently operating on is bigger than it needs to be:

```csharp
Foo f = new Bar();
f.Blah<int>();
f = new Baz();
f.Blah<double>();

class Foo { public virtual void Blah<T>() { } }
class Bar : Foo { public override void Blah<T> { } }
class Baz : Foo { public override void Blah<T> { } }
```

Previously, the analysis would see M = 3 and N = 6 because we would track each of the overrides as something that needs to be considered for each M. This changes the analysis to only look at the definition of the slot, i.e. N = 2 (one for int, other for double).

The result of the analysis will still be same, it will just take less time. The new GenericVirtualMethodImpl node responds false to HasDynamicDependencies and doesn't participate in expensive activities.
@ghost
Copy link

ghost commented Feb 16, 2023

Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas
See info in area-owners.md if you want to be subscribed.

Issue Details

I'm looking at generic virtual method again because of #80602.

The analysis of generic virtual methods within the compiler is an N * M algorithm where N is the number of unique generic virtual method instantiations called and M is the number of types that implement generic virtual methods.

We use dynamic dependencies within the dependency analysis engine to model this relationship.

It is important to try to limit the N and M. Looking at things, I realized the N we're currently operating on is bigger than it needs to be:

Foo f = new Bar();
f.Blah<int>();
f = new Baz();
f.Blah<double>();

class Foo { public virtual void Blah<T>() { } }
class Bar : Foo { public override void Blah<T> { } }
class Baz : Foo { public override void Blah<T> { } }

Previously, the analysis would see M = 3 and N = 6 because we would track each of the overrides as something that needs to be considered for each M. This changes the analysis to only look at the definition of the slot, i.e. N = 2 (one for int, other for double).

The result of the analysis will still be same, it will just take less time. The new GenericVirtualMethodImpl node responds false to HasDynamicDependencies and doesn't participate in expensive activities.

Cc @dotnet/ilc-contrib

Author: MichalStrehovsky
Assignees: -
Labels:

area-NativeAOT-coreclr

Milestone: -

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-extra-platforms

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-extra-platforms

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Member

@agocke agocke left a comment

Choose a reason for hiding this comment

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

Added some questions, because I'm not quite sure what this PR is doing.


// This is either a generic virtual method or a MethodImpl for a static interface method.
// We can't test for static MethodImpl so at least sanity check it's static and noninterface.
Debug.Assert(method.IsVirtual || (method.Signature.IsStatic && !method.OwningType.IsInterface));
Copy link
Member

Choose a reason for hiding this comment

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

What about default interface implementations? In that case, isn't the method owned by an interface?

Copy link
Member Author

Choose a reason for hiding this comment

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

This will change with #80602 because we'll run into the assert. We don't currently track interfaces because they're assumed to be associated with a class (default implementations just show up on the owning class). This works for instance methods. It doesn't work for statics. It's a bit of a overhaul which is why I didn't want to do it in one go.

@@ -10,6 +10,8 @@
namespace ILCompiler.DependencyAnalysis
{
/// <summary>
/// Represents a use of a generic virtual method slot. This node only tracks
Copy link
Member

Choose a reason for hiding this comment

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

It's not quite clear to me what a "slot" means in this context.

Copy link
Member Author

Choose a reason for hiding this comment

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

Slot in the sense of ECMA-335 - either a newslot method, or virtual that doesn't have another virtual method with the same name/sig in the inheritance hierarchy.

_method.OwningType.Instantiation.CheckValidInstantiationArguments() &&
_method.CheckConstraints());

if (validInstantiation)
Copy link
Member

Choose a reason for hiding this comment

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

By "valid" here, do we mean that the code is not broken? That the compiler hasn't written incorrect IL?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a cargo cult I brought over from the previous spot. I don't know when it kicks in. It goes back all the way to dotnet/corert#2521.


if (validInstantiation)
{
if (factory.TypeSystemContext.SupportsUniversalCanon && _method.IsGenericDepthGreaterThan(UniversalCanonGVMDepthHeuristic_CanonDepth))
Copy link
Member

Choose a reason for hiding this comment

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

I don't think I understand the "generic depth" thing here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy paste from the previous spot - if universal shared generics are supported, we cut off analysis early if it gets too deep and leave universal shared code to handle it. SupportsUniversalCanon is always false in NativeAOT.

if (_method.HasInstantiation)
{
dependencies.Add(factory.GVMDependencies(_method.GetCanonMethodTarget(CanonicalFormKind.Specific)), "GVM callable reflectable method");
// FindSlotDefiningMethod might uninstantiate. We might want to fix the method not to do that.
if (slotDefiningMethod.IsMethodDefinition)
Copy link
Member

Choose a reason for hiding this comment

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

I definitely don't understand what "uninstantiate" means here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Might remove the instantiation of the generic method. Sometimes. I think we just don't call this method with generic methods. I didn't want to fix this here because we call this method in two dozen places and it would require carefully reviewing each. So we just instantiate the method back if it became uninstantiated.


public override IEnumerable<CombinedDependencyListEntry> GetConditionalStaticDependencies(NodeFactory context) => null;

public override bool HasDynamicDependencies => false;
Copy link
Member

Choose a reason for hiding this comment

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

It seems like this is important, but it's not clear to me why.

Copy link
Member Author

Choose a reason for hiding this comment

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

All nodes have HasDynamicDependencies false, except for GVMDependencies node.

Returning true means the dependency analysis will call into SearchDynamicDependencies after every iteration of the graph expansion with a list of all nodes we've seen so far.

This node not reporting it is the optimization.

Copy link
Member

Choose a reason for hiding this comment

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

And why is this a valid optimization? Or, why is it necessary for GVMDependencies to always return true?

Copy link
Member Author

Choose a reason for hiding this comment

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

GVMDependencies needs to look at each type in the graph that has something to do with generic virtual methods to check whether the type provides an override of the slot. If so, it needs to instantiate the override with whatever generic method instantiation this GVMDependencies has.

But we don't need to do this for generic virtual method implementations that don't define a new slot (i.e. they're just overrides) - this is what the optimization tries to split - a slot use + an override ("implementation", since we also introduce this node for non abstract new slots) of the slot.

Copy link
Member

@agocke agocke left a comment

Choose a reason for hiding this comment

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

LGTM, I think I understand this code more now. Just a few follow-up questions


public override IEnumerable<CombinedDependencyListEntry> GetConditionalStaticDependencies(NodeFactory context) => null;

public override bool HasDynamicDependencies => false;
Copy link
Member

Choose a reason for hiding this comment

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

And why is this a valid optimization? Or, why is it necessary for GVMDependencies to always return true?

@MichalStrehovsky MichalStrehovsky merged commit 443c42b into dotnet:main Feb 27, 2023
@MichalStrehovsky MichalStrehovsky deleted the splitgvm branch February 27, 2023 23:49
@ghost ghost locked as resolved and limited conversation to collaborators Mar 30, 2023
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.

2 participants