diff --git a/Tapeti.Benchmarks/Program.cs b/Tapeti.Benchmarks/Program.cs new file mode 100644 index 0000000..3e45b81 --- /dev/null +++ b/Tapeti.Benchmarks/Program.cs @@ -0,0 +1,5 @@ +using BenchmarkDotNet.Running; +using Tapeti.Benchmarks.Tests; + +BenchmarkRunner.Run(); +//new MethodInvokeBenchmarks().InvokeExpressionValueFactory(); \ No newline at end of file diff --git a/Tapeti.Benchmarks/Tapeti.Benchmarks.csproj b/Tapeti.Benchmarks/Tapeti.Benchmarks.csproj new file mode 100644 index 0000000..e3ab297 --- /dev/null +++ b/Tapeti.Benchmarks/Tapeti.Benchmarks.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + diff --git a/Tapeti.Benchmarks/Tests/MethodInvokeBenchmarks.cs b/Tapeti.Benchmarks/Tests/MethodInvokeBenchmarks.cs new file mode 100644 index 0000000..601aa5b --- /dev/null +++ b/Tapeti.Benchmarks/Tests/MethodInvokeBenchmarks.cs @@ -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(); + */ + } + + + 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); + //} + } +} diff --git a/Tapeti.sln b/Tapeti.sln index d065fcf..d740886 100644 --- a/Tapeti.sln +++ b/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 diff --git a/Tapeti/Default/ControllerMethodBinding.cs b/Tapeti/Default/ControllerMethodBinding.cs index 52aa0eb..24267e4 100644 --- a/Tapeti/Default/ControllerMethodBinding.cs +++ b/Tapeti/Default/ControllerMethodBinding.cs @@ -205,30 +205,30 @@ namespace Tapeti.Default private MessageHandlerFunc WrapMethod(MethodInfo method, IEnumerable 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 parameterFactories, ResultHandler resultHandler) + private MessageHandlerFunc WrapResultHandlerMethod(ExpressionInvoke invoke, IEnumerable parameterFactories, ResultHandler resultHandler) { return context => { var controllerPayload = context.Get(); 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 parameterFactories) + private MessageHandlerFunc WrapNullMethod(ExpressionInvoke invoke, IEnumerable parameterFactories) { return context => { var controllerPayload = context.Get(); 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 parameterFactories) + private MessageHandlerFunc WrapTaskMethod(ExpressionInvoke invoke, IEnumerable parameterFactories) { return context => { var controllerPayload = context.Get(); 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 parameterFactories) + private MessageHandlerFunc WrapValueTaskMethod(ExpressionInvoke invoke, IEnumerable parameterFactories) { return context => { var controllerPayload = context.Get(); 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) { diff --git a/Tapeti/Helpers/ExpressionInvoker.cs b/Tapeti/Helpers/ExpressionInvoker.cs new file mode 100644 index 0000000..b0d6a1b --- /dev/null +++ b/Tapeti/Helpers/ExpressionInvoker.cs @@ -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 +{ + /// + /// The precompiled version of MethodInfo.Invoke. + /// + /// The instance on which the method should be called. + /// The arguments passed to the method. + public delegate object ExpressionInvoke(object target, params object[] args); + + + /// + /// Provides a way to create a precompiled version of MethodInfo.Invoke with decreased overhead. + /// + public static class ExpressionInvokeExtensions + { + /// + /// Creates a precompiled version of MethodInfo.Invoke with decreased overhead. + /// + 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 lambda; + + if (method.ReturnType != typeof(void)) + { + var result = Expression.Convert(invoke, typeof(object)); + lambda = Expression.Lambda(result, target, argsParameter); + } + else + { + var nullResult = Expression.Constant(null, typeof(object)); + var body = Expression.Block(invoke, nullResult); + lambda = Expression.Lambda(body, target, argsParameter); + } + + return lambda.Compile(); + } + } +} \ No newline at end of file