-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Improve performance of interface method resolution in ILC #103066
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
Conversation
Presumably the last interfaces on the list are more likely to be coming from the less nested classes and allow short circuiting the loop earlier.
When ResolveInterfaceMethodToVirtualMethodOnType is recursively calling itself, use the result instead of returning `null` and then continuing the outer loop that is effectively doing the same recursion on base types.
17707f6
to
9b20327
Compare
@dotnet/ilc-contrib anyone? |
Like I wrote in #103034 (comment). One change is straightforward and I can easily sign off on it. For the other one you'd need @davidwrighton to look at it. If you want to get traction on this, I would suggest splitting this PR. |
I can split it... but arguably the other change is just as trivial. The added parameter |
@@ -729,7 +729,7 @@ private static MethodDesc ResolveInterfaceMethodToVirtualMethodOnTypeRecursive(M | |||
return null; | |||
} | |||
|
|||
MethodDesc currentTypeInterfaceResolution = ResolveInterfaceMethodToVirtualMethodOnType(interfaceMethod, currentType); | |||
MethodDesc currentTypeInterfaceResolution = ResolveInterfaceMethodToVirtualMethodOnType(interfaceMethod, currentType, returnRecursive: true); | |||
if (currentTypeInterfaceResolution != 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.
When can this be null now?
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.
There's still one branch that can return null
in theory:
runtime/src/coreclr/tools/Common/TypeSystem/Common/MetadataVirtualMethodAlgorithm.cs
Line 659 in 9b20327
return null; |
I don't think it can happen in practice since it's likely impossible code path in the grand scheme of things but I didn't feel safe enough to reason about 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.
And if it does we go back through the loop with the base type… but didn’t we already do that in the first call? Is there a way this could return anything except 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.
And if it does we go back through the loop with the base type… but didn’t we already do that in the first call?
That's precisely what this PR fixes. If we call ResolveInterfaceMethodToVirtualMethodOnType
recursively from inside ResolveInterfaceMethodToVirtualMethodOnTypeRecursive
we just return and use the result instead of throwing it away (ie. returning null
) and then going to the base type (next step of the outer loop) and computing the exact same thing again.
--
There are three cases when ResolveInterfaceMethodToVirtualMethodOnType
returns null
:
if (currentType.IsInterface)
if (!IsInterfaceImplementedOnType(currentType, interfaceType))
MethodDesc baseClassImplementationOfInterfaceMethod = ResolveInterfaceMethodToVirtualMethodOnTypeRecursive(interfaceMethod, baseType);
Presumably, 1. can only happen once and will never walk the type hierarchy through the base type. I didn't study the case 2. in detail but it should not matter, worst case it ends up doing the same thing it did now. Case 3. is the one I am trying to optimize.
If we are already in the ResolveInterfaceMethodToVirtualMethodOnTypeRecursive
loop then returning null
at 3. would continue the loop and proceed to base type. That's exactly the same thing we just computed by calling ResolveInterfaceMethodToVirtualMethodOnTypeRecursive(interfaceMethod, baseType);
though, so instead of returning null
in the case (and specifically only in this case, hence the added parameter) just return the computed value and short-circuit the outer loop.
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.
Or, put another way, the returnRecursive: true
indicates that if x = ResolveInterfaceMethodToVirtualMethodOnTypeRecursive(interfaceMethod, baseType);
is computed AND x is not null
then it changes the behavior to return x;
instead of return null
.
Previously it returned null
, the loop proceeded to set currentType = currentType.MetadataBaseType;
and continuing the loop would be equivalent to executing the recursive form return ResolveInterfaceMethodToVirtualMethodOnTypeRecursive(interfaceMethod, currentType.MetadataBaseType);
.
The observation is that we just computed it and threw it away... so let's not throw it away.
@@ -729,7 +729,7 @@ private static MethodDesc ResolveInterfaceMethodToVirtualMethodOnTypeRecursive(M | |||
return null; | |||
} | |||
|
|||
MethodDesc currentTypeInterfaceResolution = ResolveInterfaceMethodToVirtualMethodOnType(interfaceMethod, currentType); | |||
MethodDesc currentTypeInterfaceResolution = ResolveInterfaceMethodToVirtualMethodOnType(interfaceMethod, currentType, returnRecursive: true); | |||
if (currentTypeInterfaceResolution != 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.
And if it does we go back through the loop with the base type… but didn’t we already do that in the first call? Is there a way this could return anything except null?
@@ -89,7 +89,7 @@ public static bool MightHaveInterfaceDispatchMap(TypeDesc type, NodeFactory fact | |||
|
|||
DefType declType = type.GetClosestDefType(); | |||
|
|||
for (int interfaceIndex = 0; interfaceIndex < declType.RuntimeInterfaces.Length; interfaceIndex++) | |||
for (int interfaceIndex = declType.RuntimeInterfaces.Length - 1; interfaceIndex >= 0; interfaceIndex--) |
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.
Since this method is only returning a Boolean I think this is equivalent, but could you ensure that there’s no possible condition in which this could change the result? I think what I want to know is the invariants that need to hold for the same behavior to occur. Something like: the search returns true if any of the items are true, and checks on earlier items don’t affect the checks on later items (each check is independent) so this effectively forms a Contains call that is order invariant. But I don’t if that’s true (the code/type system is complex).
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 this is quite trivial to reason about. There's no return false
or break
inside the loop. All the called methods are deterministic/idempotent, and there's no local state that is modified in the loop. Hence, the only thing that can short circuit the loop is one of the return true
calls. That cannot affect the returned result, it can only happen sooner or later (and the assumption of the optimization is that it's more likely to hit the condition sooner and avoid extra work).
@jkotas @davidwrighton any opinions on the riskiness of this change? I'm inclined to take it for .NET 10. I'd appreciate your thoughts. |
/azp run runtime-nativeaot-outerloop |
Azure Pipelines successfully started running 1 pipeline(s). |
Same opinion as what @MichalStrehovsky said above - @davidwrighton needs to review the second change. |
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.
@agocke thank you for assigning this to me, and sorry for the delay @filipnavara. I've spent most of the fall dealing with a Microsoft internal issue. Thankfully, this change isn't in the brain-melting complicated part of virtual resolution, and I believe this is correct, and is probably a nice performance optimization that should improve things nicely for all sorts of customers.
However, I'd like a comment to describe the use fo the returnRecursive switch, describing the conditions under which it is used, which makes it clear that it is only for use when being called from something like ResolveInterfaceMethodToVirtualMethodOnTypeRecursive
I'll try to add the comment tomorrow. I need to refresh my memory a bit and think about how to word it. |
* main: (89 commits) Add Dispose for X509Chain instance (dotnet#110740) Fix XML comment on regex split enumerator (dotnet#111572) JIT: tolerate missing InitClass map in SPMI (dotnet#111555) Build ilasm/ildasm packages for the host machine (dotnet#111512) Unicode 16.0 Support (dotnet#111469) Improve performance of interface method resolution in ILC (dotnet#103066) Fix building the host-targeting components and packing ILC (dotnet#111552) Improve JSON validation perf (dotnet#111332) Update github-merge-flow.jsonc to autoflow 9.0 to 9.0-staging (dotnet#111549) Include GPL-3 licence text in the notice (dotnet#111528) Remove explicit __compact_unwind entries from x64 assembler (dotnet#111530) Add MemoryExtensions overloads with comparer (dotnet#110197) Avoid capturing the ExecutionContext for the whole HTTP connection lifetime (dotnet#111475) Forward DefaultArtifactVisibility down from the VMR orchestrator (dotnet#111513) Fix relocs errors on riscv64 (dotnet#111317) Added JITDUMP_USE_ARCH_TIMESTAMP support. (dotnet#111359) add rcl/rcr tp and latency info (dotnet#111442) Fix stack overflow in compiler-generated state (dotnet#109207) Produce a package with the host-running ILC for repos in the VMR (dotnet#111443) Delete dead code in ilasm PE writer (dotnet#111218) ...
Fixes #103034
Presumably the last interfaces on the list are more likely to be coming from the less nested classes and allow short circuiting the loop earlier.
When ResolveInterfaceMethodToVirtualMethodOnType is recursively calling itself, use the result instead of returning
null
and then continuing the outer loop that is effectively doing the same recursion on base types.