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

Downgrade MethodTables used in reflection invoke #111610

Merged
merged 11 commits into from
Mar 6, 2025

Conversation

MichalStrehovsky
Copy link
Member

@MichalStrehovsky MichalStrehovsky commented Jan 20, 2025

When we make a method reflection-callable, we have to make sure the reflection stack can get type handles of types within the method signature. The type handles are used as part of reflection activation to do castability checks (is it valid to call the method with this parameter?) and to do boxing of return values.

In the past, what we did:

  • If the type has a canonical form, generate type loader template for it.
  • Otherwise, generate a constructed MethodTable.

This was too much, so we restricted it in #92994 into:

  • If the type is a GC pointer, don't generate anything. Update reflection stack to be able to deal with a null type handle if and only if this is a reference type and nobody actually allocated such reference type.
  • Otherwise do what we did above.

This is still a bit too much, as demonstrated in #111544 (comment).

In this PR, I'm restricting this further to:

  • If the type is in the canonical form (not just "has canonical form" - "is canonical form"), generate type loader template.
  • If the type is an out valuetype parameter, generate constructed method table (since reflection stack is going to end up boxing)
  • Generate necessary (unconstructed) method table otherwise (since we're only going to do castability checks and necessary method tables are exactly for that)

I'm deleting the "GC pointer could have null methodtable" complication since it doesn't help much in practice once we downgraded to necessary method tables (that revert is in dc34018 and rt-sz measurements show it really doesn't affect much).

Cc @dotnet/ilc-contrib

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Reverts #92994 (manually since there were many merge conflicts due to EETypePtr deletion.)
@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 MichalStrehovsky marked this pull request as ready for review February 7, 2025 10:02
@MichalStrehovsky
Copy link
Member Author

Size statistics

Pull request #111610

Project Size before Size after Difference
avalonia.app-windows 19094016 19026944 -67072
hello-minimal-windows 857600 857600 0
hello-windows 1102848 1102848 0
kestrel-minimal-windows 4897792 4856832 -40960
reflection-windows 1748480 1747968 -512
webapiaot-windows 9153024 9110016 -43008
winrt-component-full-windows 5727232 5727744 512
winrt-component-minimal-windows 1759232 1758720 -512

@@ -62,6 +62,7 @@ interface IFoo3<T, K, J>
int Bar3 { get; set; }
}

[Kept (By = Tool.NativeAot)]
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 test is not actually testing anything relevant, so I'm not worried about "regression" here.

  • In ILLinker, looks like preserve=fields is going to skip over compiler generated backing fields. Not sure where that logic is but we clearly don't do that in native AOT and we do preserve them. Won't lose my sleep over that.
  • The test infrastructure doesn't check field preservation on native AOT side since we don't really "trim" fields (we vacate space for them no matter what, there's leeway whether reflection metadata is generated).

So the only observable effect in this test is whether the type of the field survived and now it by design survives if the field was a target of reflection.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Thank you

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test failures look like a real problem introduced by this change:

System.NotSupportedException : 'System.Nullable`1[System.EnvironmentVariableTarget]' is missing native code or metadata. This can happen for code that is not compatible with trimming or AOT. Inspect and fix trimming and AOT related warnings that were generated when the app was published. For more information see https://aka.ms/nativeaot-compatibility
   at System.Reflection.Runtime.TypeInfos.RuntimeTypeInfo.get_TypeHandle() + 0x94
   at System.Reflection.DynamicInvokeInfo..ctor(MethodBase, IntPtr) + 0xdc
   at Internal.Reflection.Execution.ExecutionEnvironmentImplementation.TryGetMethodInvokeInfo(RuntimeTypeHandle, QMethodDefinition, RuntimeTypeHandle[], MethodBase, MethodSignatureComparer&, CanonicalFormKind) + 0x168
   at Internal.Reflection.Execution.ExecutionEnvironmentImplementation.TryGetMethodInvoker(RuntimeTypeHandle, QMethodDefinition, RuntimeTypeHandle[]) + 0xcb
   at Internal.Reflection.Core.Execution.ExecutionEnvironment.GetMethodInvoker(RuntimeTypeInfo, QMethodDefinition, RuntimeTypeInfo[], MemberInfo, Exception&) + 0xc7
   at System.Reflection.Runtime.MethodInfos.NativeFormat.NativeFormatMethodCommon.GetUncachedMethodInvoker(RuntimeTypeInfo[], MemberInfo, Exception&) + 0x49
   at System.Reflection.Runtime.MethodInfos.RuntimeNamedMethodInfo`1.GetUncachedMethodInvoker(RuntimeTypeInfo[], MemberInfo) + 0x1b
   at System.Reflection.Runtime.MethodInfos.RuntimeMethodInfo.Invoke(Object, BindingFlags, Binder, Object[], CultureInfo) + 0x2b
--- End of stack trace from previous location ---

@MichalStrehovsky
Copy link
Member Author

The test failures look like a real problem introduced by this change:

That's annoying. We don't keep track of unconstructed generic MethodTables because of dotnet/runtimelab#1319.

So reflection cannot find the MethodTable we prepared in the compiler.

@MichalStrehovsky
Copy link
Member Author

Yeah, as expected, forcing all the generic types back into existence makes this no longer save much. In particular it brings back the problem with "ValueTuple in a signature of a reflection-targeted method is surprisingly expensive".

Now I wonder if we should undo dotnet/runtimelab#1319 and fix that some other way. This would lead to MakeGenericType sometimes succeeding but having a type handle that is not usable. Which is not very different from Type.GetType sometimes succeeding and having type handle that is not usable. We do live with that one. These all generate trimming warnings after all...

Size statistics

Pull request #111610

Project Size before Size after Difference
avalonia.app-windows 19094016 19088896 -5120
hello-minimal-windows 857600 857600 0
hello-windows 1102848 1102848 0
kestrel-minimal-windows 4897792 4887552 -10240
reflection-windows 1748480 1747968 -512
webapiaot-windows 9153024 9149952 -3072
winrt-component-full-windows 5727232 5728768 1536
winrt-component-minimal-windows 1759232 1758720 -512

@jkotas
Copy link
Member

jkotas commented Feb 10, 2025

This would lead to MakeGenericType sometimes succeeding but having a type handle that is not usable. Which is not very different from Type.GetType sometimes succeeding and having type handle that is not usable. We do live with that one. These all generate trimming warnings after all...

It would be fine with me if it comes with good error message. What is the error message we generate to unusable type handles? I do not have a strong opinion.

@MichalStrehovsky
Copy link
Member Author

It would be fine with me if it comes with good error message. What is the error message we generate to unusable type handles? I do not have a strong opinion.

Grabbing TypeHandle will take us to the same old MissingMetadata-like experience here (we now instruct people to inspect publish-time warnings):

// If got here, this is a "plain old type" that has metadata but no type handle. We can get here if the only
// representation of the type is in the native metadata and there's no MethodTable at the runtime side.
// If you squint hard, this is a missing metadata situation - the metadata is missing on the runtime side - and
// the action for the user to take is the same: go mess with RD.XML.
throw ReflectionCoreExecution.ExecutionEnvironment.CreateMissingMetadataException(this.ToType());

I have a sketch of a fix at #112986.

@Sergio0694
Copy link
Contributor

How come this saves 10 KB in kestrel minimal, but regresses 1.5 KB in CsWinRT?
Is it us doing too many weird things there that don't play well with this? I guess too many delegate types? 🥲

@MichalStrehovsky
Copy link
Member Author

How come this saves 10 KB in kestrel minimal, but regresses 1.5 KB in CsWinRT? Is it us doing too many weird things there that don't play well with this? I guess too many delegate types? 🥲

The relevant table is the 512 byte regression: #111610 (comment)

The 1.5 kB regression is the current state of the PR, which is not what I want to merge.

I can't tell what the problem is because rt-sz doesn't upload mstat files for the WinRT projects. I think I fixed it in MichalStrehovsky/rt-sz@5b17679 but I haven't rerun the measurement since.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, assuming the tests pass and the original size savings still hold #111610 (comment)

@MichalStrehovsky
Copy link
Member Author

LGTM, assuming the tests pass and the original size savings still hold #111610 (comment)

Yup, latest rt-sz results are supportive!

@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

Outerloop found an issue in one of the pri-1 tests. Trying to mark f1 as reflected on brings in the recursive dependency. Previously the optimization from #92994 that I'm rolling back in dc34018 would avoid the dependency because Expansive2 is a reference type but we now need to check for recursion.

  class Expansive2<A,B>
  {
    public Expansive2<Expansive2<A,B>, B > f1;
    public Expansive2(Expansive2<Expansive2<A,B>, B> a1) { f1 = a1; }
  }

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).


if (!(RuntimeImports.AreTypesAssignable(srcEEType, dstEEType) ||
(dstEEType->IsInterface && arg is System.Runtime.InteropServices.IDynamicInterfaceCastable castable
if (!(srcEEType == dstEEType ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering: Since this conditional clause is identical to the one in the parallel method, would extracting a helper method to ensure they never diverge? Seems like this whole block doesn't reference the parameters (except by the indexed-to arg) so it should be possible to extract 682-705.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would need to validate it doesn't deoptimize codegen, this code is quite performance sensitive.

@MichalStrehovsky MichalStrehovsky merged commit 666e918 into main Mar 6, 2025
122 of 141 checks passed
@MichalStrehovsky MichalStrehovsky deleted the MichalStrehovsky-patch-1 branch March 6, 2025 05:35
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.

4 participants