-
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
Split generic virtual method slot use and impl tracking #82222
Conversation
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.
Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas Issue DetailsI'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 Cc @dotnet/ilc-contrib
|
/azp run runtime-extra-platforms |
Azure Pipelines successfully started running 1 pipeline(s). |
/azp run runtime-extra-platforms |
Azure Pipelines successfully started running 1 pipeline(s). |
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.
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)); |
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.
What about default interface implementations? In that case, isn't the method owned by an interface?
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 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 |
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 not quite clear to me what a "slot" means in this context.
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.
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) |
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.
By "valid" here, do we mean that the code is not broken? That the compiler hasn't written incorrect IL?
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 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)) |
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 don't think I understand the "generic depth" thing 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.
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) |
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 definitely don't understand what "uninstantiate" means 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.
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; |
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 seems like this is important, but it's not clear to me why.
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.
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.
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.
And why is this a valid optimization? Or, why is it necessary for GVMDependencies to always return 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.
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.
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.
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; |
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.
And why is this a valid optimization? Or, why is it necessary for GVMDependencies to always return true?
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:
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 toHasDynamicDependencies
and doesn't participate in expensive activities.Cc @dotnet/ilc-contrib