Reduced Invoke overhead for controller methods
This commit is contained in:
parent
3aee6f1c53
commit
adde0c3c8d
5
Tapeti.Benchmarks/Program.cs
Normal file
5
Tapeti.Benchmarks/Program.cs
Normal file
@ -0,0 +1,5 @@
|
||||
using BenchmarkDotNet.Running;
|
||||
using Tapeti.Benchmarks.Tests;
|
||||
|
||||
BenchmarkRunner.Run<MethodInvokeBenchmarks>();
|
||||
//new MethodInvokeBenchmarks().InvokeExpressionValueFactory();
|
18
Tapeti.Benchmarks/Tapeti.Benchmarks.csproj
Normal file
18
Tapeti.Benchmarks/Tapeti.Benchmarks.csproj
Normal 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>
|
112
Tapeti.Benchmarks/Tests/MethodInvokeBenchmarks.cs
Normal file
112
Tapeti.Benchmarks/Tests/MethodInvokeBenchmarks.cs
Normal 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);
|
||||
//}
|
||||
}
|
||||
}
|
10
Tapeti.sln
10
Tapeti.sln
@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.31005.135
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.32112.339
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti", "Tapeti\Tapeti.csproj", "{2952B141-C54D-4E6F-8108-CAD735B0279F}"
|
||||
EndProject
|
||||
@ -55,6 +55,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "07-ParallelizationTest", "E
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "08-MessageHandlerLogging", "Examples\08-MessageHandlerLogging\08-MessageHandlerLogging.csproj", "{906605A6-2CAB-4B29-B0DD-B735BF265E39}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tapeti.Benchmarks", "Tapeti.Benchmarks\Tapeti.Benchmarks.csproj", "{DBE56131-9207-4CEA-BA3E-031351677C48}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -205,30 +205,30 @@ namespace Tapeti.Default
|
||||
private MessageHandlerFunc WrapMethod(MethodInfo method, IEnumerable<ValueFactory> parameterFactories, ResultHandler resultHandler)
|
||||
{
|
||||
if (resultHandler != null)
|
||||
return WrapResultHandlerMethod(method, parameterFactories, resultHandler);
|
||||
return WrapResultHandlerMethod(method.CreateExpressionInvoke(), parameterFactories, resultHandler);
|
||||
|
||||
if (method.ReturnType == typeof(void))
|
||||
return WrapNullMethod(method, parameterFactories);
|
||||
return WrapNullMethod(method.CreateExpressionInvoke(), parameterFactories);
|
||||
|
||||
if (method.ReturnType == typeof(Task))
|
||||
return WrapTaskMethod(method, parameterFactories);
|
||||
return WrapTaskMethod(method.CreateExpressionInvoke(), parameterFactories);
|
||||
|
||||
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.
|
||||
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 =>
|
||||
{
|
||||
var controllerPayload = context.Get<ControllerMessageContextPayload>();
|
||||
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);
|
||||
}
|
||||
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 =>
|
||||
{
|
||||
var controllerPayload = context.Get<ControllerMessageContextPayload>();
|
||||
try
|
||||
{
|
||||
method.Invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray());
|
||||
invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray());
|
||||
return default;
|
||||
}
|
||||
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 =>
|
||||
{
|
||||
var controllerPayload = context.Get<ControllerMessageContextPayload>();
|
||||
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)
|
||||
{
|
||||
@ -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 =>
|
||||
{
|
||||
var controllerPayload = context.Get<ControllerMessageContextPayload>();
|
||||
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)
|
||||
{
|
||||
|
68
Tapeti/Helpers/ExpressionInvoker.cs
Normal file
68
Tapeti/Helpers/ExpressionInvoker.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user