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
|
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
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
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