1
0
mirror of synced 2024-11-24 19:53:10 +01:00

Reduced Invoke overhead for controller methods

This commit is contained in:
Mark van Renswoude 2022-02-10 10:16:16 +01:00
parent 3aee6f1c53
commit adde0c3c8d
6 changed files with 223 additions and 14 deletions

View File

@ -0,0 +1,5 @@
using BenchmarkDotNet.Running;
using Tapeti.Benchmarks.Tests;
BenchmarkRunner.Run<MethodInvokeBenchmarks>();
//new MethodInvokeBenchmarks().InvokeExpressionValueFactory();

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tapeti\Tapeti.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,112 @@
using System.Reflection;
using BenchmarkDotNet.Attributes;
using Tapeti.Helpers;
#pragma warning disable CA1822 // Mark members as static - required for Benchmark.NET
namespace Tapeti.Benchmarks.Tests
{
[MemoryDiagnoser]
public class MethodInvokeBenchmarks
{
private delegate bool MethodToInvokeDelegate(object obj);
private static readonly MethodInfo MethodToInvokeInfo;
private static readonly MethodToInvokeDelegate MethodToInvokeDelegateInstance;
private static readonly ExpressionInvoke MethodToInvokeExpression;
static MethodInvokeBenchmarks()
{
MethodToInvokeInfo = typeof(MethodInvokeBenchmarks).GetMethod(nameof(MethodToInvoke))!;
var inputInstance = new MethodInvokeBenchmarks();
MethodToInvokeDelegateInstance = i => ((MethodInvokeBenchmarks)i).MethodToInvoke(inputInstance.GetSomeObject(), inputInstance.GetCancellationToken());
MethodToInvokeExpression = MethodToInvokeInfo.CreateExpressionInvoke();
/*
Fun experiment, but a bit too tricky for me at the moment.
var dynamicMethodToInvoke = new DynamicMethod(
nameof(MethodToInvoke),
typeof(bool),
new[] { typeof(object) },
typeof(MethodInvokeBenchmarks).Module);
var generator = dynamicMethodToInvoke.GetILGenerator(256);
generator.Emit(OpCodes.Ldarg_0); // Load the first argument (the instance) onto the stack
generator.Emit(OpCodes.Castclass, typeof(MethodInvokeBenchmarks)); // Cast to the expected instance type
generator.Emit(OpCodes.Ldc_I4_S, 42); // Push the first argument onto the stack
generator.EmitCall(OpCodes.Callvirt, MethodToInvokeInfo, null); // Call the method
generator.Emit(OpCodes.Ret);
MethodToInvokeEmitted = dynamicMethodToInvoke.CreateDelegate<MethodToInvokeDelegate>();
*/
}
public bool MethodToInvoke(object someObject, CancellationToken cancellationToken)
{
return true;
}
// ReSharper disable MemberCanBeMadeStatic.Local
private object GetSomeObject()
{
return new object();
}
private CancellationToken GetCancellationToken()
{
return CancellationToken.None;
}
// ReSharper restore MemberCanBeMadeStatic.Local
// For comparison
[Benchmark]
public bool Direct()
{
return MethodToInvoke(GetSomeObject(), GetCancellationToken());
}
// For comparison as well, as we don't know the signature beforehand
[Benchmark]
public bool Delegate()
{
var instance = new MethodInvokeBenchmarks();
return MethodToInvokeDelegateInstance(instance);
}
[Benchmark]
public bool MethodInvoke()
{
var instance = new MethodInvokeBenchmarks();
return (bool)(MethodToInvokeInfo.Invoke(instance, BindingFlags.DoNotWrapExceptions, null, new[] { GetSomeObject(), GetCancellationToken() }, null) ?? false);
}
[Benchmark]
public bool InvokeExpression()
{
var instance = new MethodInvokeBenchmarks();
return (bool)MethodToInvokeExpression(instance, GetSomeObject(), GetCancellationToken());
}
//[Benchmark]
//public bool ReflectionEmit()
//{
// var instance = new MethodInvokeBenchmarks();
// return MethodToInvokeEmitted(instance);
//}
}
}

View File

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16 # Visual Studio Version 17
VisualStudioVersion = 16.0.31005.135 VisualStudioVersion = 17.0.32112.339
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti", "Tapeti\Tapeti.csproj", "{2952B141-C54D-4E6F-8108-CAD735B0279F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti", "Tapeti\Tapeti.csproj", "{2952B141-C54D-4E6F-8108-CAD735B0279F}"
EndProject EndProject
@ -55,6 +55,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "07-ParallelizationTest", "E
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "08-MessageHandlerLogging", "Examples\08-MessageHandlerLogging\08-MessageHandlerLogging.csproj", "{906605A6-2CAB-4B29-B0DD-B735BF265E39}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "08-MessageHandlerLogging", "Examples\08-MessageHandlerLogging\08-MessageHandlerLogging.csproj", "{906605A6-2CAB-4B29-B0DD-B735BF265E39}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tapeti.Benchmarks", "Tapeti.Benchmarks\Tapeti.Benchmarks.csproj", "{DBE56131-9207-4CEA-BA3E-031351677C48}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -149,6 +151,10 @@ Global
{906605A6-2CAB-4B29-B0DD-B735BF265E39}.Debug|Any CPU.Build.0 = Debug|Any CPU {906605A6-2CAB-4B29-B0DD-B735BF265E39}.Debug|Any CPU.Build.0 = Debug|Any CPU
{906605A6-2CAB-4B29-B0DD-B735BF265E39}.Release|Any CPU.ActiveCfg = Release|Any CPU {906605A6-2CAB-4B29-B0DD-B735BF265E39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{906605A6-2CAB-4B29-B0DD-B735BF265E39}.Release|Any CPU.Build.0 = Release|Any CPU {906605A6-2CAB-4B29-B0DD-B735BF265E39}.Release|Any CPU.Build.0 = Release|Any CPU
{DBE56131-9207-4CEA-BA3E-031351677C48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DBE56131-9207-4CEA-BA3E-031351677C48}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DBE56131-9207-4CEA-BA3E-031351677C48}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DBE56131-9207-4CEA-BA3E-031351677C48}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -205,30 +205,30 @@ namespace Tapeti.Default
private MessageHandlerFunc WrapMethod(MethodInfo method, IEnumerable<ValueFactory> parameterFactories, ResultHandler resultHandler) private MessageHandlerFunc WrapMethod(MethodInfo method, IEnumerable<ValueFactory> parameterFactories, ResultHandler resultHandler)
{ {
if (resultHandler != null) if (resultHandler != null)
return WrapResultHandlerMethod(method, parameterFactories, resultHandler); return WrapResultHandlerMethod(method.CreateExpressionInvoke(), parameterFactories, resultHandler);
if (method.ReturnType == typeof(void)) if (method.ReturnType == typeof(void))
return WrapNullMethod(method, parameterFactories); return WrapNullMethod(method.CreateExpressionInvoke(), parameterFactories);
if (method.ReturnType == typeof(Task)) if (method.ReturnType == typeof(Task))
return WrapTaskMethod(method, parameterFactories); return WrapTaskMethod(method.CreateExpressionInvoke(), parameterFactories);
if (method.ReturnType == typeof(ValueTask)) if (method.ReturnType == typeof(ValueTask))
return WrapValueTaskMethod(method, parameterFactories); return WrapValueTaskMethod(method.CreateExpressionInvoke(), parameterFactories);
// Breaking change in Tapeti 2.9: PublishResultBinding or other middleware should have taken care of the return value. If not, don't silently discard it. // Breaking change in Tapeti 2.9: PublishResultBinding or other middleware should have taken care of the return value. If not, don't silently discard it.
throw new ArgumentException($"Method {method.Name} on controller {method.DeclaringType?.FullName} returns type {method.ReturnType.FullName}, which can not be handled by Tapeti or any registered middleware"); throw new ArgumentException($"Method {method.Name} on controller {method.DeclaringType?.FullName} returns type {method.ReturnType.FullName}, which can not be handled by Tapeti or any registered middleware");
} }
private MessageHandlerFunc WrapResultHandlerMethod(MethodBase method, IEnumerable<ValueFactory> parameterFactories, ResultHandler resultHandler) private MessageHandlerFunc WrapResultHandlerMethod(ExpressionInvoke invoke, IEnumerable<ValueFactory> parameterFactories, ResultHandler resultHandler)
{ {
return context => return context =>
{ {
var controllerPayload = context.Get<ControllerMessageContextPayload>(); var controllerPayload = context.Get<ControllerMessageContextPayload>();
try try
{ {
var result = method.Invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray()); var result = invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray());
return resultHandler(context, result); return resultHandler(context, result);
} }
catch (Exception e) catch (Exception e)
@ -239,14 +239,14 @@ namespace Tapeti.Default
}; };
} }
private MessageHandlerFunc WrapNullMethod(MethodBase method, IEnumerable<ValueFactory> parameterFactories) private MessageHandlerFunc WrapNullMethod(ExpressionInvoke invoke, IEnumerable<ValueFactory> parameterFactories)
{ {
return context => return context =>
{ {
var controllerPayload = context.Get<ControllerMessageContextPayload>(); var controllerPayload = context.Get<ControllerMessageContextPayload>();
try try
{ {
method.Invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray()); invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray());
return default; return default;
} }
catch (Exception e) catch (Exception e)
@ -258,14 +258,14 @@ namespace Tapeti.Default
} }
private MessageHandlerFunc WrapTaskMethod(MethodBase method, IEnumerable<ValueFactory> parameterFactories) private MessageHandlerFunc WrapTaskMethod(ExpressionInvoke invoke, IEnumerable<ValueFactory> parameterFactories)
{ {
return context => return context =>
{ {
var controllerPayload = context.Get<ControllerMessageContextPayload>(); var controllerPayload = context.Get<ControllerMessageContextPayload>();
try try
{ {
return new ValueTask((Task) method.Invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray())); return new ValueTask((Task) invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray()));
} }
catch (Exception e) catch (Exception e)
{ {
@ -276,14 +276,14 @@ namespace Tapeti.Default
} }
private MessageHandlerFunc WrapValueTaskMethod(MethodBase method, IEnumerable<ValueFactory> parameterFactories) private MessageHandlerFunc WrapValueTaskMethod(ExpressionInvoke invoke, IEnumerable<ValueFactory> parameterFactories)
{ {
return context => return context =>
{ {
var controllerPayload = context.Get<ControllerMessageContextPayload>(); var controllerPayload = context.Get<ControllerMessageContextPayload>();
try try
{ {
return (ValueTask)method.Invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray()); return (ValueTask)invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray());
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -0,0 +1,68 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
// Note: I also tried a version which accepts a ValueFactory[] to reduce the object array allocations,
// but the performance benefits were negligable and it still allocated more memory than expected.
//
// Reflection.Emit is another option which I've dabbled with, but that's too much of a risk for me to
// attempt at the moment and there's probably other code which could benefit from optimization more.
namespace Tapeti.Helpers
{
/// <summary>
/// The precompiled version of MethodInfo.Invoke.
/// </summary>
/// <param name="target">The instance on which the method should be called.</param>
/// <param name="args">The arguments passed to the method.</param>
public delegate object ExpressionInvoke(object target, params object[] args);
/// <summary>
/// Provides a way to create a precompiled version of MethodInfo.Invoke with decreased overhead.
/// </summary>
public static class ExpressionInvokeExtensions
{
/// <summary>
/// Creates a precompiled version of MethodInfo.Invoke with decreased overhead.
/// </summary>
public static ExpressionInvoke CreateExpressionInvoke(this MethodInfo method)
{
if (method.DeclaringType == null)
throw new ArgumentException("Method must have a declaring type");
var argsParameter = Expression.Parameter(typeof(object[]), "args");
var parameters = method.GetParameters().Select(
(p, i) =>
{
var argsIndexExpression = Expression.Constant(i, typeof(int));
var argExpression = Expression.ArrayIndex(argsParameter, argsIndexExpression);
return Expression.Convert(argExpression, p.ParameterType) as Expression;
})
.ToArray();
var target = Expression.Parameter(typeof(object), "target");
var castTarget = Expression.Convert(target, method.DeclaringType);
var invoke = Expression.Call(castTarget, method, parameters);
Expression<ExpressionInvoke> lambda;
if (method.ReturnType != typeof(void))
{
var result = Expression.Convert(invoke, typeof(object));
lambda = Expression.Lambda<ExpressionInvoke>(result, target, argsParameter);
}
else
{
var nullResult = Expression.Constant(null, typeof(object));
var body = Expression.Block(invoke, nullResult);
lambda = Expression.Lambda<ExpressionInvoke>(body, target, argsParameter);
}
return lambda.Compile();
}
}
}