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
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions src/coreclr/vm/appdomain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1523,10 +1523,19 @@ bool SystemDomain::IsReflectionInvocationMethod(MethodDesc* pMeth)

MethodTable* pCaller = pMeth->GetMethodTable();

// All Reflection Invocation methods are defined in CoreLib
// All reflection invocation methods are defined in CoreLib.
if (!pCaller->GetModule()->IsSystem())
return false;

// Check for dynamically generated Invoke methods.
if (pMeth->IsLCGMethod())
{
// Even if a user-created DynamicMethod uses the same naming convention, it will likely not
// get here since since DynamicMethods by default are created in a special (non-system) module.
// If this is not sufficient for conflict prevention, we can create a new private module.
return (strncmp(pMeth->GetName(), "InvokeStub_", ARRAY_SIZE("InvokeStub_") - 1) == 0);
}

/* List of types that should be skipped to identify true caller */
static const BinderClassID reflectionInvocationTypes[] = {
CLASS__METHOD,
Expand Down Expand Up @@ -1579,13 +1588,6 @@ bool SystemDomain::IsReflectionInvocationMethod(MethodDesc* pMeth)
if (CoreLibBinder::GetExistingClass(reflectionInvocationTypes[i]) == pCaller)
return true;
}

// Check for dynamically generated Invoke methods.
if (pMeth->IsDynamicMethod())
{
if (strncmp(pMeth->GetName(), "InvokeStub_", ARRAY_SIZE("InvokeStub_") - 1) == 0)
return true;
}
}

return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

skipVisibility: true);

ILGenerator il = dm.GetILGenerator();

Expand Down Expand Up @@ -64,6 +65,11 @@ public static unsafe InvokeFunc CreateInvokeDelegate(MethodBase method)
}

// Invoke the method.
#if !MONO
il.Emit(OpCodes.Call, Methods.NextCallReturnAddress()); // For CallStack reasons, don't inline target method.
il.Emit(OpCodes.Pop);
Comment on lines +69 to +70
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

#endif

if (emitNew)
{
il.Emit(OpCodes.Newobj, (ConstructorInfo)method);
Expand Down Expand Up @@ -182,6 +188,13 @@ public static MethodInfo Pointer_Box() =>
public static MethodInfo Type_GetTypeFromHandle() =>
s_Type_GetTypeFromHandle ??
(s_Type_GetTypeFromHandle = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle), new[] { typeof(RuntimeTypeHandle) })!);

#if !MONO
private static MethodInfo? s_NextCallReturnAddress;
public static MethodInfo NextCallReturnAddress() =>
s_NextCallReturnAddress ??
(s_NextCallReturnAddress = typeof(System.StubHelpers.StubHelpers).GetMethod(nameof(System.StubHelpers.StubHelpers.NextCallReturnAddress), BindingFlags.NonPublic | BindingFlags.Static)!);
#endif
}
}
}
1 change: 1 addition & 0 deletions src/libraries/System.Reflection/tests/ExceptionTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.CompilerServices;
using Xunit;

namespace System.Reflection.Tests
Expand Down
27 changes: 26 additions & 1 deletion src/libraries/System.Reflection/tests/MethodInfoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
Expand Down Expand Up @@ -771,6 +770,32 @@ public void CopyBackWithByRefArgs()
Assert.Null(args[0]);
}

[Fact]
[ActiveIssue("https://github.com/dotnet/runtime/issues/50957", typeof(PlatformDetection), nameof(PlatformDetection.IsMonoInterpreter))]
public static void CallStackFrame_AggressiveInlining()
{
MethodInfo mi = typeof(System.Reflection.TestAssembly.ClassToInvoke).GetMethod(nameof(System.Reflection.TestAssembly.ClassToInvoke.CallMe_AggressiveInlining),
BindingFlags.Public | BindingFlags.Static)!;

// Although the target method has AggressiveInlining, currently reflection should not inline the target into any generated IL.
FirstCall(mi);
SecondCall(mi);
}

[MethodImpl(MethodImplOptions.NoInlining)] // Separate non-inlineable method to aid any test failures
private static void FirstCall(MethodInfo mi)
{
Assembly asm = (Assembly)mi.Invoke(null, null);
Assert.Contains("TestAssembly", asm.ToString());
}

[MethodImpl(MethodImplOptions.NoInlining)] // Separate non-inlineable method to aid any test failures
private static void SecondCall(MethodInfo mi)
{
Assembly asm = (Assembly)mi.Invoke(null, null);
Assert.Contains("TestAssembly", asm.ToString());
}

//Methods for Reflection Metadata
private void DummyMethod1(string str, int iValue, long lValue)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.CompilerServices;

namespace System.Reflection.TestAssembly
{
public sealed class ClassToInvoke
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Assembly CallMe_AggressiveInlining()
{
return CallMeActual();
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static Assembly CallMeActual()
{
return Assembly.GetCallingAssembly();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="ClassToInvoke.cs" />
<Compile Include="TestAssembly.cs" />
</ItemGroup>
</Project>