Skip to content

Conversation

MichalStrehovsky
Copy link
Member

@MichalStrehovsky MichalStrehovsky commented Jul 10, 2025

Fixes #82384

The template type loader is a limited type loader that we use in native AOT to support scenarios such as MakeGenericType. We keep around extra MethodTables and metadata to be able to construct new type instantiations (e.g. List<string>) at runtime from template instantiations (e.g. List<__Canon>) we made at compile time.

The template instantiation is a MethodTable like any other that we make a copy of and patch up with the help of the metadata ("native layout metadata"). Patching up involves e.g. building interface list (List<string> should have IList<string> in the interface list). This patching up may involve loading more new types from templates (e.g. the mentioned IList<string> that needs to be loadable from a IList<__Canon> template MethodTable).

The job of the compiler is to figure out all the templates we might need (recursively) to build a type. This is done in places using a rather non-exact TemplateConstructableTypes call that just decomposes the type and makes templates for everything. Some of these might not be actually needed.

This is an attempt to somewhat limit it.

Cc @dotnet/ilc-contrib

@Sergio0694
Copy link
Contributor

Out of curiosity, will all of this be trimmed out if you don't actually use MakeGenericType? I'd ask for a feature switch but I imagine you probably wouldn't like that, which is fair. What I'm thinking is: in CsWinRT 3.0 we're going out of our way to avoid doing MakeGenericType anywhere. If you have an app that never calls it (but still uses some reflection, like to get attributes), would all of this template type loader infrastructure be trimmed? Does it save a noticeable amount of size?

@MichalStrehovsky
Copy link
Member Author

Out of curiosity, will all of this be trimmed out if you don't actually use MakeGenericType?

We generate this whenever method on a generic type/generic method is a target of reflection due to our rule that says MakeGenericType/MakeGenericMethod will always work with reference types. We could postpone until we see a call to this API, but I don't know if it would help in practice because of how widespread the use of this API is. The key is not to make the compiler think the method/field is target of reflection, then you don't have to pay for this (and more).

This is also used to support generic virtual and interface methods. The other key to get rid of this data is not to use generic virtual methods.

@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

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

The template type loader is a limited type loader that we use in native AOT to support scenarios such as `MakeGenericType`. We keep around extra MethodTables and metadata to be able to construct new type instantiations (e.g. `List<string>`) at runtime from template instantiations (e.g. `List<__Canon>`) we made at compile time.

The template instantiation is a `MethodTable` like any other that we make a copy of and patch up with the help of the metadata ("native layout metadata"). Patching up involves e.g. building interface list (`List<string>` should have `IList<string>` in the interface list). This patching up may involve loading more new types from templates (e.g. the mentioned `IList<string>` that needs to be loadable from a `IList<__Canon>` template MethodTable).

The job of the compiler is to figure out all the templates we might need (recursively) to build a type. This is done in places using a rather non-exact `TemplateConstructableTypes` call that just decomposes the type and makes templates for _everything_. Some of these might not be actually needed.

This is an attempt to somewhat limit it.
Ever since GVM support was added to native AOT, we were generating the GVM resolution metadata for every type considered allocated. This included GVMs that were never even called (see `TypeGVMEntriesNode` that simply goes over everything on the type). This PR introduces tracking with method level granularity. I ran into this in a different PR where this was dragging `double`/`float` into compilation just because `int` implements generic math.
@Sergio0694
Copy link
Contributor

The size savings for CsWinRT look not too bad! 👀

@MichalStrehovsky
Copy link
Member Author

The size savings for CsWinRT look not too bad! 👀

This is currently multiple changes in one, I'm peeling some of it off into #118632 for reviewability, but it looks promising!

@jkotas
Copy link
Member

jkotas commented Aug 15, 2025

The test failure looks weird. Is it related to the changes?

[FAIL] System.Linq.Expressions.Tests.ExpressionDebuggerTypeProxyTests.ThrowOnNullToCtor(sourceObject: Param_0)
System.AggregateException : One or more errors occurred. (Didn't find DebuggerTypeProxyAttribute on System.Linq.Expressions.PrimitiveParameterExpression`1[System.Int32].) (Object reference not set to an instance of an object.)
---- Microsoft.DotNet.XUnitExtensions.SkipTestException : Didn't find DebuggerTypeProxyAttribute on System.Linq.Expressions.PrimitiveParameterExpression`1[System.Int32].
---- System.NullReferenceException : Object reference not set to an instance of an object.

----- Inner Stack Trace #1 (Microsoft.DotNet.XUnitExtensions.SkipTestException) -----
   at System.Linq.Expressions.Tests.ExpressionDebuggerTypeProxyTests.ThrowOnNullToCtor(Object sourceObject) + 0x205
   at System.Linq.Expressions!<BaseAddress>+0x1df7e62
   at System.Reflection.DynamicInvokeInfo.InvokeWithFewArguments(IntPtr, Byte&, Byte&, Object[], BinderBundle, Boolean) + 0x91
----- Inner Stack Trace #2 (System.NullReferenceException) -----
--- End of stack trace from previous location ---
--- End of stack trace from previous location ---

@davidwrighton
Copy link
Member

Looking at this change, it changes the policy of what the expansion analysis is doing.

Before the change, if a type ended up being marked as something that had a template, it would force all the various bits and pieces of the type to be constructed, such that a MakeGenericType on it would succeed reliably.

After the change, the policy is more that template construction will not trigger additional codegen. If it so happens that the needed code for the final application will not work, it is what it is. I'm not currently familiar enough with the code to determine if this will fail in an explainable fashion, but it is plausible that the failures that occur would be explained by the various annotations we have.

@MichalStrehovsky
Copy link
Member Author

The test failure looks weird. Is it related to the changes?

I couldn't quickly figure out this fails only on Linux. The test is trim unsafe and based on what I'm seeing it shouldn't even work in main, but for some reason I guess it works. I'll need to debug it next week. From compiler logs the test should be failing in main already

@MichalStrehovsky
Copy link
Member Author

The test failure looks weird. Is it related to the changes?

I couldn't quickly figure out this fails only on Linux. The test is trim unsafe and based on what I'm seeing it shouldn't even work in main, but for some reason I guess it works. I'll need to debug it next week. From compiler logs the test should be failing in main already

Ah, so the test does not succeed - neither in main nor here. We're taking the SkipTestException path and then hit some null ref. It didn't repro locally for me and a retry in the CI cleared it. Filed #118979 to try collect something from the infra to see if there's any history of this.

@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

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@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

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@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

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@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

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Get rid of TemplateConstructableTypes
4 participants