-
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
Avoid scanning typeof checks when building whole program view #103883
Conversation
Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas |
/azp run runtime-nativeaot-outerloop |
Azure Pipelines successfully started running 1 pipeline(s). |
I wish all test failures were like this:
|
7b382cd
to
0ea6c49
Compare
0ea6c49
to
59287d1
Compare
Before this PR, we were somewhat able to eliminate dead typeof checks such as: ```csharp if (someType == typeof(Foo) { ExpensiveMethod(); } ``` This work was done in dotnet#102248. However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent. With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them. With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
59287d1
to
6ec1308
Compare
/azp run runtime-nativeaot-outerloop |
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.
This node represents a concrete instantiation of a method that has a shared method body. We only look at the method body once, but take note of all dependencies that are per-instantiation.
For example, when we compile static void Foo<T>() => typeof(T)
, the method body of Foo<__Canon>
would say "I also depend on the MethodTable
of T____Canon
after substitution. If this compilation also contain a generic dictionary for Foo<Atom>
(where Atom
is a reference type), this is the node that would say "please generate a MethodTable
for Atom
- we do that in the existing GetStaticDependencies
.
What this node was missing was reporting of conditional dependencies - if the canonical method body conditionally depends on something, we need to do the same thing as GetStaticDependencies
, but also replicate the condition from the canonical method.
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 this node was missing was reporting of conditional dependencies
Could you elaborate slightly? When you say "conditional dependency" here are you thinking of dependencies literally in conditions? That is, given the following code
void M<T>()
{
if (typeof(T) == typeof(int)) { ... }
}
There is nominally a "conditional" dependency in M
st., if T
is instantiated as int
, that block is necessary. Otherwise, that block is dead code.
Is that what this change handles?
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, you said that "This node represents a concrete instantiation of a method that has a shared method body.", meaning that this node is only present for reference types, right?
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, you said that "This node represents a concrete instantiation of a method that has a shared method body.", meaning that this node is only present for reference types, right?
Yes, this is only for instantiations over reference types.
Let's say we have:
class Foo<T>
{
public Type Blah() => typeof(T);
public Type Blah2() => typeof(T[,,,]);
}
// And we do:
new Foo<object>().Blah();
- Canonical method
Blah
depends on e.g. T__Canon (for thetypeof(T)
). This by itself is rather useless for the dependency analysis system. - We don't know what
Blah2
depends on since it was not called and got "trimmed". Foo<object>
MethodTable conditionally depends onShadowConcreteMethod
(the node we're discussing) ofFoo<object>.Blah
andFoo<object>.Blah2
, the condition is the presence of the canonical method body (so for the former, the condition is satisfied, for the latter it isn't since the canonical body was not looked at).- When evaluating the dependencies of the
ShadowConcreteMethod
we look at the dependencies of the canonical method and specialize as necessary
The extension in this PR is to ensure we also look at the conditional dependencies of the canonical method, not just the unconditional ones.
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.
Could you give an example of a conditional dependency of ShadowConcreteMethod
?
@@ -301,12 +301,17 @@ private void ImportBasicBlocks() | |||
} | |||
|
|||
private void MarkBasicBlock(BasicBlock basicBlock) | |||
{ | |||
MarkBasicBlock(basicBlock, ref _pendingBasicBlocks); |
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'm adding the notion of additional lists that we should look at. This helps in the conditional basic block scanning because it lets us defer looking at conditional blocks until after we scanned all the unconditional codepaths. The conditional scanning is rather simplistic and for:
static int Method()
{
if (Foo() == typeof(Bla))
Expensive();
return 0;
}
We'd see the Expensive
block is conditioned, then we'd make the return 0
conditional as well because the control falls through, and then we'd find out that return 0
is also reachable as a fallthrough without the condition and undo all the conditions (we don't keep track of edges and directions, just the condition).
Looking at conditional blocks last lets us avoid the rollback (see logic in ILImporter.Scanner.cs).
This is now ready for review. Cc @dotnet/ilc-contrib Fundamentally this has two parts:
Nice savings for WinRT components and simple app that uses reflection (probably representative of e.g. cdacreader): Size statisticsPull request #103883
|
Great improvements! 😄
should we be expecting more based on:
|
@@ -28,7 +30,7 @@ internal partial class ILImporter | |||
|
|||
private readonly MethodDesc _canonMethod; | |||
|
|||
private DependencyList _dependencies = new DependencyList(); | |||
private DependencyList _unconditionalDependencies = new DependencyList(); |
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.
private DependencyList _unconditionalDependencies = new DependencyList(); | |
private readonly DependencyList _unconditionalDependencies = new DependencyList(); |
@@ -172,9 +182,21 @@ public DependencyList Import() | |||
FindBasicBlocks(); | |||
ImportBasicBlocks(); | |||
|
|||
CodeBasedDependencyAlgorithm.AddDependenciesDueToMethodCodePresence(ref _dependencies, _factory, _canonMethod, _canonMethodIL); | |||
CombinedDependencyList conditionalDependencies = null; | |||
foreach (BasicBlock bb in _basicBlocks) |
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.
Why can there be null
blocks in this list?
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 array is indexed into by the IL offset. The entries are non-null at basic block start locations and null elsewhere.
@sbomer could you have a look at this please? We could also go over this on a teams call. |
@sbomer could you have a look at this please? We could also go over this on a teams call. |
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.
Sorry I missed the first ping. A couple questions but LGTM!
Debug.Assert(canonDep.OtherReasonNode is not INodeWithRuntimeDeterminedDependencies); | ||
|
||
var node = canonDep.Node; | ||
if (node is INodeWithRuntimeDeterminedDependencies runtimeDeterminedNode) |
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 I didn't miss any cases, this can only be ReadyToRunGenericHelperNode, MakeGenericMethodSite, or MakeGenericTypeSite. How does this work for methods on generic types that are instantiated directly rather than through MakeGenericType?
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 #103883 (comment) explains it, but skips a small step. I wrote "Canonical method Blah
depends on e.g. T__Canon (for the typeof(T)
).", but the actual dependency is: "Canonical method Blah
depends on ReadyToRunGenericHelperNode of T__Canon."
So this is how directly instantiated things are tracked. The remaining MakeGenericMethodSite and MakeGenericTypeSite are relatively recent additions from #99037 and are only used in relation to dataflow since the problem is similar (we compute something on a "lesser instantiated thing" and need to specialize it for whatever else is in the dependency graph).
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.
Thanks!
} | ||
} | ||
|
||
private void ImportFallthrough(BasicBlock next, object condition = 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.
nit: to me "fallthrough" means "the case where the condition wasn't satisfied (or there was no condition)", so I'd expect condition to always be null. Maybe add a separate helper that accepts a condition? I see the existing code uses ImportFallthrough for branch targets too, so feel free to leave as-is if you prefer.
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.
Yes, it's weird, it's not my naming, but I don't have a better name, so I'll not churn CI for this.
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.
Could you give an example of a conditional dependency of ShadowConcreteMethod
?
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
This work was done in #102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at
ExpensiveMethod
and whatever damage this caused to the whole program view was permanent.With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning
ExpensiveMethod
and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.