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

Add IL Emit support for MethodInfo.Invoke() and friends #69575

Merged
merged 2 commits into from
May 23, 2022

Conversation

steveharter
Copy link
Member

@steveharter steveharter commented May 19, 2022

Brings back the original commit from #67917 plus a new commit to prevent a missing stack frame that blocked SDK integration thus causing the original commit to be reverted.

Testing performed with these changes:

  • Ran Microsoft.NET.Sdk.Razor.Tests in the SDK's integration tests locally which were previously failing in some cases due to chained constructors calling Assembly.GetCallingAssembly` to find the calling test class assembly.
  • Ran System.Diagnostics.StackTrace.Tests with COMPlus_JitStress=1 plus forcing emit on every invoke. This was also previously failing.
  • Added a new reflection test that previously failed.

The JIT will no longer inline the target method into the generated IL. A future commit will likely switch the implementation from using call and newobj opcodes to using function pointers (calli opcode plus a call to allocate for constructors). Doing that and passing the method pointer into the generated method will also prevent the JIT from inlining the target method, so in effect this PR prevents a temporary breaking change until we switch to calli.

Fixes #69251

@steveharter steveharter added this to the 7.0.0 milestone May 19, 2022
@steveharter steveharter self-assigned this May 19, 2022
@ghost
Copy link

ghost commented May 19, 2022

Tagging subscribers to this area: @dotnet/area-system-reflection
See info in area-owners.md if you want to be subscribed.

Issue Details

Brings back the original commit from #67917 plus a new commit to prevent a missing stack frame that blocked SDK integration thus causing the original commit to be reverted.

Author: steveharter
Assignees: steveharter
Labels:

area-System.Reflection

Milestone: 7.0.0

@steveharter steveharter marked this pull request as ready for review May 23, 2022 13:20
@steveharter steveharter requested a review from marek-safar as a code owner May 23, 2022 13:20
@@ -29,7 +29,8 @@ public static unsafe InvokeFunc CreateInvokeDelegate(MethodBase method)
InvokeStubPrefix + declaringTypeName + method.Name,
returnType: typeof(object),
delegateParameters,
restrictedSkipVisibility: true);
typeof(object).Module, // Use system module to identify our DynamicMethods.
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 helps address #69251 by assigning the system module. We could also create our own module with a special name and compare that, which would reduce all chances of naming collision but would require a new module\alloc.

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.

Thanks

@steveharter steveharter merged commit 51df356 into dotnet:main May 23, 2022
@steveharter steveharter deleted the EmitInvoke2 branch May 23, 2022 17:51
Comment on lines +69 to +70
il.Emit(OpCodes.Call, Methods.NextCallReturnAddress()); // For CallStack reasons, don't inline target method.
il.Emit(OpCodes.Pop);
Copy link
Member

@jakobbotsch jakobbotsch May 23, 2022

Choose a reason for hiding this comment

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

Out of curiosity, did you check codegen/your benchmarks when this is used? Just to verify that there is no significant perf impact by using this (beyond what is expected by not inlining the target call).

FWIW, in the current JIT I believe this will actually end up suppressing inlining for the remainder of the IL it sees. That shouldn't be too bad for common cases although I can see that it may have some impact on pointer/by-ref returns below.

Copy link
Member

Choose a reason for hiding this comment

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

FWIW, in the current JIT I believe this will actually end up suppressing inlining for the remainder of the IL it sees

For my edification, what is "this" in the above statement?

Copy link
Member

Choose a reason for hiding this comment

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

System.StubHelpers.StubHelpers.NextCallReturnAddress() is an intrinsic used to implement tailcalls. It has the side effect of guaranteeing that the next call-producing IL instruction will not be inlined by the JIT, so I suggested to @steveharter to use it in #69154.

Copy link
Member

Choose a reason for hiding this comment

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

Cool, thanks.

Copy link
Member Author

@steveharter steveharter May 24, 2022

Choose a reason for hiding this comment

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

did you check codegen/your benchmarks when this is used

There was perhaps a slight regression, but still within the margin of error. I'm not too concerned since we'd want to switch to use calli anyway and remove the call to the intrisic.

Just doing a single run and picking the most canonical benchmark:

previous
|                                                                       Method |        Job |              Toolchain |       Mean |      Error |     StdDev |     Median |        Min |        Max | Ratio | RatioSD |  Gen 0 | Allocated | Alloc Ratio |
|----------------------------------------------------------------------------- |----------- |----------------------- |-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|------:|--------:|-------:|----------:|------------:|
|                                        StaticMethod4_int_string_struct_class | Job-AEFDZT |      \main\corerun.exe | 200.804 ns |  3.3102 ns |  2.7642 ns | 200.939 ns | 194.044 ns | 205.150 ns |  3.22 |    0.07 | 0.0146 |     160 B |        1.00 |
|                                        StaticMethod4_int_string_struct_class | Job-VAFQFP | \newinvoke\corerun.exe |  62.374 ns |  0.8406 ns |  0.7452 ns |  62.538 ns |  61.110 ns |  63.808 ns |  1.00 |    0.00 | 0.0151 |     160 B |        1.00 

current
|                                                                       Method |        Job |               Toolchain |       Mean |     Error |    StdDev |     Median |        Min |        Max | Ratio | RatioSD |  Gen 0 | Allocated | Alloc Ratio |
|----------------------------------------------------------------------------- |----------- |------------------------ |-----------:|----------:|----------:|-----------:|-----------:|-----------:|------:|--------:|-------:|----------:|------------:|
|                                        StaticMethod4_int_string_struct_class | Job-WQJSNV |       \main\corerun.exe | 200.456 ns | 1.4665 ns | 1.3717 ns | 200.110 ns | 197.585 ns | 202.696 ns |  3.12 |    0.03 | 0.0145 |     160 B |        1.00 |
|                                        StaticMethod4_int_string_struct_class | Job-IAUHXD | \newinvoke3\corerun.exe |  64.245 ns | 0.4365 ns | 0.3869 ns |  64.281 ns |  63.756 ns |  65.193 ns |  1.00 |    0.00 | 0.0151 |     160 B | 

it went from a ratio of 3.22 to 3.12

@ericstj ericstj added the breaking-change Issue or PR that represents a breaking API or functional change over a prerelease. label May 24, 2022
@ghost ghost added the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label May 24, 2022
@ghost
Copy link

ghost commented May 24, 2022

Added needs-breaking-change-doc-created label because this PR has the breaking-change label.

When you commit this breaking change:

  1. Create and link to this PR and the issue a matching issue in the dotnet/docs repo using the breaking change documentation template, then remove this needs-breaking-change-doc-created label.
  2. Ask a committer to mail the .NET Breaking Change Notification DL.

Tagging @dotnet/compat for awareness of the breaking change.

@ericstj
Copy link
Member

ericstj commented May 24, 2022

Let's consider this "breaking" as a functional breaking change. That way we can get some docs that let folks know about the types of behavior changes they might see as a result of this change. -- Rereading your mitigations above, feel free to remove the breaking change tag if you feel that this is less breaking in its current form.

@AndyAyersMS
Copy link
Member

AndyAyersMS commented May 27, 2022

@steveharter
Copy link
Member Author

@sebastienros have you seen any TechEmpower changes from this? Basically scenarios that use reflection to invoke members. Thanks

@sebastienros
Copy link
Member

sebastienros commented Jun 9, 2022

@steveharter Nothing visible on the charts or caught by the bot. In the future ping me when the PR is still open and if you have a local build I can show you how to check if there is an impact.

@ericstj
Copy link
Member

ericstj commented Jun 9, 2022

@sebastienros do you have any other ASP.NET benchmarks that we could check? I would hope that tech-empower doesn't have reflection invoke on the hot path but perhaps other ASP.NET scenarios do.

@sebastienros
Copy link
Member

@ericstj I looked at MVC scenarios actually because this is where we should see reflection the most. And nothing special shows up around the date it was merged. There is an MVC page in the dashboard with different scenarios all using MVC.

@ghost ghost locked as resolved and limited conversation to collaborators Jul 9, 2022
@steveharter
Copy link
Member Author

steveharter commented Sep 26, 2022

Removing "breaking change" tags since:

  • The "tailcall" issue was fixed without a breaking change. Originally, the thinking was that a breaking change may be necessary where the breaking change would have been to assume an invoked method may be inlined thus causing Assembly.GetCallingAssembly() to behave differently. This PR preserved the functionality of Assembly.GetCallingAssembly() without a breaking change.
  • Although stack frames may be different now within an invoked method, that does not really raise to the level of a breaking change. See also Consider hiding stack frames when using Invoke #68923 for future changes here where we may try to hide more reflection frames.
  • Exception breaking changes due to the previous refactoring is documented in [Breaking change]: exceptions thrown by reflection Invoke() APIs have changed docs#29199

@steveharter steveharter removed needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet breaking-change Issue or PR that represents a breaking API or functional change over a prerelease. labels Sep 26, 2022
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.

Follow-up for Assembly.GetCallingAssembly() - tests and stack walk
7 participants