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

Avoid scanning typeof checks when building whole program view #103883

Merged
merged 2 commits into from
Jul 18, 2024

Conversation

MichalStrehovsky
Copy link
Member

Before this PR, we were somewhat able to eliminate dead typeof checks such as:

if (someType == typeof(Foo)
{
    ExpensiveMethod();
}

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.

Copy link
Contributor

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

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@MichalStrehovsky
Copy link
Member Author

I wish all test failures were like this:

      ****************************************************
      * Size test                                        *
      * Size of the executable is   1,262 kB             *
      ****************************************************
      BUG: File size is not in the expected range (1331200 to 1945600 bytes). Did a libraries change regress size of Hello World?

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.
@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

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 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.

Copy link
Member

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?

Copy link
Member

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?

Copy link
Member Author

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 the typeof(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 on ShadowConcreteMethod (the node we're discussing) of Foo<object>.Blah and Foo<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.

Copy link
Member

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);
Copy link
Member Author

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).

@MichalStrehovsky MichalStrehovsky marked this pull request as ready for review July 1, 2024 12:10
@MichalStrehovsky
Copy link
Member Author

This is now ready for review. Cc @dotnet/ilc-contrib

Fundamentally this has two parts:

  • Infrastructure to support generating conditional dependencies when building whole program view (ability to say a part of method only depends on something if something else is in the whole program view graph).
  • Leverage Extract shared IL pattern analysis to a class #103701 and report conditional dependencies whenever a basic block is guarded by a typeof check.

Nice savings for WinRT components and simple app that uses reflection (probably representative of e.g. cdacreader):

Size statistics

Pull request #103883

Project Size before Size after Difference
avalonia.app-linux 24597280 24556288 -40992
avalonia.app-windows 22124544 22089728 -34816
hello-linux 1348320 1348288 -32
hello-minimal-linux 1077880 1077848 -32
hello-minimal-windows 858624 854016 -4608
hello-windows 1104384 1099776 -4608
kestrel-minimal-linux 5578976 5570784 -8192
kestrel-minimal-windows 4924416 4923904 -512
reflection-linux 2228112 2071840 -156272
reflection-windows 1883648 1753088 -130560
webapiaot-linux 10225648 10225632 -16
webapiaot-windows 9160192 9160192 0
winrt-component-full-windows 5532160 5508608 -23552
winrt-component-minimal-windows 1835008 1747968 -87040

@am11
Copy link
Member

am11 commented Jul 1, 2024

Great improvements! 😄

hello-linux 1348320 1348288 -32

should we be expecting more based on:

      ****************************************************
      * Size test                                        *
      * Size of the executable is   1,262 kB             *
      ****************************************************
      BUG: File size is not in the expected range (1331200 to 1945600 bytes). Did a libraries change regress size of Hello World?

@@ -28,7 +30,7 @@ internal partial class ILImporter

private readonly MethodDesc _canonMethod;

private DependencyList _dependencies = new DependencyList();
private DependencyList _unconditionalDependencies = new DependencyList();
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
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)
Copy link
Member

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?

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 array is indexed into by the IL offset. The entries are non-null at basic block start locations and null elsewhere.

@MichalStrehovsky
Copy link
Member Author

@sbomer could you have a look at this please? We could also go over this on a teams call.

@MichalStrehovsky
Copy link
Member Author

@sbomer could you have a look at this please? We could also go over this on a teams call.

Copy link
Member

@sbomer sbomer left a 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)
Copy link
Member

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?

Copy link
Member Author

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).

Copy link
Member

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)
Copy link
Member

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.

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, it's weird, it's not my naming, but I don't have a better name, so I'll not churn CI for this.

Copy link
Member

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?

@MichalStrehovsky MichalStrehovsky merged commit 9c7ee97 into dotnet:main Jul 18, 2024
84 of 93 checks passed
@MichalStrehovsky MichalStrehovsky deleted the scantypeof branch July 18, 2024 14:51
@github-actions github-actions bot locked and limited conversation to collaborators Aug 18, 2024
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