From 5720494574978d43100ccbc324fa14928b5207f0 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 19 Sep 2022 18:06:47 -0400 Subject: [PATCH 01/13] Added AwaitHelper to properly wait for ValueTasks. --- .../Code/DeclarationsProvider.cs | 14 +- src/BenchmarkDotNet/Helpers/AwaitHelper.cs | 112 +++++++++++++ .../IlGeneratorStatementExtensions.cs | 31 ++++ .../Templates/BenchmarkType.txt | 4 + .../ConsumableTypeInfo.cs | 34 ++-- .../Emitters/RunnableEmitter.cs | 94 +++++------ .../Runnable/RunnableConstants.cs | 1 + .../BenchmarkActionFactory_Implementations.cs | 9 +- .../BenchmarkActionFactory_Implementations.cs | 9 +- .../Validators/ExecutionValidatorBase.cs | 20 +-- .../AllSetupAndCleanupTest.cs | 156 ++++++++---------- .../AsyncBenchmarksTests.cs | 43 ++++- .../InProcessEmitTest.cs | 14 ++ .../Validators/ExecutionValidatorTests.cs | 78 +++++++++ 14 files changed, 430 insertions(+), 189 deletions(-) create mode 100644 src/BenchmarkDotNet/Helpers/AwaitHelper.cs diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs index ddf78eb572..21e3ce54c1 100644 --- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs +++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs @@ -63,7 +63,7 @@ private string GetMethodName(MethodInfo method) (method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>) || method.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)))) { - return $"() => {method.Name}().GetAwaiter().GetResult()"; + return $"() => awaitHelper.GetResult({method.Name}())"; } return method.Name; @@ -149,12 +149,10 @@ internal class TaskDeclarationsProvider : VoidDeclarationsProvider { public TaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { } - // we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way, - // and will eventually throw actual exception, not aggregated one public override string WorkloadMethodDelegate(string passArguments) - => $"({passArguments}) => {{ {Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult(); }}"; + => $"({passArguments}) => {{ awaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments})); }}"; - public override string GetWorkloadMethodCall(string passArguments) => $"{Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult()"; + public override string GetWorkloadMethodCall(string passArguments) => $"awaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments}))"; protected override Type WorkloadMethodReturnType => typeof(void); } @@ -168,11 +166,9 @@ public GenericTaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) protected override Type WorkloadMethodReturnType => Descriptor.WorkloadMethod.ReturnType.GetTypeInfo().GetGenericArguments().Single(); - // we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way, - // and will eventually throw actual exception, not aggregated one public override string WorkloadMethodDelegate(string passArguments) - => $"({passArguments}) => {{ return {Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult(); }}"; + => $"({passArguments}) => {{ return awaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments})); }}"; - public override string GetWorkloadMethodCall(string passArguments) => $"{Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult()"; + public override string GetWorkloadMethodCall(string passArguments) => $"awaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments}))"; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs new file mode 100644 index 0000000000..eb27c4cfa2 --- /dev/null +++ b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs @@ -0,0 +1,112 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Helpers +{ + public class AwaitHelper + { + private readonly object awaiterLock = new object(); + private readonly Action awaiterCallback; + private bool awaiterCompleted; + + public AwaitHelper() + { + awaiterCallback = AwaiterCallback; + } + + private void AwaiterCallback() + { + lock (awaiterLock) + { + awaiterCompleted = true; + System.Threading.Monitor.Pulse(awaiterLock); + } + } + + // we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way, + // and will eventually throw actual exception, not aggregated one + public void GetResult(Task task) + { + task.GetAwaiter().GetResult(); + } + + public T GetResult(Task task) + { + return task.GetAwaiter().GetResult(); + } + + // It is illegal to call GetResult from an uncomplete ValueTask, so we must hook up a callback. + public void GetResult(ValueTask task) + { + // Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process. + var awaiter = task.ConfigureAwait(false).GetAwaiter(); + if (!awaiter.IsCompleted) + { + lock (awaiterLock) + { + awaiterCompleted = false; + awaiter.UnsafeOnCompleted(awaiterCallback); + // Check if the callback executed synchronously before blocking. + if (!awaiterCompleted) + { + System.Threading.Monitor.Wait(awaiterLock); + } + } + } + awaiter.GetResult(); + } + + public T GetResult(ValueTask task) + { + // Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process. + var awaiter = task.ConfigureAwait(false).GetAwaiter(); + if (!awaiter.IsCompleted) + { + lock (awaiterLock) + { + awaiterCompleted = false; + awaiter.UnsafeOnCompleted(awaiterCallback); + // Check if the callback executed synchronously before blocking. + if (!awaiterCompleted) + { + System.Threading.Monitor.Wait(awaiterLock); + } + } + } + return awaiter.GetResult(); + } + + internal static MethodInfo GetGetResultMethod(Type taskType) + { + if (!taskType.IsGenericType) + { + return typeof(AwaitHelper).GetMethod(nameof(AwaitHelper.GetResult), BindingFlags.Public | BindingFlags.Instance, null, new Type[1] { taskType }, null); + } + + Type compareType = taskType.GetGenericTypeDefinition() == typeof(ValueTask<>) ? typeof(ValueTask<>) + : typeof(Task).IsAssignableFrom(taskType.GetGenericTypeDefinition()) ? typeof(Task<>) + : null; + if (compareType == null) + { + return null; + } + var resultType = taskType + .GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance) + .ReturnType + .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlags.Public | BindingFlags.Instance) + .ReturnType; + return typeof(AwaitHelper).GetMethods(BindingFlags.Public | BindingFlags.Instance) + .First(m => + { + if (m.Name != nameof(AwaitHelper.GetResult)) return false; + Type paramType = m.GetParameters().First().ParameterType; + // We have to compare the types indirectly, == check doesn't work. + return paramType.Assembly == compareType.Assembly && paramType.Namespace == compareType.Namespace && paramType.Name == compareType.Name; + }) + .MakeGenericMethod(new[] { resultType }); + } + } +} diff --git a/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs index 6d601e6495..65b016a0dd 100644 --- a/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs +++ b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs @@ -42,6 +42,37 @@ public static void EmitVoidReturn(this ILGenerator ilBuilder, MethodBuilder meth ilBuilder.Emit(OpCodes.Ret); } + public static void EmitSetFieldToNewInstance( + this ILGenerator ilBuilder, + FieldBuilder field, + Type instanceType) + { + if (field.IsStatic) + throw new ArgumentException("The field should be instance field", nameof(field)); + + if (instanceType != null) + { + /* + IL_0006: ldarg.0 + IL_0007: newobj instance void BenchmarkDotNet.Helpers.AwaitHelper::.ctor() + IL_000c: stfld class BenchmarkDotNet.Helpers.AwaitHelper BenchmarkDotNet.Autogenerated.Runnable_0::awaitHelper + */ + var ctor = instanceType.GetConstructor(Array.Empty()); + if (ctor == null) + throw new InvalidOperationException($"Bug: instanceType {instanceType.Name} does not have a 0-parameter accessible constructor."); + + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Newobj, ctor); + ilBuilder.Emit(OpCodes.Stfld, field); + } + else + { + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldnull); + ilBuilder.Emit(OpCodes.Stfld, field); + } + } + public static void EmitSetDelegateToThisField( this ILGenerator ilBuilder, FieldBuilder delegateField, diff --git a/src/BenchmarkDotNet/Templates/BenchmarkType.txt b/src/BenchmarkDotNet/Templates/BenchmarkType.txt index d8f15f9138..7a215a4c93 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkType.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkType.txt @@ -57,6 +57,8 @@ public Runnable_$ID$() { + awaitHelper = new BenchmarkDotNet.Helpers.AwaitHelper(); + globalSetupAction = $GlobalSetupMethodName$; globalCleanupAction = $GlobalCleanupMethodName$; iterationSetupAction = $IterationSetupMethodName$; @@ -66,6 +68,8 @@ $InitializeArgumentFields$ } + private readonly BenchmarkDotNet.Helpers.AwaitHelper awaitHelper; + private System.Action globalSetupAction; private System.Action globalCleanupAction; private System.Action iterationSetupAction; diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/ConsumableTypeInfo.cs b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/ConsumableTypeInfo.cs index 060c977014..8c3bf24eac 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/ConsumableTypeInfo.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/ConsumableTypeInfo.cs @@ -1,6 +1,8 @@ using BenchmarkDotNet.Engines; using JetBrains.Annotations; using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; @@ -17,28 +19,24 @@ public ConsumableTypeInfo(Type methodReturnType) OriginMethodReturnType = methodReturnType; - // Please note this code does not support await over extension methods. - var getAwaiterMethod = methodReturnType.GetMethod(nameof(Task.GetAwaiter), BindingFlagsPublicInstance); - if (getAwaiterMethod == null) + // Only support (Value)Task for parity with other toolchains (and so we can use AwaitHelper). + IsAwaitable = methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask) + || (methodReturnType.GetTypeInfo().IsGenericType + && (methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) + || methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>))); + + if (!IsAwaitable) { WorkloadMethodReturnType = methodReturnType; } else { - var getResultMethod = getAwaiterMethod + WorkloadMethodReturnType = methodReturnType + .GetMethod(nameof(Task.GetAwaiter), BindingFlagsPublicInstance) .ReturnType - .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlagsPublicInstance); - - if (getResultMethod == null) - { - WorkloadMethodReturnType = methodReturnType; - } - else - { - WorkloadMethodReturnType = getResultMethod.ReturnType; - GetAwaiterMethod = getAwaiterMethod; - GetResultMethod = getResultMethod; - } + .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlagsPublicInstance) + .ReturnType; + GetResultMethod = Helpers.AwaitHelper.GetGetResultMethod(methodReturnType); } if (WorkloadMethodReturnType == null) @@ -78,8 +76,6 @@ public ConsumableTypeInfo(Type methodReturnType) [NotNull] public Type OverheadMethodReturnType { get; } - [CanBeNull] - public MethodInfo GetAwaiterMethod { get; } [CanBeNull] public MethodInfo GetResultMethod { get; } @@ -89,6 +85,6 @@ public ConsumableTypeInfo(Type methodReturnType) [CanBeNull] public FieldInfo WorkloadConsumableField { get; } - public bool IsAwaitable => GetAwaiterMethod != null && GetResultMethod != null; + public bool IsAwaitable { get; } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs index 91e97cc71e..23b453043e 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs @@ -246,6 +246,7 @@ private static void EmitNoArgsMethodCallPopReturn( private ConsumableTypeInfo consumableInfo; private ConsumeEmitter consumeEmitter; + private FieldBuilder awaitHelperField; private FieldBuilder globalSetupActionField; private FieldBuilder globalCleanupActionField; private FieldBuilder iterationSetupActionField; @@ -412,6 +413,8 @@ private Type EmitWorkloadDelegateType() private void DefineFields() { + awaitHelperField = + runnableBuilder.DefineField(AwaitHelperFieldName, typeof(Helpers.AwaitHelper), FieldAttributes.Private | FieldAttributes.InitOnly); globalSetupActionField = runnableBuilder.DefineField(GlobalSetupActionFieldName, typeof(Action), FieldAttributes.Private); globalCleanupActionField = @@ -581,42 +584,35 @@ private MethodInfo EmitWorkloadImplementation(string methodName) workloadInvokeMethod.ReturnParameter, args); args = methodBuilder.GetEmitParameters(args); - var callResultType = consumableInfo.OriginMethodReturnType; - var awaiterType = consumableInfo.GetAwaiterMethod?.ReturnType - ?? throw new InvalidOperationException($"Bug: {nameof(consumableInfo.GetAwaiterMethod)} is null"); var ilBuilder = methodBuilder.GetILGenerator(); /* - .locals init ( - [0] valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1 - ) - */ - var callResultLocal = - ilBuilder.DeclareOptionalLocalForInstanceCall(callResultType, consumableInfo.GetAwaiterMethod); - var awaiterLocal = - ilBuilder.DeclareOptionalLocalForInstanceCall(awaiterType, consumableInfo.GetResultMethod); + IL_0007: ldarg.0 + IL_0008: ldfld class BenchmarkDotNet.Helpers.AwaitHelper BenchmarkDotNet.Helpers.Runnable_0::awaitHelper + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, awaitHelperField); /* - // return TaskSample(arg0). ... ; - IL_0000: ldarg.0 - IL_0001: ldarg.1 - IL_0002: call instance class [mscorlib]System.Threading.Tasks.Task`1 [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::TaskSample(int64) - */ + IL_0026: ldarg.0 + IL_0027: ldloc.0 + IL_0028: ldloc.1 + IL_0029: ldloc.2 + IL_002a: ldloc.3 + IL_002b: call instance class [System.Private.CoreLib]System.Threading.Tasks.Task`1 BenchmarkDotNet.Helpers.Runnable_0::WorkloadMethod(string, string, string, string) + */ if (!Descriptor.WorkloadMethod.IsStatic) ilBuilder.Emit(OpCodes.Ldarg_0); ilBuilder.EmitLdargs(args); ilBuilder.Emit(OpCodes.Call, Descriptor.WorkloadMethod); /* - // ... .GetAwaiter().GetResult(); - IL_0007: callvirt instance valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1 class [mscorlib]System.Threading.Tasks.Task`1::GetAwaiter() - IL_000c: stloc.0 - IL_000d: ldloca.s 0 - IL_000f: call instance !0 valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1::GetResult() - */ - ilBuilder.EmitInstanceCallThisValueOnStack(callResultLocal, consumableInfo.GetAwaiterMethod); - ilBuilder.EmitInstanceCallThisValueOnStack(awaiterLocal, consumableInfo.GetResultMethod); + // awaitHelper.GetResult(...); + IL_000e: callvirt instance void BenchmarkDotNet.Helpers.AwaitHelper::GetResult(class [System.Private.CoreLib]System.Threading.Tasks.Task) + */ + + ilBuilder.Emit(OpCodes.Callvirt, consumableInfo.GetResultMethod); /* IL_0014: ret @@ -831,19 +827,6 @@ .locals init ( var skipFirstArg = workloadMethod.IsStatic; var argLocals = EmitDeclareArgLocals(ilBuilder, skipFirstArg); - LocalBuilder callResultLocal = null; - LocalBuilder awaiterLocal = null; - if (consumableInfo.IsAwaitable) - { - var callResultType = consumableInfo.OriginMethodReturnType; - var awaiterType = consumableInfo.GetAwaiterMethod?.ReturnType - ?? throw new InvalidOperationException($"Bug: {nameof(consumableInfo.GetAwaiterMethod)} is null"); - callResultLocal = - ilBuilder.DeclareOptionalLocalForInstanceCall(callResultType, consumableInfo.GetAwaiterMethod); - awaiterLocal = - ilBuilder.DeclareOptionalLocalForInstanceCall(awaiterType, consumableInfo.GetResultMethod); - } - consumeEmitter.DeclareDisassemblyDiagnoserLocals(ilBuilder); var notElevenLabel = ilBuilder.DefineLabel(); @@ -867,30 +850,38 @@ .locals init ( */ EmitLoadArgFieldsToLocals(ilBuilder, argLocals, skipFirstArg); + if (consumableInfo.IsAwaitable) + { + /* + IL_0026: ldarg.0 + IL_0027: ldfld class BenchmarkDotNet.Helpers.AwaitHelper BenchmarkDotNet.Helpers.Runnable_0::awaitHelper + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, awaitHelperField); + } + /* - // return TaskSample(_argField) ... ; - IL_0011: ldarg.0 - IL_0012: ldloc.0 - IL_0013: call instance class [mscorlib]System.Threading.Tasks.Task`1 [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::TaskSample(int64) - IL_0018: ret + IL_0026: ldarg.0 + IL_0027: ldloc.0 + IL_0028: ldloc.1 + IL_0029: ldloc.2 + IL_002a: ldloc.3 + IL_002b: call instance class [System.Private.CoreLib]System.Threading.Tasks.Task`1 BenchmarkDotNet.Helpers.Runnable_0::WorkloadMethod(string, string, string, string) */ - if (!workloadMethod.IsStatic) + { ilBuilder.Emit(OpCodes.Ldarg_0); + } ilBuilder.EmitLdLocals(argLocals); ilBuilder.Emit(OpCodes.Call, workloadMethod); if (consumableInfo.IsAwaitable) { /* - // ... .GetAwaiter().GetResult(); - IL_0007: callvirt instance valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1 class [mscorlib]System.Threading.Tasks.Task`1::GetAwaiter() - IL_000c: stloc.0 - IL_000d: ldloca.s 0 - IL_000f: call instance !0 valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1::GetResult() - */ - ilBuilder.EmitInstanceCallThisValueOnStack(callResultLocal, consumableInfo.GetAwaiterMethod); - ilBuilder.EmitInstanceCallThisValueOnStack(awaiterLocal, consumableInfo.GetResultMethod); + // awaitHelper.GetResult(...); + IL_0036: callvirt instance void BenchmarkDotNet.Helpers.AwaitHelper::GetResult(class [System.Private.CoreLib]System.Threading.Tasks.Task) + */ + ilBuilder.Emit(OpCodes.Callvirt, consumableInfo.GetResultMethod); } /* @@ -953,6 +944,7 @@ private void EmitCtorBody() consumeEmitter.OnEmitCtorBody(ctorMethod, ilBuilder); + ilBuilder.EmitSetFieldToNewInstance(awaitHelperField, typeof(Helpers.AwaitHelper)); ilBuilder.EmitSetDelegateToThisField(globalSetupActionField, globalSetupMethod); ilBuilder.EmitSetDelegateToThisField(globalCleanupActionField, globalCleanupMethod); ilBuilder.EmitSetDelegateToThisField(iterationSetupActionField, iterationSetupMethod); diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableConstants.cs b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableConstants.cs index c6e8cd8ae1..920fabb03e 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableConstants.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableConstants.cs @@ -16,6 +16,7 @@ public class RunnableConstants public const string ArgFieldPrefix = "__argField"; public const string ArgParamPrefix = "arg"; + public const string AwaitHelperFieldName = "awaitHelper"; public const string GlobalSetupActionFieldName = "globalSetupAction"; public const string GlobalCleanupActionFieldName = "globalCleanupAction"; public const string IterationSetupActionFieldName = "iterationSetupAction"; diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs index eef3ce8997..a139adbafc 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs @@ -69,6 +69,7 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionTask : BenchmarkActionBase { + private readonly Helpers.AwaitHelper awaitHelper = new Helpers.AwaitHelper(); private readonly Func startTaskCallback; private readonly Action callback; private readonly Action unrolledCallback; @@ -97,7 +98,7 @@ public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) private void Overhead() { } // must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate - private void ExecuteBlocking() => startTaskCallback.Invoke().GetAwaiter().GetResult(); + private void ExecuteBlocking() => awaitHelper.GetResult(startTaskCallback.Invoke()); private void InvokeMultipleHardcoded(long repeatCount) { @@ -108,6 +109,7 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionTask : BenchmarkActionBase { + private readonly Helpers.AwaitHelper awaitHelper = new Helpers.AwaitHelper(); private readonly Func> startTaskCallback; private readonly Func callback; private readonly Func unrolledCallback; @@ -135,7 +137,7 @@ public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) private T Overhead() => default; // must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate - private T ExecuteBlocking() => startTaskCallback().GetAwaiter().GetResult(); + private T ExecuteBlocking() => awaitHelper.GetResult(startTaskCallback.Invoke()); private void InvokeSingleHardcoded() => result = callback(); @@ -150,6 +152,7 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionValueTask : BenchmarkActionBase { + private readonly Helpers.AwaitHelper awaitHelper = new Helpers.AwaitHelper(); private readonly Func> startTaskCallback; private readonly Func callback; private readonly Func unrolledCallback; @@ -178,7 +181,7 @@ public BenchmarkActionValueTask(object instance, MethodInfo method, int unrollFa private T Overhead() => default; // must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate - private T ExecuteBlocking() => startTaskCallback().GetAwaiter().GetResult(); + private T ExecuteBlocking() => awaitHelper.GetResult(startTaskCallback.Invoke()); private void InvokeSingleHardcoded() => result = callback(); diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs b/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs index 5ca5592e1e..9cb9d4c2a2 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs @@ -83,6 +83,7 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionTask : BenchmarkActionBase { + private readonly Helpers.AwaitHelper awaitHelper = new Helpers.AwaitHelper(); private readonly Func startTaskCallback; private readonly Action callback; private readonly Action unrolledCallback; @@ -117,7 +118,7 @@ public BenchmarkActionTask(object instance, MethodInfo method, BenchmarkActionCo private void Overhead() { } // must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate - private void ExecuteBlocking() => startTaskCallback.Invoke().GetAwaiter().GetResult(); + private void ExecuteBlocking() => awaitHelper.GetResult(startTaskCallback.Invoke()); private void InvokeMultipleHardcoded(long repeatCount) { @@ -128,6 +129,7 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionTask : BenchmarkActionBase { + private readonly Helpers.AwaitHelper awaitHelper = new Helpers.AwaitHelper(); private readonly Func> startTaskCallback; private readonly Func callback; private readonly Func unrolledCallback; @@ -162,7 +164,7 @@ public BenchmarkActionTask(object instance, MethodInfo method, BenchmarkActionCo private T Overhead() => default; // must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate - private T ExecuteBlocking() => startTaskCallback().GetAwaiter().GetResult(); + private T ExecuteBlocking() => awaitHelper.GetResult(startTaskCallback()); private void InvokeSingleHardcoded() => result = callback(); @@ -177,6 +179,7 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionValueTask : BenchmarkActionBase { + private readonly Helpers.AwaitHelper awaitHelper = new Helpers.AwaitHelper(); private readonly Func> startTaskCallback; private readonly Func callback; private readonly Func unrolledCallback; @@ -211,7 +214,7 @@ public BenchmarkActionValueTask(object instance, MethodInfo method, BenchmarkAct private T Overhead() => default; // must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate - private T ExecuteBlocking() => startTaskCallback().GetAwaiter().GetResult(); + private T ExecuteBlocking() => awaitHelper.GetResult(startTaskCallback()); private void InvokeSingleHardcoded() => result = callback(); diff --git a/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs b/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs index 92d7422ae2..8c644c11f8 100644 --- a/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs +++ b/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs @@ -5,12 +5,15 @@ using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Running; namespace BenchmarkDotNet.Validators { public abstract class ExecutionValidatorBase : IValidator { + protected AwaitHelper awaitHelper = new (); + protected ExecutionValidatorBase(bool failOnError) { TreatsWarningsAsErrors = failOnError; @@ -130,21 +133,8 @@ private void TryToGetTaskResult(object result) return; } - var returnType = result.GetType(); - if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) - { - var asTaskMethod = result.GetType().GetMethod("AsTask"); - result = asTaskMethod.Invoke(result, null); - } - - if (result is Task task) - { - task.GetAwaiter().GetResult(); - } - else if (result is ValueTask valueTask) - { - valueTask.GetAwaiter().GetResult(); - } + AwaitHelper.GetGetResultMethod(result.GetType()) + ?.Invoke(awaitHelper, new[] { result }); } private bool TryToSetParamsFields(object benchmarkTypeInstance, List errors) diff --git a/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs b/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs index 6975a01401..314e183d2b 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs @@ -49,13 +49,20 @@ public AllSetupAndCleanupTest(ITestOutputHelper output) : base(output) { } private static string[] GetActualLogLines(Summary summary) => GetSingleStandardOutput(summary).Where(line => line.StartsWith(Prefix)).ToArray(); - [Fact] - public void AllSetupAndCleanupMethodRunsTest() + [Theory] + [InlineData(typeof(AllSetupAndCleanupAttributeBenchmarks))] + [InlineData(typeof(AllSetupAndCleanupAttributeBenchmarksTask))] + [InlineData(typeof(AllSetupAndCleanupAttributeBenchmarksGenericTask))] + [InlineData(typeof(AllSetupAndCleanupAttributeBenchmarksValueTask))] + [InlineData(typeof(AllSetupAndCleanupAttributeBenchmarksGenericValueTask))] + [InlineData(typeof(AllSetupAndCleanupAttributeBenchmarksValueTaskSource))] + [InlineData(typeof(AllSetupAndCleanupAttributeBenchmarksGenericValueTaskSource))] + public void AllSetupAndCleanupMethodRunsTest(Type benchmarkType) { var miniJob = Job.Default.WithStrategy(RunStrategy.Monitoring).WithWarmupCount(2).WithIterationCount(3).WithInvocationCount(1).WithUnrollFactor(1).WithId("MiniJob"); var config = CreateSimpleConfig(job: miniJob); - var summary = CanExecute(config); + var summary = CanExecute(benchmarkType, config); var actualLogLines = GetActualLogLines(summary); foreach (string line in actualLogLines) @@ -84,21 +91,7 @@ public class AllSetupAndCleanupAttributeBenchmarks public void Benchmark() => Console.WriteLine(BenchmarkCalled); } - [Fact] - public void AllSetupAndCleanupMethodRunsAsyncTest() - { - var miniJob = Job.Default.WithStrategy(RunStrategy.Monitoring).WithWarmupCount(2).WithIterationCount(3).WithInvocationCount(1).WithUnrollFactor(1).WithId("MiniJob"); - var config = CreateSimpleConfig(job: miniJob); - - var summary = CanExecute(config); - - var actualLogLines = GetActualLogLines(summary); - foreach (string line in actualLogLines) - Output.WriteLine(line); - Assert.Equal(expectedLogLines, actualLogLines); - } - - public class AllSetupAndCleanupAttributeBenchmarksAsync + public class AllSetupAndCleanupAttributeBenchmarksTask { private int setupCounter; private int cleanupCounter; @@ -116,24 +109,10 @@ public class AllSetupAndCleanupAttributeBenchmarksAsync public Task GlobalCleanup() => Console.Out.WriteLineAsync(GlobalCleanupCalled); [Benchmark] - public Task Benchmark() => Console.Out.WriteLineAsync(BenchmarkCalled); - } - - [Fact] - public void AllSetupAndCleanupMethodRunsAsyncTaskSetupTest() - { - var miniJob = Job.Default.WithStrategy(RunStrategy.Monitoring).WithWarmupCount(2).WithIterationCount(3).WithInvocationCount(1).WithUnrollFactor(1).WithId("MiniJob"); - var config = CreateSimpleConfig(job: miniJob); - - var summary = CanExecute(config); - - var actualLogLines = GetActualLogLines(summary); - foreach (string line in actualLogLines) - Output.WriteLine(line); - Assert.Equal(expectedLogLines, actualLogLines); + public void Benchmark() => Console.WriteLine(BenchmarkCalled); } - public class AllSetupAndCleanupAttributeBenchmarksAsyncTaskSetup + public class AllSetupAndCleanupAttributeBenchmarksGenericTask { private int setupCounter; private int cleanupCounter; @@ -145,30 +124,47 @@ public class AllSetupAndCleanupAttributeBenchmarksAsyncTaskSetup public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); [GlobalSetup] - public Task GlobalSetup() => Console.Out.WriteLineAsync(GlobalSetupCalled); + public async Task GlobalSetup() + { + await Console.Out.WriteLineAsync(GlobalSetupCalled); + + return 42; + } [GlobalCleanup] - public Task GlobalCleanup() => Console.Out.WriteLineAsync(GlobalCleanupCalled); + public async Task GlobalCleanup() + { + await Console.Out.WriteLineAsync(GlobalCleanupCalled); + + return 42; + } [Benchmark] public void Benchmark() => Console.WriteLine(BenchmarkCalled); } - [Fact] - public void AllSetupAndCleanupMethodRunsAsyncGenericTaskSetupTest() + public class AllSetupAndCleanupAttributeBenchmarksValueTask { - var miniJob = Job.Default.WithStrategy(RunStrategy.Monitoring).WithWarmupCount(2).WithIterationCount(3).WithInvocationCount(1).WithUnrollFactor(1).WithId("MiniJob"); - var config = CreateSimpleConfig(job: miniJob); + private int setupCounter; + private int cleanupCounter; - var summary = CanExecute(config); + [IterationSetup] + public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")"); - var actualLogLines = GetActualLogLines(summary); - foreach (string line in actualLogLines) - Output.WriteLine(line); - Assert.Equal(expectedLogLines, actualLogLines); + [IterationCleanup] + public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + + [GlobalSetup] + public ValueTask GlobalSetup() => new ValueTask(Console.Out.WriteLineAsync(GlobalSetupCalled)); + + [GlobalCleanup] + public ValueTask GlobalCleanup() => new ValueTask(Console.Out.WriteLineAsync(GlobalCleanupCalled)); + + [Benchmark] + public void Benchmark() => Console.WriteLine(BenchmarkCalled); } - public class AllSetupAndCleanupAttributeBenchmarksAsyncGenericTaskSetup + public class AllSetupAndCleanupAttributeBenchmarksGenericValueTask { private int setupCounter; private int cleanupCounter; @@ -180,7 +176,7 @@ public class AllSetupAndCleanupAttributeBenchmarksAsyncGenericTaskSetup public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); [GlobalSetup] - public async Task GlobalSetup() + public async ValueTask GlobalSetup() { await Console.Out.WriteLineAsync(GlobalSetupCalled); @@ -188,7 +184,7 @@ public async Task GlobalSetup() } [GlobalCleanup] - public async Task GlobalCleanup() + public async ValueTask GlobalCleanup() { await Console.Out.WriteLineAsync(GlobalCleanupCalled); @@ -199,22 +195,9 @@ public async Task GlobalCleanup() public void Benchmark() => Console.WriteLine(BenchmarkCalled); } - [Fact] - public void AllSetupAndCleanupMethodRunsAsyncValueTaskSetupTest() - { - var miniJob = Job.Default.WithStrategy(RunStrategy.Monitoring).WithWarmupCount(2).WithIterationCount(3).WithInvocationCount(1).WithUnrollFactor(1).WithId("MiniJob"); - var config = CreateSimpleConfig(job: miniJob); - - var summary = CanExecute(config); - - var actualLogLines = GetActualLogLines(summary); - foreach (string line in actualLogLines) - Output.WriteLine(line); - Assert.Equal(expectedLogLines, actualLogLines); - } - - public class AllSetupAndCleanupAttributeBenchmarksAsyncValueTaskSetup + public class AllSetupAndCleanupAttributeBenchmarksValueTaskSource { + private readonly ValueTaskSource valueTaskSource = new (); private int setupCounter; private int cleanupCounter; @@ -225,31 +208,28 @@ public class AllSetupAndCleanupAttributeBenchmarksAsyncValueTaskSetup public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); [GlobalSetup] - public ValueTask GlobalSetup() => new ValueTask(Console.Out.WriteLineAsync(GlobalSetupCalled)); + public ValueTask GlobalSetup() + { + valueTaskSource.Reset(); + Console.Out.WriteLineAsync(GlobalSetupCalled).ContinueWith(_ => valueTaskSource.SetResult(42)); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } [GlobalCleanup] - public ValueTask GlobalCleanup() => new ValueTask(Console.Out.WriteLineAsync(GlobalCleanupCalled)); + public ValueTask GlobalCleanup() + { + valueTaskSource.Reset(); + Console.Out.WriteLineAsync(GlobalCleanupCalled).ContinueWith(_ => valueTaskSource.SetResult(42)); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } [Benchmark] public void Benchmark() => Console.WriteLine(BenchmarkCalled); } - [FactNotGitHubActionsWindows] - public void AllSetupAndCleanupMethodRunsAsyncGenericValueTaskSetupTest() - { - var miniJob = Job.Default.WithStrategy(RunStrategy.Monitoring).WithWarmupCount(2).WithIterationCount(3).WithInvocationCount(1).WithUnrollFactor(1).WithId("MiniJob"); - var config = CreateSimpleConfig(job: miniJob); - - var summary = CanExecute(config); - - var actualLogLines = GetActualLogLines(summary); - foreach (string line in actualLogLines) - Output.WriteLine(line); - Assert.Equal(expectedLogLines, actualLogLines); - } - - public class AllSetupAndCleanupAttributeBenchmarksAsyncGenericValueTaskSetup + public class AllSetupAndCleanupAttributeBenchmarksGenericValueTaskSource { + private readonly ValueTaskSource valueTaskSource = new (); private int setupCounter; private int cleanupCounter; @@ -260,19 +240,19 @@ public class AllSetupAndCleanupAttributeBenchmarksAsyncGenericValueTaskSetup public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); [GlobalSetup] - public async ValueTask GlobalSetup() + public ValueTask GlobalSetup() { - await Console.Out.WriteLineAsync(GlobalSetupCalled); - - return 42; + valueTaskSource.Reset(); + Console.Out.WriteLineAsync(GlobalSetupCalled).ContinueWith(_ => valueTaskSource.SetResult(42)); + return new ValueTask(valueTaskSource, valueTaskSource.Token); } [GlobalCleanup] - public async ValueTask GlobalCleanup() + public ValueTask GlobalCleanup() { - await Console.Out.WriteLineAsync(GlobalCleanupCalled); - - return 42; + valueTaskSource.Reset(); + Console.Out.WriteLineAsync(GlobalCleanupCalled).ContinueWith(_ => valueTaskSource.SetResult(42)); + return new ValueTask(valueTaskSource, valueTaskSource.Token); } [Benchmark] diff --git a/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs b/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs index 8eb149b5dc..e9d1f3785e 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs @@ -1,10 +1,27 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; using BenchmarkDotNet.Attributes; using Xunit; using Xunit.Abstractions; namespace BenchmarkDotNet.IntegrationTests { + internal class ValueTaskSource : IValueTaskSource, IValueTaskSource + { + private ManualResetValueTaskSourceCore _core; + + T IValueTaskSource.GetResult(short token) => _core.GetResult(token); + void IValueTaskSource.GetResult(short token) => _core.GetResult(token); + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _core.GetStatus(token); + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _core.GetStatus(token); + void IValueTaskSource.OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags); + void IValueTaskSource.OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags); + public void Reset() => _core.Reset(); + public short Token => _core.Version; + public void SetResult(T result) => _core.SetResult(result); + } + public class AsyncBenchmarksTests : BenchmarkTestExecutor { public AsyncBenchmarksTests(ITestOutputHelper output) : base(output) { } @@ -26,6 +43,8 @@ public void TaskReturningMethodsAreAwaited() public class TaskDelayMethods { + private readonly ValueTaskSource valueTaskSource = new (); + private const int MillisecondsDelay = 100; internal const double NanosecondsDelay = MillisecondsDelay * 1e+6; @@ -39,6 +58,17 @@ public class TaskDelayMethods [Benchmark] public ValueTask ReturningValueTask() => new ValueTask(Task.Delay(MillisecondsDelay)); + [Benchmark] + public ValueTask ReturningValueTaskBackByIValueTaskSource() + { + valueTaskSource.Reset(); + Task.Delay(MillisecondsDelay).ContinueWith(_ => + { + valueTaskSource.SetResult(default); + }); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } + [Benchmark] public async Task Awaiting() => await Task.Delay(MillisecondsDelay); @@ -47,6 +77,17 @@ public class TaskDelayMethods [Benchmark] public ValueTask ReturningGenericValueTask() => new ValueTask(ReturningGenericTask()); + + [Benchmark] + public ValueTask ReturningGenericValueTaskBackByIValueTaskSource() + { + valueTaskSource.Reset(); + Task.Delay(MillisecondsDelay).ContinueWith(_ => + { + valueTaskSource.SetResult(default); + }); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } } } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs index 227e6411ed..2a07e942e9 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs @@ -149,6 +149,13 @@ public async Task InvokeOnceTaskAsync() Interlocked.Increment(ref Counter); } + [Benchmark] + public async ValueTask InvokeOnceValueTaskAsync() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + [Benchmark] public string InvokeOnceRefType() { @@ -191,6 +198,13 @@ public static async Task InvokeOnceStaticTaskAsync() Interlocked.Increment(ref Counter); } + [Benchmark] + public static async ValueTask InvokeOnceStaticValueTaskAsync() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + [Benchmark] public static string InvokeOnceStaticRefType() { diff --git a/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs b/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs index 6ca7394f4d..49313c14db 100644 --- a/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs +++ b/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using System.Threading.Tasks.Sources; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; using BenchmarkDotNet.Validators; @@ -597,5 +598,82 @@ public async ValueTask GlobalCleanup() [Benchmark] public void NonThrowing() { } } + + private class ValueTaskSource : IValueTaskSource, IValueTaskSource + { + private ManualResetValueTaskSourceCore _core; + + T IValueTaskSource.GetResult(short token) => _core.GetResult(token); + void IValueTaskSource.GetResult(short token) => _core.GetResult(token); + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _core.GetStatus(token); + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _core.GetStatus(token); + void IValueTaskSource.OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags); + void IValueTaskSource.OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags); + public void Reset() => _core.Reset(); + public short Token => _core.Version; + public void SetResult(T result) => _core.SetResult(result); + } + + [Fact] + public void AsyncValueTaskBackedByIValueTaskSourceIsAwaitedProperly() + { + var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncValueTaskSource))).ToList(); + + Assert.True(AsyncValueTaskSource.WasCalled); + Assert.Empty(validationErrors); + } + + public class AsyncValueTaskSource + { + private readonly ValueTaskSource valueTaskSource = new (); + + public static bool WasCalled; + + [GlobalSetup] + public ValueTask GlobalSetup() + { + valueTaskSource.Reset(); + Task.Delay(1).ContinueWith(_ => + { + WasCalled = true; + valueTaskSource.SetResult(true); + }); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } + + [Benchmark] + public void NonThrowing() { } + } + + [Fact] + public void AsyncGenericValueTaskBackedByIValueTaskSourceIsAwaitedProperly() + { + var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncGenericValueTaskSource))).ToList(); + + Assert.True(AsyncGenericValueTaskSource.WasCalled); + Assert.Empty(validationErrors); + } + + public class AsyncGenericValueTaskSource + { + private readonly ValueTaskSource valueTaskSource = new (); + + public static bool WasCalled; + + [GlobalSetup] + public ValueTask GlobalSetup() + { + valueTaskSource.Reset(); + Task.Delay(1).ContinueWith(_ => + { + WasCalled = true; + valueTaskSource.SetResult(1); + }); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } + + [Benchmark] + public void NonThrowing() { } + } } } \ No newline at end of file From 4b3d20aaa1acbde016147006a22fb109d4b2caa9 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 25 Sep 2022 15:05:01 -0400 Subject: [PATCH 02/13] Adjust `AwaitHelper` to allow multiple threads to use it concurrently. --- src/BenchmarkDotNet/Helpers/AwaitHelper.cs | 116 ++++++++++++--------- 1 file changed, 67 insertions(+), 49 deletions(-) diff --git a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs index eb27c4cfa2..85b9829b18 100644 --- a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs +++ b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs @@ -8,77 +8,95 @@ namespace BenchmarkDotNet.Helpers { public class AwaitHelper { - private readonly object awaiterLock = new object(); - private readonly Action awaiterCallback; - private bool awaiterCompleted; - - public AwaitHelper() + private class ValueTaskWaiter { - awaiterCallback = AwaiterCallback; - } + private readonly Action awaiterCallback; + private bool awaiterCompleted; - private void AwaiterCallback() - { - lock (awaiterLock) + internal ValueTaskWaiter() { - awaiterCompleted = true; - System.Threading.Monitor.Pulse(awaiterLock); + awaiterCallback = AwaiterCallback; } - } - // we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way, - // and will eventually throw actual exception, not aggregated one - public void GetResult(Task task) - { - task.GetAwaiter().GetResult(); - } + private void AwaiterCallback() + { + lock (this) + { + awaiterCompleted = true; + System.Threading.Monitor.Pulse(this); + } + } - public T GetResult(Task task) - { - return task.GetAwaiter().GetResult(); - } + // Hook up a callback instead of converting to Task to prevent extra allocations on each benchmark run. + internal void GetResult(ValueTask task) + { + // Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process. + var awaiter = task.ConfigureAwait(false).GetAwaiter(); + if (!awaiter.IsCompleted) + { + lock (this) + { + awaiterCompleted = false; + awaiter.UnsafeOnCompleted(awaiterCallback); + // Check if the callback executed synchronously before blocking. + if (!awaiterCompleted) + { + System.Threading.Monitor.Wait(this); + } + } + } + awaiter.GetResult(); + } - // It is illegal to call GetResult from an uncomplete ValueTask, so we must hook up a callback. - public void GetResult(ValueTask task) - { - // Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process. - var awaiter = task.ConfigureAwait(false).GetAwaiter(); - if (!awaiter.IsCompleted) + internal T GetResult(ValueTask task) { - lock (awaiterLock) + // Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process. + var awaiter = task.ConfigureAwait(false).GetAwaiter(); + if (!awaiter.IsCompleted) { - awaiterCompleted = false; - awaiter.UnsafeOnCompleted(awaiterCallback); - // Check if the callback executed synchronously before blocking. - if (!awaiterCompleted) + lock (this) { - System.Threading.Monitor.Wait(awaiterLock); + awaiterCompleted = false; + awaiter.UnsafeOnCompleted(awaiterCallback); + // Check if the callback executed synchronously before blocking. + if (!awaiterCompleted) + { + System.Threading.Monitor.Wait(this); + } } } + return awaiter.GetResult(); } - awaiter.GetResult(); } - public T GetResult(ValueTask task) + // We use thread static field so that multiple threads can use individual lock object and callback. + [ThreadStatic] + private static ValueTaskWaiter ts_valueTaskWaiter; + + private ValueTaskWaiter CurrentValueTaskWaiter { - // Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process. - var awaiter = task.ConfigureAwait(false).GetAwaiter(); - if (!awaiter.IsCompleted) + get { - lock (awaiterLock) + if (ts_valueTaskWaiter == null) { - awaiterCompleted = false; - awaiter.UnsafeOnCompleted(awaiterCallback); - // Check if the callback executed synchronously before blocking. - if (!awaiterCompleted) - { - System.Threading.Monitor.Wait(awaiterLock); - } + ts_valueTaskWaiter = new ValueTaskWaiter(); } + return ts_valueTaskWaiter; } - return awaiter.GetResult(); } + // we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way, + // and will eventually throw actual exception, not aggregated one + public void GetResult(Task task) => task.GetAwaiter().GetResult(); + + public T GetResult(Task task) => task.GetAwaiter().GetResult(); + + // ValueTask can be backed by an IValueTaskSource that only supports asynchronous awaits, so we have to hook up a callback instead of calling .GetAwaiter().GetResult() like we do for Task. + // The alternative is to convert it to Task using .AsTask(), but that causes allocations which we must avoid for memory diagnoser. + public void GetResult(ValueTask task) => CurrentValueTaskWaiter.GetResult(task); + + public T GetResult(ValueTask task) => CurrentValueTaskWaiter.GetResult(task); + internal static MethodInfo GetGetResultMethod(Type taskType) { if (!taskType.IsGenericType) From f0acf70278bc351299681a705ebcd9d19ac13eda Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 26 Sep 2022 06:12:00 -0400 Subject: [PATCH 03/13] Changed AwaitHelper to static. --- .../Code/DeclarationsProvider.cs | 10 +- src/BenchmarkDotNet/Helpers/AwaitHelper.cs | 95 +++++++++---------- .../IlGeneratorStatementExtensions.cs | 31 ------ .../Templates/BenchmarkType.txt | 4 - .../Emitters/RunnableEmitter.cs | 33 ++----- .../Runnable/RunnableConstants.cs | 1 - .../BenchmarkActionFactory_Implementations.cs | 9 +- .../BenchmarkActionFactory_Implementations.cs | 9 +- .../Validators/ExecutionValidatorBase.cs | 4 +- 9 files changed, 63 insertions(+), 133 deletions(-) diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs index 21e3ce54c1..7528e8ed62 100644 --- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs +++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs @@ -63,7 +63,7 @@ private string GetMethodName(MethodInfo method) (method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>) || method.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)))) { - return $"() => awaitHelper.GetResult({method.Name}())"; + return $"() => BenchmarkDotNet.Helpers.AwaitHelper.GetResult({method.Name}())"; } return method.Name; @@ -150,9 +150,9 @@ internal class TaskDeclarationsProvider : VoidDeclarationsProvider public TaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { } public override string WorkloadMethodDelegate(string passArguments) - => $"({passArguments}) => {{ awaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments})); }}"; + => $"({passArguments}) => {{ BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments})); }}"; - public override string GetWorkloadMethodCall(string passArguments) => $"awaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments}))"; + public override string GetWorkloadMethodCall(string passArguments) => $"BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments}))"; protected override Type WorkloadMethodReturnType => typeof(void); } @@ -167,8 +167,8 @@ public GenericTaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) protected override Type WorkloadMethodReturnType => Descriptor.WorkloadMethod.ReturnType.GetTypeInfo().GetGenericArguments().Single(); public override string WorkloadMethodDelegate(string passArguments) - => $"({passArguments}) => {{ return awaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments})); }}"; + => $"({passArguments}) => {{ return BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments})); }}"; - public override string GetWorkloadMethodCall(string passArguments) => $"awaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments}))"; + public override string GetWorkloadMethodCall(string passArguments) => $"BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments}))"; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs index 85b9829b18..0863f06c57 100644 --- a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs +++ b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs @@ -6,14 +6,19 @@ namespace BenchmarkDotNet.Helpers { - public class AwaitHelper + public static class AwaitHelper { private class ValueTaskWaiter { + // We use thread static field so that multiple threads can use individual lock object and callback. + [ThreadStatic] + private static ValueTaskWaiter ts_current; + internal static ValueTaskWaiter Current => ts_current ??= new ValueTaskWaiter(); + private readonly Action awaiterCallback; private bool awaiterCompleted; - internal ValueTaskWaiter() + private ValueTaskWaiter() { awaiterCallback = AwaiterCallback; } @@ -28,80 +33,70 @@ private void AwaiterCallback() } // Hook up a callback instead of converting to Task to prevent extra allocations on each benchmark run. - internal void GetResult(ValueTask task) + internal void Wait(ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) { - // Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process. - var awaiter = task.ConfigureAwait(false).GetAwaiter(); - if (!awaiter.IsCompleted) + lock (this) { - lock (this) + awaiterCompleted = false; + awaiter.UnsafeOnCompleted(awaiterCallback); + // Check if the callback executed synchronously before blocking. + if (!awaiterCompleted) { - awaiterCompleted = false; - awaiter.UnsafeOnCompleted(awaiterCallback); - // Check if the callback executed synchronously before blocking. - if (!awaiterCompleted) - { - System.Threading.Monitor.Wait(this); - } + System.Threading.Monitor.Wait(this); } } - awaiter.GetResult(); } - internal T GetResult(ValueTask task) + internal void Wait(ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) { - // Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process. - var awaiter = task.ConfigureAwait(false).GetAwaiter(); - if (!awaiter.IsCompleted) + lock (this) { - lock (this) + awaiterCompleted = false; + awaiter.UnsafeOnCompleted(awaiterCallback); + // Check if the callback executed synchronously before blocking. + if (!awaiterCompleted) { - awaiterCompleted = false; - awaiter.UnsafeOnCompleted(awaiterCallback); - // Check if the callback executed synchronously before blocking. - if (!awaiterCompleted) - { - System.Threading.Monitor.Wait(this); - } + System.Threading.Monitor.Wait(this); } } - return awaiter.GetResult(); - } - } - - // We use thread static field so that multiple threads can use individual lock object and callback. - [ThreadStatic] - private static ValueTaskWaiter ts_valueTaskWaiter; - - private ValueTaskWaiter CurrentValueTaskWaiter - { - get - { - if (ts_valueTaskWaiter == null) - { - ts_valueTaskWaiter = new ValueTaskWaiter(); - } - return ts_valueTaskWaiter; } } // we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way, // and will eventually throw actual exception, not aggregated one - public void GetResult(Task task) => task.GetAwaiter().GetResult(); + public static void GetResult(Task task) => task.GetAwaiter().GetResult(); - public T GetResult(Task task) => task.GetAwaiter().GetResult(); + public static T GetResult(Task task) => task.GetAwaiter().GetResult(); // ValueTask can be backed by an IValueTaskSource that only supports asynchronous awaits, so we have to hook up a callback instead of calling .GetAwaiter().GetResult() like we do for Task. // The alternative is to convert it to Task using .AsTask(), but that causes allocations which we must avoid for memory diagnoser. - public void GetResult(ValueTask task) => CurrentValueTaskWaiter.GetResult(task); + public static void GetResult(ValueTask task) + { + // Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process. + var awaiter = task.ConfigureAwait(false).GetAwaiter(); + if (!awaiter.IsCompleted) + { + ValueTaskWaiter.Current.Wait(awaiter); + } + awaiter.GetResult(); + } - public T GetResult(ValueTask task) => CurrentValueTaskWaiter.GetResult(task); + public static T GetResult(ValueTask task) + { + // Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process. + var awaiter = task.ConfigureAwait(false).GetAwaiter(); + if (!awaiter.IsCompleted) + { + ValueTaskWaiter.Current.Wait(awaiter); + } + return awaiter.GetResult(); + } internal static MethodInfo GetGetResultMethod(Type taskType) { if (!taskType.IsGenericType) { - return typeof(AwaitHelper).GetMethod(nameof(AwaitHelper.GetResult), BindingFlags.Public | BindingFlags.Instance, null, new Type[1] { taskType }, null); + return typeof(AwaitHelper).GetMethod(nameof(AwaitHelper.GetResult), BindingFlags.Public | BindingFlags.Static, null, new Type[1] { taskType }, null); } Type compareType = taskType.GetGenericTypeDefinition() == typeof(ValueTask<>) ? typeof(ValueTask<>) @@ -116,7 +111,7 @@ internal static MethodInfo GetGetResultMethod(Type taskType) .ReturnType .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlags.Public | BindingFlags.Instance) .ReturnType; - return typeof(AwaitHelper).GetMethods(BindingFlags.Public | BindingFlags.Instance) + return typeof(AwaitHelper).GetMethods(BindingFlags.Public | BindingFlags.Static) .First(m => { if (m.Name != nameof(AwaitHelper.GetResult)) return false; diff --git a/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs index 65b016a0dd..6d601e6495 100644 --- a/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs +++ b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs @@ -42,37 +42,6 @@ public static void EmitVoidReturn(this ILGenerator ilBuilder, MethodBuilder meth ilBuilder.Emit(OpCodes.Ret); } - public static void EmitSetFieldToNewInstance( - this ILGenerator ilBuilder, - FieldBuilder field, - Type instanceType) - { - if (field.IsStatic) - throw new ArgumentException("The field should be instance field", nameof(field)); - - if (instanceType != null) - { - /* - IL_0006: ldarg.0 - IL_0007: newobj instance void BenchmarkDotNet.Helpers.AwaitHelper::.ctor() - IL_000c: stfld class BenchmarkDotNet.Helpers.AwaitHelper BenchmarkDotNet.Autogenerated.Runnable_0::awaitHelper - */ - var ctor = instanceType.GetConstructor(Array.Empty()); - if (ctor == null) - throw new InvalidOperationException($"Bug: instanceType {instanceType.Name} does not have a 0-parameter accessible constructor."); - - ilBuilder.Emit(OpCodes.Ldarg_0); - ilBuilder.Emit(OpCodes.Newobj, ctor); - ilBuilder.Emit(OpCodes.Stfld, field); - } - else - { - ilBuilder.Emit(OpCodes.Ldarg_0); - ilBuilder.Emit(OpCodes.Ldnull); - ilBuilder.Emit(OpCodes.Stfld, field); - } - } - public static void EmitSetDelegateToThisField( this ILGenerator ilBuilder, FieldBuilder delegateField, diff --git a/src/BenchmarkDotNet/Templates/BenchmarkType.txt b/src/BenchmarkDotNet/Templates/BenchmarkType.txt index 7a215a4c93..d8f15f9138 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkType.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkType.txt @@ -57,8 +57,6 @@ public Runnable_$ID$() { - awaitHelper = new BenchmarkDotNet.Helpers.AwaitHelper(); - globalSetupAction = $GlobalSetupMethodName$; globalCleanupAction = $GlobalCleanupMethodName$; iterationSetupAction = $IterationSetupMethodName$; @@ -68,8 +66,6 @@ $InitializeArgumentFields$ } - private readonly BenchmarkDotNet.Helpers.AwaitHelper awaitHelper; - private System.Action globalSetupAction; private System.Action globalCleanupAction; private System.Action iterationSetupAction; diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs index 23b453043e..5b799a679a 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs @@ -246,7 +246,6 @@ private static void EmitNoArgsMethodCallPopReturn( private ConsumableTypeInfo consumableInfo; private ConsumeEmitter consumeEmitter; - private FieldBuilder awaitHelperField; private FieldBuilder globalSetupActionField; private FieldBuilder globalCleanupActionField; private FieldBuilder iterationSetupActionField; @@ -413,8 +412,6 @@ private Type EmitWorkloadDelegateType() private void DefineFields() { - awaitHelperField = - runnableBuilder.DefineField(AwaitHelperFieldName, typeof(Helpers.AwaitHelper), FieldAttributes.Private | FieldAttributes.InitOnly); globalSetupActionField = runnableBuilder.DefineField(GlobalSetupActionFieldName, typeof(Action), FieldAttributes.Private); globalCleanupActionField = @@ -587,13 +584,6 @@ private MethodInfo EmitWorkloadImplementation(string methodName) var ilBuilder = methodBuilder.GetILGenerator(); - /* - IL_0007: ldarg.0 - IL_0008: ldfld class BenchmarkDotNet.Helpers.AwaitHelper BenchmarkDotNet.Helpers.Runnable_0::awaitHelper - */ - ilBuilder.Emit(OpCodes.Ldarg_0); - ilBuilder.Emit(OpCodes.Ldfld, awaitHelperField); - /* IL_0026: ldarg.0 IL_0027: ldloc.0 @@ -608,11 +598,11 @@ private MethodInfo EmitWorkloadImplementation(string methodName) ilBuilder.Emit(OpCodes.Call, Descriptor.WorkloadMethod); /* - // awaitHelper.GetResult(...); - IL_000e: callvirt instance void BenchmarkDotNet.Helpers.AwaitHelper::GetResult(class [System.Private.CoreLib]System.Threading.Tasks.Task) + // BenchmarkDotNet.Helpers.AwaitHelper.GetResult(...); + IL_000e: call !!0 BenchmarkDotNet.Helpers.AwaitHelper::GetResult(valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1) */ - ilBuilder.Emit(OpCodes.Callvirt, consumableInfo.GetResultMethod); + ilBuilder.Emit(OpCodes.Call, consumableInfo.GetResultMethod); /* IL_0014: ret @@ -850,16 +840,6 @@ .locals init ( */ EmitLoadArgFieldsToLocals(ilBuilder, argLocals, skipFirstArg); - if (consumableInfo.IsAwaitable) - { - /* - IL_0026: ldarg.0 - IL_0027: ldfld class BenchmarkDotNet.Helpers.AwaitHelper BenchmarkDotNet.Helpers.Runnable_0::awaitHelper - */ - ilBuilder.Emit(OpCodes.Ldarg_0); - ilBuilder.Emit(OpCodes.Ldfld, awaitHelperField); - } - /* IL_0026: ldarg.0 IL_0027: ldloc.0 @@ -878,10 +858,10 @@ .locals init ( if (consumableInfo.IsAwaitable) { /* - // awaitHelper.GetResult(...); - IL_0036: callvirt instance void BenchmarkDotNet.Helpers.AwaitHelper::GetResult(class [System.Private.CoreLib]System.Threading.Tasks.Task) + // BenchmarkDotNet.Helpers.AwaitHelper.GetResult(...); + IL_000e: call !!0 BenchmarkDotNet.Helpers.AwaitHelper::GetResult(valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1) */ - ilBuilder.Emit(OpCodes.Callvirt, consumableInfo.GetResultMethod); + ilBuilder.Emit(OpCodes.Call, consumableInfo.GetResultMethod); } /* @@ -944,7 +924,6 @@ private void EmitCtorBody() consumeEmitter.OnEmitCtorBody(ctorMethod, ilBuilder); - ilBuilder.EmitSetFieldToNewInstance(awaitHelperField, typeof(Helpers.AwaitHelper)); ilBuilder.EmitSetDelegateToThisField(globalSetupActionField, globalSetupMethod); ilBuilder.EmitSetDelegateToThisField(globalCleanupActionField, globalCleanupMethod); ilBuilder.EmitSetDelegateToThisField(iterationSetupActionField, iterationSetupMethod); diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableConstants.cs b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableConstants.cs index 920fabb03e..c6e8cd8ae1 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableConstants.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableConstants.cs @@ -16,7 +16,6 @@ public class RunnableConstants public const string ArgFieldPrefix = "__argField"; public const string ArgParamPrefix = "arg"; - public const string AwaitHelperFieldName = "awaitHelper"; public const string GlobalSetupActionFieldName = "globalSetupAction"; public const string GlobalCleanupActionFieldName = "globalCleanupAction"; public const string IterationSetupActionFieldName = "iterationSetupAction"; diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs index a139adbafc..39ec7f5b42 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs @@ -69,7 +69,6 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionTask : BenchmarkActionBase { - private readonly Helpers.AwaitHelper awaitHelper = new Helpers.AwaitHelper(); private readonly Func startTaskCallback; private readonly Action callback; private readonly Action unrolledCallback; @@ -98,7 +97,7 @@ public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) private void Overhead() { } // must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate - private void ExecuteBlocking() => awaitHelper.GetResult(startTaskCallback.Invoke()); + private void ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); private void InvokeMultipleHardcoded(long repeatCount) { @@ -109,7 +108,6 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionTask : BenchmarkActionBase { - private readonly Helpers.AwaitHelper awaitHelper = new Helpers.AwaitHelper(); private readonly Func> startTaskCallback; private readonly Func callback; private readonly Func unrolledCallback; @@ -137,7 +135,7 @@ public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) private T Overhead() => default; // must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate - private T ExecuteBlocking() => awaitHelper.GetResult(startTaskCallback.Invoke()); + private T ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); private void InvokeSingleHardcoded() => result = callback(); @@ -152,7 +150,6 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionValueTask : BenchmarkActionBase { - private readonly Helpers.AwaitHelper awaitHelper = new Helpers.AwaitHelper(); private readonly Func> startTaskCallback; private readonly Func callback; private readonly Func unrolledCallback; @@ -181,7 +178,7 @@ public BenchmarkActionValueTask(object instance, MethodInfo method, int unrollFa private T Overhead() => default; // must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate - private T ExecuteBlocking() => awaitHelper.GetResult(startTaskCallback.Invoke()); + private T ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); private void InvokeSingleHardcoded() => result = callback(); diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs b/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs index 9cb9d4c2a2..a78b18c5ee 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs @@ -83,7 +83,6 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionTask : BenchmarkActionBase { - private readonly Helpers.AwaitHelper awaitHelper = new Helpers.AwaitHelper(); private readonly Func startTaskCallback; private readonly Action callback; private readonly Action unrolledCallback; @@ -118,7 +117,7 @@ public BenchmarkActionTask(object instance, MethodInfo method, BenchmarkActionCo private void Overhead() { } // must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate - private void ExecuteBlocking() => awaitHelper.GetResult(startTaskCallback.Invoke()); + private void ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); private void InvokeMultipleHardcoded(long repeatCount) { @@ -129,7 +128,6 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionTask : BenchmarkActionBase { - private readonly Helpers.AwaitHelper awaitHelper = new Helpers.AwaitHelper(); private readonly Func> startTaskCallback; private readonly Func callback; private readonly Func unrolledCallback; @@ -164,7 +162,7 @@ public BenchmarkActionTask(object instance, MethodInfo method, BenchmarkActionCo private T Overhead() => default; // must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate - private T ExecuteBlocking() => awaitHelper.GetResult(startTaskCallback()); + private T ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback()); private void InvokeSingleHardcoded() => result = callback(); @@ -179,7 +177,6 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionValueTask : BenchmarkActionBase { - private readonly Helpers.AwaitHelper awaitHelper = new Helpers.AwaitHelper(); private readonly Func> startTaskCallback; private readonly Func callback; private readonly Func unrolledCallback; @@ -214,7 +211,7 @@ public BenchmarkActionValueTask(object instance, MethodInfo method, BenchmarkAct private T Overhead() => default; // must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate - private T ExecuteBlocking() => awaitHelper.GetResult(startTaskCallback()); + private T ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback()); private void InvokeSingleHardcoded() => result = callback(); diff --git a/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs b/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs index 8c644c11f8..b495e4d87f 100644 --- a/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs +++ b/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs @@ -12,8 +12,6 @@ namespace BenchmarkDotNet.Validators { public abstract class ExecutionValidatorBase : IValidator { - protected AwaitHelper awaitHelper = new (); - protected ExecutionValidatorBase(bool failOnError) { TreatsWarningsAsErrors = failOnError; @@ -134,7 +132,7 @@ private void TryToGetTaskResult(object result) } AwaitHelper.GetGetResultMethod(result.GetType()) - ?.Invoke(awaitHelper, new[] { result }); + ?.Invoke(null, new[] { result }); } private bool TryToSetParamsFields(object benchmarkTypeInstance, List errors) From c4a07b0fe190992b2f98f8e1e20f7ceed391e704 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 19 Sep 2022 18:06:47 -0400 Subject: [PATCH 04/13] Fix async GlobalSetup/GlobalCleanup not being awaited with InProcessEmit toolchain. --- .../Emitters/RunnableEmitter.cs | 91 +++++++++++--- .../BenchmarkTestExecutor.cs | 2 +- .../InProcessEmitTest.cs | 114 ++++++++++++++++-- 3 files changed, 179 insertions(+), 28 deletions(-) diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs index 5b799a679a..df45229474 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs @@ -245,6 +245,10 @@ private static void EmitNoArgsMethodCallPopReturn( private TypeBuilder runnableBuilder; private ConsumableTypeInfo consumableInfo; private ConsumeEmitter consumeEmitter; + private ConsumableTypeInfo globalSetupReturnInfo; + private ConsumableTypeInfo globalCleanupReturnInfo; + private ConsumableTypeInfo iterationSetupReturnInfo; + private ConsumableTypeInfo iterationCleanupReturnInfo; private FieldBuilder globalSetupActionField; private FieldBuilder globalCleanupActionField; @@ -357,6 +361,10 @@ private void InitForEmitRunnable(BenchmarkBuildInfo newBenchmark) consumableInfo = new ConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.WorkloadMethod.ReturnType); consumeEmitter = ConsumeEmitter.GetConsumeEmitter(consumableInfo); + globalSetupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.GlobalSetupMethod?.ReturnType); + globalCleanupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.GlobalCleanupMethod?.ReturnType); + iterationSetupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.IterationSetupMethod?.ReturnType); + iterationCleanupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.IterationCleanupMethod?.ReturnType); // Init types runnableBuilder = DefineRunnableTypeBuilder(benchmark, moduleBuilder); @@ -364,6 +372,11 @@ private void InitForEmitRunnable(BenchmarkBuildInfo newBenchmark) workloadDelegateType = EmitWorkloadDelegateType(); } + private static ConsumableTypeInfo GetConsumableTypeInfo(Type methodReturnType) + { + return methodReturnType == null ? null : new ConsumableTypeInfo(methodReturnType); + } + private Type EmitOverheadDelegateType() { // .class public auto ansi sealed BenchmarkDotNet.Autogenerated.Runnable_0OverheadDelegate @@ -888,34 +901,84 @@ private void EmitSetupCleanupMethods() { // Emit Setup/Cleanup methods // We emit empty method instead of EmptyAction = "() => { }" - globalSetupMethod = EmitWrapperMethod( - GlobalSetupMethodName, - Descriptor.GlobalSetupMethod); - globalCleanupMethod = EmitWrapperMethod( - GlobalCleanupMethodName, - Descriptor.GlobalCleanupMethod); - iterationSetupMethod = EmitWrapperMethod( - IterationSetupMethodName, - Descriptor.IterationSetupMethod); - iterationCleanupMethod = EmitWrapperMethod( - IterationCleanupMethodName, - Descriptor.IterationCleanupMethod); + globalSetupMethod = EmitWrapperMethod(GlobalSetupMethodName, Descriptor.GlobalSetupMethod, globalSetupReturnInfo); + globalCleanupMethod = EmitWrapperMethod(GlobalCleanupMethodName, Descriptor.GlobalCleanupMethod, globalCleanupReturnInfo); + iterationSetupMethod = EmitWrapperMethod(IterationSetupMethodName, Descriptor.IterationSetupMethod, iterationSetupReturnInfo); + iterationCleanupMethod = EmitWrapperMethod(IterationCleanupMethodName, Descriptor.IterationCleanupMethod, iterationCleanupReturnInfo); } - private MethodBuilder EmitWrapperMethod(string methodName, MethodInfo optionalTargetMethod) + private MethodBuilder EmitWrapperMethod(string methodName, MethodInfo optionalTargetMethod, ConsumableTypeInfo returnTypeInfo) { var methodBuilder = runnableBuilder.DefinePrivateVoidInstanceMethod(methodName); var ilBuilder = methodBuilder.GetILGenerator(); if (optionalTargetMethod != null) - EmitNoArgsMethodCallPopReturn(methodBuilder, optionalTargetMethod, ilBuilder, forceDirectCall: true); + { + if (returnTypeInfo?.IsAwaitable == true) + { + EmitAwaitableSetupTeardown(methodBuilder, optionalTargetMethod, ilBuilder, returnTypeInfo); + } + else + { + EmitNoArgsMethodCallPopReturn(methodBuilder, optionalTargetMethod, ilBuilder, forceDirectCall: true); + } + } ilBuilder.EmitVoidReturn(methodBuilder); return methodBuilder; } + private void EmitAwaitableSetupTeardown( + MethodBuilder methodBuilder, + MethodInfo targetMethod, + ILGenerator ilBuilder, + ConsumableTypeInfo returnTypeInfo) + { + if (targetMethod == null) + throw new ArgumentNullException(nameof(targetMethod)); + + if (returnTypeInfo.WorkloadMethodReturnType == typeof(void)) + { + ilBuilder.Emit(OpCodes.Ldarg_0); + } + /* + // call for instance + // GlobalSetup(); + IL_0006: ldarg.0 + IL_0007: call instance void [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::GlobalSetup() + */ + /* + // call for static + // GlobalSetup(); + IL_0006: call string [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::GlobalCleanup() + */ + if (targetMethod.IsStatic) + { + ilBuilder.Emit(OpCodes.Call, targetMethod); + + } + else if (methodBuilder.IsStatic) + { + throw new InvalidOperationException( + $"[BUG] Static method {methodBuilder.Name} tries to call instance member {targetMethod.Name}"); + } + else + { + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Call, targetMethod); + } + + /* + // BenchmarkDotNet.Helpers.AwaitHelper.GetResult(...); + IL_000e: call !!0 BenchmarkDotNet.Helpers.AwaitHelper::GetResult(valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1) + */ + + ilBuilder.Emit(OpCodes.Call, consumableInfo.GetResultMethod); + ilBuilder.Emit(OpCodes.Pop); + } + private void EmitCtorBody() { var ilBuilder = ctorMethod.GetILGenerator(); diff --git a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs index 64d270b3d6..d1cbc7aae0 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs @@ -49,7 +49,7 @@ public Reports.Summary CanExecute(IConfig config = null, bool fullVa /// Optional custom config to be used instead of the default /// Optional: disable validation (default = true/enabled) /// The summary from the benchmark run - protected Reports.Summary CanExecute(Type type, IConfig config = null, bool fullValidation = true) + public Reports.Summary CanExecute(Type type, IConfig config = null, bool fullValidation = true) { // Add logging, so the Benchmark execution is in the TestRunner output (makes Debugging easier) if (config == null) diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs index 2a07e942e9..8c0782f074 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs @@ -235,35 +235,123 @@ public static ValueTask InvokeOnceStaticValueTaskOfT() } } - [Fact] - public void InProcessEmitToolchainSupportsIterationSetupAndCleanup() + [Theory] + [InlineData(typeof(IterationSetupCleanup))] + [InlineData(typeof(GlobalSetupCleanupTask))] + [InlineData(typeof(GlobalSetupCleanupValueTask))] + [InlineData(typeof(GlobalSetupCleanupValueTaskSource))] + public void InProcessEmitToolchainSupportsSetupAndCleanup(Type benchmarkType) { var logger = new OutputLogger(Output); var config = CreateInProcessConfig(logger); - WithIterationSetupAndCleanup.SetupCounter = 0; - WithIterationSetupAndCleanup.BenchmarkCounter = 0; - WithIterationSetupAndCleanup.CleanupCounter = 0; + Counters.SetupCounter = 0; + Counters.BenchmarkCounter = 0; + Counters.CleanupCounter = 0; - var summary = CanExecute(config); + var summary = CanExecute(benchmarkType, config); - Assert.Equal(1, WithIterationSetupAndCleanup.SetupCounter); - Assert.Equal(16, WithIterationSetupAndCleanup.BenchmarkCounter); - Assert.Equal(1, WithIterationSetupAndCleanup.CleanupCounter); + Assert.Equal(1, Counters.SetupCounter); + Assert.Equal(16, Counters.BenchmarkCounter); + Assert.Equal(1, Counters.CleanupCounter); } - public class WithIterationSetupAndCleanup + private static class Counters { public static int SetupCounter, BenchmarkCounter, CleanupCounter; + } + public class IterationSetupCleanup + { [IterationSetup] - public void Setup() => Interlocked.Increment(ref SetupCounter); + public void Setup() => Interlocked.Increment(ref Counters.SetupCounter); [Benchmark] - public void Benchmark() => Interlocked.Increment(ref BenchmarkCounter); + public void Benchmark() => Interlocked.Increment(ref Counters.BenchmarkCounter); [IterationCleanup] - public void Cleanup() => Interlocked.Increment(ref CleanupCounter); + public void Cleanup() => Interlocked.Increment(ref Counters.CleanupCounter); + } + + public class GlobalSetupCleanupTask + { + [GlobalSetup] + public static async Task GlobalSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.SetupCounter); + } + + [GlobalCleanup] + public async Task GlobalCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.CleanupCounter); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counters.BenchmarkCounter); + } + } + + public class GlobalSetupCleanupValueTask + { + [GlobalSetup] + public static async ValueTask GlobalSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.SetupCounter); + } + + [GlobalCleanup] + public async ValueTask GlobalCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.CleanupCounter); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counters.BenchmarkCounter); + } + } + + public class GlobalSetupCleanupValueTaskSource + { + private readonly static ValueTaskSource valueTaskSource = new (); + + [GlobalSetup] + public static ValueTask GlobalSetup() + { + valueTaskSource.Reset(); + Task.Delay(1).ContinueWith(_ => + { + Interlocked.Increment(ref Counters.SetupCounter); + valueTaskSource.SetResult(42); + }); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } + + [GlobalCleanup] + public ValueTask GlobalCleanup() + { + valueTaskSource.Reset(); + Task.Delay(1).ContinueWith(_ => + { + Interlocked.Increment(ref Counters.CleanupCounter); + valueTaskSource.SetResult(42); + }); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counters.BenchmarkCounter); + } } } } \ No newline at end of file From 4d6159cc1d1168caa8d25b3010ddd880add4490a Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 19 Sep 2022 18:06:47 -0400 Subject: [PATCH 05/13] Added missing ValueTask (non-generic) to InProcess (no emit) toolchains. --- .../BenchmarkActionFactory.cs | 3 + .../BenchmarkActionFactory_Implementations.cs | 39 ++++ .../InProcess/BenchmarkActionFactory.cs | 3 + .../BenchmarkActionFactory_Implementations.cs | 45 ++++ .../InProcessTest.cs | 200 +++++++++++++++--- 5 files changed, 265 insertions(+), 25 deletions(-) diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory.cs b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory.cs index ef351975a1..130c67dbf3 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory.cs @@ -32,6 +32,9 @@ private static BenchmarkAction CreateCore( if (resultType == typeof(Task)) return new BenchmarkActionTask(resultInstance, targetMethod, unrollFactor); + if (resultType == typeof(ValueTask)) + return new BenchmarkActionValueTask(resultInstance, targetMethod, unrollFactor); + if (resultType.GetTypeInfo().IsGenericType) { var genericType = resultType.GetGenericTypeDefinition(); diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs index 39ec7f5b42..2ed1f7ccc6 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs @@ -148,6 +148,45 @@ private void InvokeMultipleHardcoded(long repeatCount) public override object LastRunResult => result; } + internal class BenchmarkActionValueTask : BenchmarkActionBase + { + private readonly Func startTaskCallback; + private readonly Action callback; + private readonly Action unrolledCallback; + + public BenchmarkActionValueTask(object instance, MethodInfo method, int unrollFactor) + { + bool isIdle = method == null; + if (!isIdle) + { + startTaskCallback = CreateWorkload>(instance, method); + callback = ExecuteBlocking; + } + else + { + callback = Overhead; + } + + InvokeSingle = callback; + + unrolledCallback = Unroll(callback, unrollFactor); + InvokeMultiple = InvokeMultipleHardcoded; + + } + + // must be kept in sync with VoidDeclarationsProvider.IdleImplementation + private void Overhead() { } + + // must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate + private void ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); + + private void InvokeMultipleHardcoded(long repeatCount) + { + for (long i = 0; i < repeatCount; i++) + unrolledCallback(); + } + } + internal class BenchmarkActionValueTask : BenchmarkActionBase { private readonly Func> startTaskCallback; diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory.cs b/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory.cs index 0774339ad2..987a4c81b6 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory.cs @@ -34,6 +34,9 @@ private static BenchmarkAction CreateCore( if (resultType == typeof(Task)) return new BenchmarkActionTask(resultInstance, targetMethod, codegenMode, unrollFactor); + if (resultType == typeof(ValueTask)) + return new BenchmarkActionValueTask(resultInstance, targetMethod, codegenMode, unrollFactor); + if (resultType.GetTypeInfo().IsGenericType) { var genericType = resultType.GetGenericTypeDefinition(); diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs b/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs index a78b18c5ee..5ebd1999c5 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs @@ -175,6 +175,51 @@ private void InvokeMultipleHardcoded(long repeatCount) public override object LastRunResult => result; } + internal class BenchmarkActionValueTask : BenchmarkActionBase + { + private readonly Func startTaskCallback; + private readonly Action callback; + private readonly Action unrolledCallback; + + public BenchmarkActionValueTask(object instance, MethodInfo method, BenchmarkActionCodegen codegenMode, int unrollFactor) + { + bool isIdle = method == null; + if (!isIdle) + { + startTaskCallback = CreateWorkload>(instance, method); + callback = ExecuteBlocking; + } + else + { + callback = Overhead; + } + + InvokeSingle = callback; + + if (UseFallbackCode(codegenMode, unrollFactor)) + { + unrolledCallback = Unroll(callback, unrollFactor); + InvokeMultiple = InvokeMultipleHardcoded; + } + else + { + InvokeMultiple = EmitInvokeMultiple(this, nameof(callback), null, unrollFactor); + } + } + + // must be kept in sync with VoidDeclarationsProvider.IdleImplementation + private void Overhead() { } + + // must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate + private void ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); + + private void InvokeMultipleHardcoded(long repeatCount) + { + for (long i = 0; i < repeatCount; i++) + unrolledCallback(); + } + } + internal class BenchmarkActionValueTask : BenchmarkActionBase { private readonly Func> startTaskCallback; diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs index 89120e84c4..544b847304 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs @@ -44,6 +44,9 @@ public InProcessTest(ITestOutputHelper output) : base(output) [Fact] public void BenchmarkActionTaskSupported() => TestInvoke(x => x.InvokeOnceTaskAsync(), UnrollFactor, null); + [Fact] + public void BenchmarkActionValueTaskSupported() => TestInvoke(x => x.InvokeOnceValueTaskAsync(), UnrollFactor, null); + [Fact] public void BenchmarkActionRefTypeSupported() => TestInvoke(x => x.InvokeOnceRefType(), UnrollFactor, StringResult); @@ -56,6 +59,18 @@ public InProcessTest(ITestOutputHelper output) : base(output) [Fact] public void BenchmarkActionValueTaskOfTSupported() => TestInvoke(x => x.InvokeOnceValueTaskOfT(), UnrollFactor, DecimalResult); + [Fact] + public void BenchmarkActionGlobalSetupTaskSupported() => TestInvokeSetupCleanupTask(x => BenchmarkSetupCleanupTask.GlobalSetup(), UnrollFactor); + + [Fact] + public void BenchmarkActionGlobalCleanupTaskSupported() => TestInvokeSetupCleanupTask(x => x.GlobalCleanup(), UnrollFactor); + + [Fact] + public void BenchmarkActionGlobalSetupValueTaskSupported() => TestInvokeSetupCleanupValueTask(x => BenchmarkSetupCleanupValueTask.GlobalSetup(), UnrollFactor); + + [Fact] + public void BenchmarkActionGlobalCleanupValueTaskSupported() => TestInvokeSetupCleanupValueTask(x => x.GlobalCleanup(), UnrollFactor); + [AssertionMethod] private void TestInvoke(Expression> methodCall, int unrollFactor) { @@ -64,34 +79,34 @@ private void TestInvoke(Expression> methodCall, int un // Run mode var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkAllCases(), BenchmarkActionCodegen.ReflectionEmit, unrollFactor); - TestInvoke(action, unrollFactor, false, null); + TestInvoke(action, unrollFactor, false, null, ref BenchmarkAllCases.Counter); action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkAllCases(), BenchmarkActionCodegen.DelegateCombine, unrollFactor); - TestInvoke(action, unrollFactor, false, null); + TestInvoke(action, unrollFactor, false, null, ref BenchmarkAllCases.Counter); // Idle mode action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkAllCases(), BenchmarkActionCodegen.ReflectionEmit, unrollFactor); - TestInvoke(action, unrollFactor, true, null); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkAllCases.Counter); action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkAllCases(), BenchmarkActionCodegen.DelegateCombine, unrollFactor); - TestInvoke(action, unrollFactor, true, null); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkAllCases.Counter); // GlobalSetup/GlobalCleanup action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkAllCases()); - TestInvoke(action, 1, false, null); + TestInvoke(action, 1, false, null, ref BenchmarkAllCases.Counter); action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkAllCases()); - TestInvoke(action, 1, false, null); + TestInvoke(action, 1, false, null, ref BenchmarkAllCases.Counter); // GlobalSetup/GlobalCleanup (empty) descriptor = new Descriptor(typeof(BenchmarkAllCases), targetMethod); action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkAllCases()); - TestInvoke(action, unrollFactor, true, null); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkAllCases.Counter); action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkAllCases()); - TestInvoke(action, unrollFactor, true, null); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkAllCases.Counter); // Dummy (just in case something may broke) action = BenchmarkActionFactory.CreateDummy(); - TestInvoke(action, unrollFactor, true, null); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkAllCases.Counter); action = BenchmarkActionFactory.CreateDummy(); - TestInvoke(action, unrollFactor, true, null); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkAllCases.Counter); } [AssertionMethod] @@ -102,9 +117,9 @@ private void TestInvoke(Expression> methodCall, in // Run mode var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkAllCases(), BenchmarkActionCodegen.ReflectionEmit, unrollFactor); - TestInvoke(action, unrollFactor, false, expectedResult); + TestInvoke(action, unrollFactor, false, expectedResult, ref BenchmarkAllCases.Counter); action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkAllCases(), BenchmarkActionCodegen.DelegateCombine, unrollFactor); - TestInvoke(action, unrollFactor, false, expectedResult); + TestInvoke(action, unrollFactor, false, expectedResult, ref BenchmarkAllCases.Counter); // Idle mode @@ -113,17 +128,93 @@ private void TestInvoke(Expression> methodCall, in object idleExpected; if (isValueTask) idleExpected = GetDefault(typeof(T).GetGenericArguments()[0]); + else if (expectedResult == null || typeof(T) == typeof(Task) || typeof(T) == typeof(ValueTask)) + idleExpected = null; else if (typeof(T).GetTypeInfo().IsValueType) idleExpected = 0; - else if (expectedResult == null || typeof(T) == typeof(Task)) - idleExpected = null; else idleExpected = GetDefault(expectedResult.GetType()); action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkAllCases(), BenchmarkActionCodegen.ReflectionEmit, unrollFactor); - TestInvoke(action, unrollFactor, true, idleExpected); + TestInvoke(action, unrollFactor, true, idleExpected, ref BenchmarkAllCases.Counter); action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkAllCases(), BenchmarkActionCodegen.DelegateCombine, unrollFactor); - TestInvoke(action, unrollFactor, true, idleExpected); + TestInvoke(action, unrollFactor, true, idleExpected, ref BenchmarkAllCases.Counter); + } + + [AssertionMethod] + private void TestInvokeSetupCleanupTask(Expression> methodCall, int unrollFactor) + { + var targetMethod = ((MethodCallExpression) methodCall.Body).Method; + var descriptor = new Descriptor(typeof(BenchmarkSetupCleanupTask), targetMethod, targetMethod, targetMethod, targetMethod, targetMethod); + + // Run mode + var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkSetupCleanupTask(), BenchmarkActionCodegen.ReflectionEmit, unrollFactor); + TestInvoke(action, unrollFactor, false, null, ref BenchmarkSetupCleanupTask.Counter); + action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkSetupCleanupTask(), BenchmarkActionCodegen.DelegateCombine, unrollFactor); + TestInvoke(action, unrollFactor, false, null, ref BenchmarkSetupCleanupTask.Counter); + + // Idle mode + action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkSetupCleanupTask(), BenchmarkActionCodegen.ReflectionEmit, unrollFactor); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkSetupCleanupTask.Counter); + action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkSetupCleanupTask(), BenchmarkActionCodegen.DelegateCombine, unrollFactor); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkSetupCleanupTask.Counter); + + // GlobalSetup/GlobalCleanup + action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkSetupCleanupTask()); + TestInvoke(action, 1, false, null, ref BenchmarkSetupCleanupTask.Counter); + action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkSetupCleanupTask()); + TestInvoke(action, 1, false, null, ref BenchmarkSetupCleanupTask.Counter); + + // GlobalSetup/GlobalCleanup (empty) + descriptor = new Descriptor(typeof(BenchmarkSetupCleanupTask), targetMethod); + action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkSetupCleanupTask()); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkSetupCleanupTask.Counter); + action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkSetupCleanupTask()); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkSetupCleanupTask.Counter); + + // Dummy (just in case something may broke) + action = BenchmarkActionFactory.CreateDummy(); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkSetupCleanupTask.Counter); + action = BenchmarkActionFactory.CreateDummy(); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkSetupCleanupTask.Counter); + } + + [AssertionMethod] + private void TestInvokeSetupCleanupValueTask(Expression> methodCall, int unrollFactor) + { + var targetMethod = ((MethodCallExpression) methodCall.Body).Method; + var descriptor = new Descriptor(typeof(BenchmarkSetupCleanupValueTask), targetMethod, targetMethod, targetMethod, targetMethod, targetMethod); + + // Run mode + var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkSetupCleanupValueTask(), BenchmarkActionCodegen.ReflectionEmit, unrollFactor); + TestInvoke(action, unrollFactor, false, null, ref BenchmarkSetupCleanupValueTask.Counter); + action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkSetupCleanupValueTask(), BenchmarkActionCodegen.DelegateCombine, unrollFactor); + TestInvoke(action, unrollFactor, false, null, ref BenchmarkSetupCleanupValueTask.Counter); + + // Idle mode + action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkSetupCleanupValueTask(), BenchmarkActionCodegen.ReflectionEmit, unrollFactor); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkSetupCleanupValueTask.Counter); + action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkSetupCleanupValueTask(), BenchmarkActionCodegen.DelegateCombine, unrollFactor); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkSetupCleanupValueTask.Counter); + + // GlobalSetup/GlobalCleanup + action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkSetupCleanupValueTask()); + TestInvoke(action, 1, false, null, ref BenchmarkSetupCleanupValueTask.Counter); + action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkSetupCleanupValueTask()); + TestInvoke(action, 1, false, null, ref BenchmarkSetupCleanupValueTask.Counter); + + // GlobalSetup/GlobalCleanup (empty) + descriptor = new Descriptor(typeof(BenchmarkSetupCleanupValueTask), targetMethod); + action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkSetupCleanupValueTask()); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkSetupCleanupValueTask.Counter); + action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkSetupCleanupValueTask()); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkSetupCleanupValueTask.Counter); + + // Dummy (just in case something may broke) + action = BenchmarkActionFactory.CreateDummy(); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkSetupCleanupValueTask.Counter); + action = BenchmarkActionFactory.CreateDummy(); + TestInvoke(action, unrollFactor, true, null, ref BenchmarkSetupCleanupValueTask.Counter); } private static object GetDefault(Type type) @@ -136,36 +227,36 @@ private static object GetDefault(Type type) } [AssertionMethod] - private void TestInvoke(BenchmarkAction benchmarkAction, int unrollFactor, bool isIdle, object expectedResult) + private void TestInvoke(BenchmarkAction benchmarkAction, int unrollFactor, bool isIdle, object expectedResult, ref int counter) { try { - BenchmarkAllCases.Counter = 0; + counter = 0; if (isIdle) { benchmarkAction.InvokeSingle(); - Assert.Equal(0, BenchmarkAllCases.Counter); + Assert.Equal(0, counter); benchmarkAction.InvokeMultiple(0); - Assert.Equal(0, BenchmarkAllCases.Counter); + Assert.Equal(0, counter); benchmarkAction.InvokeMultiple(11); - Assert.Equal(0, BenchmarkAllCases.Counter); + Assert.Equal(0, counter); } else { benchmarkAction.InvokeSingle(); - Assert.Equal(1, BenchmarkAllCases.Counter); + Assert.Equal(1, counter); benchmarkAction.InvokeMultiple(0); - Assert.Equal(1, BenchmarkAllCases.Counter); + Assert.Equal(1, counter); benchmarkAction.InvokeMultiple(11); - Assert.Equal(BenchmarkAllCases.Counter, 1 + unrollFactor * 11); + Assert.Equal(1 + unrollFactor * 11, counter); } Assert.Equal(benchmarkAction.LastRunResult, expectedResult); } finally { - BenchmarkAllCases.Counter = 0; + counter = 0; } } @@ -256,6 +347,13 @@ public async Task InvokeOnceTaskAsync() Interlocked.Increment(ref Counter); } + [Benchmark] + public async ValueTask InvokeOnceValueTaskAsync() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + [Benchmark] public string InvokeOnceRefType() { @@ -285,5 +383,57 @@ public ValueTask InvokeOnceValueTaskOfT() return new ValueTask(DecimalResult); } } + + [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] + public class BenchmarkSetupCleanupTask + { + public static int Counter; + + [GlobalSetup] + public static async Task GlobalSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + + [GlobalCleanup] + public async Task GlobalCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counter); + } + } + + [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] + public class BenchmarkSetupCleanupValueTask + { + public static int Counter; + + [GlobalSetup] + public static async ValueTask GlobalSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + + [GlobalCleanup] + public async ValueTask GlobalCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counter); + } + } } } \ No newline at end of file From 9a6c74fb940354efb7462aef0860eb85175a46db Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 19 Sep 2022 18:06:47 -0400 Subject: [PATCH 06/13] Refactored delegates to pass in IClock and return ValueTask. Force async unroll factor to 1. Support async IterationSetup/IterationCleanup. --- src/BenchmarkDotNet/Code/CodeGenerator.cs | 14 +- .../Code/DeclarationsProvider.cs | 43 +- src/BenchmarkDotNet/Engines/Engine.cs | 41 +- src/BenchmarkDotNet/Engines/EngineFactory.cs | 5 +- .../Engines/EngineParameters.cs | 17 +- src/BenchmarkDotNet/Engines/IEngine.cs | 10 +- .../Helpers/AutoResetValueTaskSource.cs | 53 ++ src/BenchmarkDotNet/Helpers/AwaitHelper.cs | 63 +- .../IlGeneratorStatementExtensions.cs | 53 ++ src/BenchmarkDotNet/Running/BenchmarkCase.cs | 7 +- .../Templates/BenchmarkType.txt | 239 ++++++- .../ConsumableTypeInfo.cs | 27 +- .../Emitters/ConsumableConsumeEmitter.cs | 2 +- .../Emitters/ConsumeEmitter.cs | 125 +++- .../Emitters/RunnableEmitter.cs | 247 ++----- .../Emitters/TaskConsumeEmitter.cs | 658 ++++++++++++++++++ .../Runnable/RunnableConstants.cs | 13 + .../Runnable/RunnableReflectionHelpers.cs | 10 +- .../InProcess.NoEmit/BenchmarkAction.cs | 16 +- .../BenchmarkActionFactory.cs | 15 + .../BenchmarkActionFactory_Implementations.cs | 485 +++++++++++-- .../InProcess.NoEmit/InProcessNoEmitRunner.cs | 26 +- .../Toolchains/InProcess/BenchmarkAction.cs | 17 +- .../InProcess/BenchmarkActionFactory.cs | 15 + .../BenchmarkActionFactory_Implementations.cs | 505 +++++++++++--- .../Toolchains/InProcess/InProcessRunner.cs | 24 +- .../Toolchains/Roslyn/Generator.cs | 6 +- .../AllSetupAndCleanupTest.cs | 64 +- .../CustomEngineTests.cs | 10 +- .../NaiveRunnableEmitDiff.cs | 26 +- .../InProcessEmitTest.cs | 106 +++ .../InProcessTest.cs | 87 ++- .../Engine/EngineFactoryTests.cs | 73 +- .../BenchmarkDotNet.Tests/Mocks/MockEngine.cs | 9 +- 34 files changed, 2527 insertions(+), 584 deletions(-) create mode 100644 src/BenchmarkDotNet/Helpers/AutoResetValueTaskSource.cs create mode 100644 src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/TaskConsumeEmitter.cs diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs index 52ae0e5478..7921d715f8 100644 --- a/src/BenchmarkDotNet/Code/CodeGenerator.cs +++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs @@ -35,6 +35,8 @@ internal static string Generate(BuildPartition buildPartition) var provider = GetDeclarationsProvider(benchmark.Descriptor); + provider.OverrideUnrollFactor(benchmark); + string passArguments = GetPassArguments(benchmark); string compilationId = $"{provider.ReturnsDefinition}_{buildInfo.Id}"; @@ -49,6 +51,7 @@ internal static string Generate(BuildPartition buildPartition) .Replace("$WorkloadMethodReturnType$", provider.WorkloadMethodReturnTypeName) .Replace("$WorkloadMethodReturnTypeModifiers$", provider.WorkloadMethodReturnTypeModifiers) .Replace("$OverheadMethodReturnTypeName$", provider.OverheadMethodReturnTypeName) + .Replace("$AwaiterTypeName$", provider.AwaiterTypeName) .Replace("$GlobalSetupMethodName$", provider.GlobalSetupMethodName) .Replace("$GlobalCleanupMethodName$", provider.GlobalCleanupMethodName) .Replace("$IterationSetupMethodName$", provider.IterationSetupMethodName) @@ -152,15 +155,12 @@ private static DeclarationsProvider GetDeclarationsProvider(Descriptor descripto { var method = descriptor.WorkloadMethod; - if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(ValueTask)) - { - return new TaskDeclarationsProvider(descriptor); - } - if (method.ReturnType.GetTypeInfo().IsGenericType - && (method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) + if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(ValueTask) + || method.ReturnType.GetTypeInfo().IsGenericType + && (method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) || method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>))) { - return new GenericTaskDeclarationsProvider(descriptor); + return new TaskDeclarationsProvider(descriptor); } if (method.ReturnType == typeof(void)) diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs index 7528e8ed62..5f2eca39a6 100644 --- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs +++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Reflection; using System.Threading.Tasks; using BenchmarkDotNet.Engines; @@ -11,9 +10,6 @@ namespace BenchmarkDotNet.Code { internal abstract class DeclarationsProvider { - // "GlobalSetup" or "GlobalCleanup" methods are optional, so default to an empty delegate, so there is always something that can be invoked - private const string EmptyAction = "() => { }"; - protected readonly Descriptor Descriptor; internal DeclarationsProvider(Descriptor descriptor) => Descriptor = descriptor; @@ -26,9 +22,9 @@ internal abstract class DeclarationsProvider public string GlobalCleanupMethodName => GetMethodName(Descriptor.GlobalCleanupMethod); - public string IterationSetupMethodName => Descriptor.IterationSetupMethod?.Name ?? EmptyAction; + public string IterationSetupMethodName => GetMethodName(Descriptor.IterationSetupMethod); - public string IterationCleanupMethodName => Descriptor.IterationCleanupMethod?.Name ?? EmptyAction; + public string IterationCleanupMethodName => GetMethodName(Descriptor.IterationCleanupMethod); public abstract string ReturnsDefinition { get; } @@ -48,13 +44,18 @@ internal abstract class DeclarationsProvider public string OverheadMethodReturnTypeName => OverheadMethodReturnType.GetCorrectCSharpTypeName(); + public virtual string AwaiterTypeName => string.Empty; + + public virtual void OverrideUnrollFactor(BenchmarkCase benchmarkCase) { } + public abstract string OverheadImplementation { get; } private string GetMethodName(MethodInfo method) { + // "Setup" or "Cleanup" methods are optional, so default to a simple delegate, so there is always something that can be invoked if (method == null) { - return EmptyAction; + return "() => new System.Threading.Tasks.ValueTask()"; } if (method.ReturnType == typeof(Task) || @@ -63,10 +64,10 @@ private string GetMethodName(MethodInfo method) (method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>) || method.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)))) { - return $"() => BenchmarkDotNet.Helpers.AwaitHelper.GetResult({method.Name}())"; + return $"() => BenchmarkDotNet.Helpers.AwaitHelper.ToValueTaskVoid({method.Name}())"; } - return method.Name; + return $"() => {{ {method.Name}(); return new System.Threading.Tasks.ValueTask(); }}"; } } @@ -145,30 +146,18 @@ public ByReadOnlyRefDeclarationsProvider(Descriptor descriptor) : base(descripto public override string WorkloadMethodReturnTypeModifiers => "ref readonly"; } - internal class TaskDeclarationsProvider : VoidDeclarationsProvider + internal class TaskDeclarationsProvider : DeclarationsProvider { public TaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { } - public override string WorkloadMethodDelegate(string passArguments) - => $"({passArguments}) => {{ BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments})); }}"; - - public override string GetWorkloadMethodCall(string passArguments) => $"BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments}))"; + public override string ReturnsDefinition => "RETURNS_AWAITABLE"; - protected override Type WorkloadMethodReturnType => typeof(void); - } - - /// - /// declarations provider for and - /// - internal class GenericTaskDeclarationsProvider : NonVoidDeclarationsProvider - { - public GenericTaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { } + public override string AwaiterTypeName => WorkloadMethodReturnType.GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance).ReturnType.GetCorrectCSharpTypeName(); - protected override Type WorkloadMethodReturnType => Descriptor.WorkloadMethod.ReturnType.GetTypeInfo().GetGenericArguments().Single(); + public override string OverheadImplementation => $"return default({OverheadMethodReturnType.GetCorrectCSharpTypeName()});"; - public override string WorkloadMethodDelegate(string passArguments) - => $"({passArguments}) => {{ return BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments})); }}"; + protected override Type OverheadMethodReturnType => WorkloadMethodReturnType; - public override string GetWorkloadMethodCall(string passArguments) => $"BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments}))"; + public override void OverrideUnrollFactor(BenchmarkCase benchmarkCase) => benchmarkCase.ForceUnrollFactorForAsync(); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Engines/Engine.cs b/src/BenchmarkDotNet/Engines/Engine.cs index f626eddac4..702670a1de 100644 --- a/src/BenchmarkDotNet/Engines/Engine.cs +++ b/src/BenchmarkDotNet/Engines/Engine.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Portability; @@ -19,17 +20,17 @@ public class Engine : IEngine public const int MinInvokeCount = 4; [PublicAPI] public IHost Host { get; } - [PublicAPI] public Action WorkloadAction { get; } + [PublicAPI] public Func> WorkloadAction { get; } [PublicAPI] public Action Dummy1Action { get; } [PublicAPI] public Action Dummy2Action { get; } [PublicAPI] public Action Dummy3Action { get; } - [PublicAPI] public Action OverheadAction { get; } + [PublicAPI] public Func> OverheadAction { get; } [PublicAPI] public Job TargetJob { get; } [PublicAPI] public long OperationsPerInvoke { get; } - [PublicAPI] public Action GlobalSetupAction { get; } - [PublicAPI] public Action GlobalCleanupAction { get; } - [PublicAPI] public Action IterationSetupAction { get; } - [PublicAPI] public Action IterationCleanupAction { get; } + [PublicAPI] public Func GlobalSetupAction { get; } + [PublicAPI] public Func GlobalCleanupAction { get; } + [PublicAPI] public Func IterationSetupAction { get; } + [PublicAPI] public Func IterationCleanupAction { get; } [PublicAPI] public IResolver Resolver { get; } [PublicAPI] public CultureInfo CultureInfo { get; } [PublicAPI] public string BenchmarkName { get; } @@ -50,9 +51,9 @@ public class Engine : IEngine internal Engine( IHost host, IResolver resolver, - Action dummy1Action, Action dummy2Action, Action dummy3Action, Action overheadAction, Action workloadAction, Job targetJob, - Action globalSetupAction, Action globalCleanupAction, Action iterationSetupAction, Action iterationCleanupAction, long operationsPerInvoke, - bool includeExtraStats, string benchmarkName) + Action dummy1Action, Action dummy2Action, Action dummy3Action, Func> overheadAction, Func> workloadAction, + Job targetJob, Func globalSetupAction, Func globalCleanupAction, Func iterationSetupAction, Func iterationCleanupAction, + long operationsPerInvoke, bool includeExtraStats, string benchmarkName) { Host = host; @@ -90,7 +91,7 @@ public void Dispose() { try { - GlobalCleanupAction?.Invoke(); + Helpers.AwaitHelper.GetResult(GlobalCleanupAction.Invoke()); } catch (Exception e) { @@ -155,7 +156,7 @@ public Measurement RunIteration(IterationData data) var action = isOverhead ? OverheadAction : WorkloadAction; if (!isOverhead) - IterationSetupAction(); + Helpers.AwaitHelper.GetResult(IterationSetupAction()); GcCollect(); @@ -165,15 +166,14 @@ public Measurement RunIteration(IterationData data) Span stackMemory = randomizeMemory ? stackalloc byte[random.Next(32)] : Span.Empty; // Measure - var clock = Clock.Start(); - action(invokeCount / unrollFactor); - var clockSpan = clock.GetElapsed(); + var op = action(invokeCount / unrollFactor, Clock); + var clockSpan = Helpers.AwaitHelper.GetResult(op); if (EngineEventSource.Log.IsEnabled()) EngineEventSource.Log.IterationStop(data.IterationMode, data.IterationStage, totalOperations); if (!isOverhead) - IterationCleanupAction(); + Helpers.AwaitHelper.GetResult(IterationCleanupAction()); if (randomizeMemory) RandomizeManagedHeapMemory(); @@ -196,17 +196,18 @@ public Measurement RunIteration(IterationData data) // it does not matter, because we have already obtained the results! EnableMonitoring(); - IterationSetupAction(); // we run iteration setup first, so even if it allocates, it is not included in the results + Helpers.AwaitHelper.GetResult(IterationSetupAction()); // we run iteration setup first, so even if it allocates, it is not included in the results var initialThreadingStats = ThreadingStats.ReadInitial(); // this method might allocate var initialGcStats = GcStats.ReadInitial(); - WorkloadAction(data.InvokeCount / data.UnrollFactor); + var op = WorkloadAction(data.InvokeCount / data.UnrollFactor, Clock); + Helpers.AwaitHelper.GetResult(op); var finalGcStats = GcStats.ReadFinal(); var finalThreadingStats = ThreadingStats.ReadFinal(); - IterationCleanupAction(); // we run iteration cleanup after collecting GC stats + Helpers.AwaitHelper.GetResult(IterationCleanupAction()); // we run iteration cleanup after collecting GC stats GcStats gcStats = (finalGcStats - initialGcStats).WithTotalOperations(data.InvokeCount * OperationsPerInvoke); ThreadingStats threadingStats = (finalThreadingStats - initialThreadingStats).WithTotalOperations(data.InvokeCount * OperationsPerInvoke); @@ -220,14 +221,14 @@ private void Consume(in Span _) { } private void RandomizeManagedHeapMemory() { // invoke global cleanup before global setup - GlobalCleanupAction?.Invoke(); + Helpers.AwaitHelper.GetResult(GlobalCleanupAction.Invoke()); var gen0object = new byte[random.Next(32)]; var lohObject = new byte[85 * 1024 + random.Next(32)]; // we expect the key allocations to happen in global setup (not ctor) // so we call it while keeping the random-size objects alive - GlobalSetupAction?.Invoke(); + Helpers.AwaitHelper.GetResult(GlobalSetupAction.Invoke()); GC.KeepAlive(gen0object); GC.KeepAlive(lohObject); diff --git a/src/BenchmarkDotNet/Engines/EngineFactory.cs b/src/BenchmarkDotNet/Engines/EngineFactory.cs index 0588218522..e311a927ee 100644 --- a/src/BenchmarkDotNet/Engines/EngineFactory.cs +++ b/src/BenchmarkDotNet/Engines/EngineFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using BenchmarkDotNet.Jobs; using Perfolizer.Horology; @@ -25,7 +26,7 @@ public IEngine CreateReadyToRun(EngineParameters engineParameters) if (engineParameters.TargetJob == null) throw new ArgumentNullException(nameof(engineParameters.TargetJob)); - engineParameters.GlobalSetupAction?.Invoke(); // whatever the settings are, we MUST call global setup here, the global cleanup is part of Engine's Dispose + engineParameters.GlobalSetupAction.Invoke().AsTask().GetAwaiter().GetResult(); // whatever the settings are, we MUST call global setup here, the global cleanup is part of Engine's Dispose if (!engineParameters.NeedsJitting) // just create the engine, do NOT jit return CreateMultiActionEngine(engineParameters); @@ -109,7 +110,7 @@ private static Engine CreateSingleActionEngine(EngineParameters engineParameters engineParameters.OverheadActionNoUnroll, engineParameters.WorkloadActionNoUnroll); - private static Engine CreateEngine(EngineParameters engineParameters, Job job, Action idle, Action main) + private static Engine CreateEngine(EngineParameters engineParameters, Job job, Func> idle, Func> main) => new Engine( engineParameters.Host, EngineParameters.DefaultResolver, diff --git a/src/BenchmarkDotNet/Engines/EngineParameters.cs b/src/BenchmarkDotNet/Engines/EngineParameters.cs index ec61582529..c7361b3a07 100644 --- a/src/BenchmarkDotNet/Engines/EngineParameters.cs +++ b/src/BenchmarkDotNet/Engines/EngineParameters.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; @@ -12,19 +13,19 @@ public class EngineParameters public static readonly IResolver DefaultResolver = new CompositeResolver(BenchmarkRunnerClean.DefaultResolver, EngineResolver.Instance); public IHost Host { get; set; } - public Action WorkloadActionNoUnroll { get; set; } - public Action WorkloadActionUnroll { get; set; } + public Func> WorkloadActionNoUnroll { get; set; } + public Func> WorkloadActionUnroll { get; set; } public Action Dummy1Action { get; set; } public Action Dummy2Action { get; set; } public Action Dummy3Action { get; set; } - public Action OverheadActionNoUnroll { get; set; } - public Action OverheadActionUnroll { get; set; } + public Func> OverheadActionNoUnroll { get; set; } + public Func> OverheadActionUnroll { get; set; } public Job TargetJob { get; set; } = Job.Default; public long OperationsPerInvoke { get; set; } = 1; - public Action GlobalSetupAction { get; set; } - public Action GlobalCleanupAction { get; set; } - public Action IterationSetupAction { get; set; } - public Action IterationCleanupAction { get; set; } + public Func GlobalSetupAction { get; set; } + public Func GlobalCleanupAction { get; set; } + public Func IterationSetupAction { get; set; } + public Func IterationCleanupAction { get; set; } public bool MeasureExtraStats { get; set; } [PublicAPI] public string BenchmarkName { get; set; } diff --git a/src/BenchmarkDotNet/Engines/IEngine.cs b/src/BenchmarkDotNet/Engines/IEngine.cs index c502a97e16..40bb64ce69 100644 --- a/src/BenchmarkDotNet/Engines/IEngine.cs +++ b/src/BenchmarkDotNet/Engines/IEngine.cs @@ -1,9 +1,11 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Reports; using JetBrains.Annotations; +using Perfolizer.Horology; using NotNullAttribute = JetBrains.Annotations.NotNullAttribute; namespace BenchmarkDotNet.Engines @@ -24,16 +26,16 @@ public interface IEngine : IDisposable long OperationsPerInvoke { get; } [CanBeNull] - Action GlobalSetupAction { get; } + Func GlobalSetupAction { get; } [CanBeNull] - Action GlobalCleanupAction { get; } + Func GlobalCleanupAction { get; } [NotNull] - Action WorkloadAction { get; } + Func> WorkloadAction { get; } [NotNull] - Action OverheadAction { get; } + Func> OverheadAction { get; } IResolver Resolver { get; } diff --git a/src/BenchmarkDotNet/Helpers/AutoResetValueTaskSource.cs b/src/BenchmarkDotNet/Helpers/AutoResetValueTaskSource.cs new file mode 100644 index 0000000000..7fc093a9ea --- /dev/null +++ b/src/BenchmarkDotNet/Helpers/AutoResetValueTaskSource.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; + +namespace BenchmarkDotNet.Helpers +{ + /// + /// Implementation for that will reset itself when awaited so that it can be re-used. + /// + public class AutoResetValueTaskSource : IValueTaskSource, IValueTaskSource + { + private ManualResetValueTaskSourceCore _sourceCore; + + /// Completes with a successful result. + /// The result. + public void SetResult(TResult result) => _sourceCore.SetResult(result); + + /// Completes with an error. + /// The exception. + public void SetException(Exception error) => _sourceCore.SetException(error); + + /// Gets the operation version. + public short Version => _sourceCore.Version; + + private TResult GetResult(short token) + { + // We don't want to reset this if the token is invalid. + if (token != Version) + { + throw new InvalidOperationException(); + } + try + { + return _sourceCore.GetResult(token); + } + finally + { + _sourceCore.Reset(); + } + } + + void IValueTaskSource.GetResult(short token) => GetResult(token); + TResult IValueTaskSource.GetResult(short token) => GetResult(token); + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _sourceCore.GetStatus(token); + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _sourceCore.GetStatus(token); + + // Don't pass the flags, we don't want to schedule the continuation on the current SynchronizationContext or TaskScheduler if the user runs this in-process, as that may cause a deadlock when this is waited on synchronously. + // And we don't want to capture the ExecutionContext (we don't use it, and it causes allocations in the full framework). + void IValueTaskSource.OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _sourceCore.OnCompleted(continuation, state, token, ValueTaskSourceOnCompletedFlags.None); + void IValueTaskSource.OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _sourceCore.OnCompleted(continuation, state, token, ValueTaskSourceOnCompletedFlags.None); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs index 0863f06c57..072d903410 100644 --- a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs +++ b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs @@ -92,6 +92,29 @@ public static T GetResult(ValueTask task) return awaiter.GetResult(); } + public static ValueTask ToValueTaskVoid(Task task) + { + return new ValueTask(task); + } + + public static ValueTask ToValueTaskVoid(Task task) + { + return new ValueTask(task); + } + + public static ValueTask ToValueTaskVoid(ValueTask task) + { + return task; + } + + // ValueTask unfortunately can't be converted to a ValueTask for free, so we must create a state machine. + // It's not a big deal though, as this is only used for Setup/Cleanup where allocations aren't measured. + // And in practice, this should never be used, as (Value)Task Setup/Cleanup methods have no utility. + public static async ValueTask ToValueTaskVoid(ValueTask task) + { + _ = await task.ConfigureAwait(false); + } + internal static MethodInfo GetGetResultMethod(Type taskType) { if (!taskType.IsGenericType) @@ -106,11 +129,6 @@ internal static MethodInfo GetGetResultMethod(Type taskType) { return null; } - var resultType = taskType - .GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance) - .ReturnType - .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlags.Public | BindingFlags.Instance) - .ReturnType; return typeof(AwaitHelper).GetMethods(BindingFlags.Public | BindingFlags.Static) .First(m => { @@ -119,7 +137,40 @@ internal static MethodInfo GetGetResultMethod(Type taskType) // We have to compare the types indirectly, == check doesn't work. return paramType.Assembly == compareType.Assembly && paramType.Namespace == compareType.Namespace && paramType.Name == compareType.Name; }) - .MakeGenericMethod(new[] { resultType }); + .MakeGenericMethod(new[] { GetTaskResultType(taskType) }); } + + internal static MethodInfo GetToValueTaskMethod(Type taskType) + { + if (!taskType.IsGenericType) + { + return typeof(AwaitHelper).GetMethod(nameof(AwaitHelper.ToValueTaskVoid), BindingFlags.Public | BindingFlags.Static, null, new Type[1] { taskType }, null); + } + + Type compareType = taskType.GetGenericTypeDefinition() == typeof(ValueTask<>) ? typeof(ValueTask<>) + : typeof(Task).IsAssignableFrom(taskType.GetGenericTypeDefinition()) ? typeof(Task<>) + : null; + if (compareType == null) + { + return null; + } + return compareType == null + ? null + : typeof(AwaitHelper).GetMethods(BindingFlags.Public | BindingFlags.Static) + .First(m => + { + if (m.Name != nameof(AwaitHelper.ToValueTaskVoid)) return false; + Type paramType = m.GetParameters().First().ParameterType; + // We have to compare the types indirectly, == check doesn't work. + return paramType.Assembly == compareType.Assembly && paramType.Namespace == compareType.Namespace && paramType.Name == compareType.Name; + }) + .MakeGenericMethod(new[] { GetTaskResultType(taskType) }); + } + + private static Type GetTaskResultType(Type taskType) => taskType + .GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance) + .ReturnType + .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlags.Public | BindingFlags.Instance) + .ReturnType; } } diff --git a/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs index 6d601e6495..50d4fd86af 100644 --- a/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs +++ b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs @@ -143,5 +143,58 @@ public static void EmitLoopEndFromLocToArg( ilBuilder.EmitLdarg(toArg); ilBuilder.Emit(OpCodes.Blt, loopStartLabel); } + + public static void EmitLoopBeginFromFldTo0( + this ILGenerator ilBuilder, + Label loopStartLabel, + Label loopHeadLabel) + { + // IL_001b: br.s IL_0029 // loop start (head: IL_0029) + ilBuilder.Emit(OpCodes.Br, loopHeadLabel); + + // loop start (head: IL_0036) + ilBuilder.MarkLabel(loopStartLabel); + } + + public static void EmitLoopEndFromFldTo0( + this ILGenerator ilBuilder, + Label loopStartLabel, + Label loopHeadLabel, + FieldBuilder counterField, + LocalBuilder counterLocal) + { + // loop counter stored as loc0, loop max passed as arg1 + /* + // while (--repeatsRemaining >= 0) + IL_0029: ldarg.0 + IL_002a: ldarg.0 + IL_002b: ldfld int64 BenchmarkRunner_0::repeatsRemaining + IL_0030: ldc.i4.1 + IL_0031: conv.i8 + IL_0032: sub + IL_0033: stloc.1 + IL_0034: ldloc.1 + IL_0035: stfld int64 BenchmarkRunner_0::repeatsRemaining + IL_003a: ldloc.1 + IL_003b: ldc.i4.0 + IL_003c: conv.i8 + IL_003d: bge.s IL_001d + // end loop + */ + ilBuilder.MarkLabel(loopHeadLabel); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, counterField); + ilBuilder.Emit(OpCodes.Ldc_I4_1); + ilBuilder.Emit(OpCodes.Conv_I8); + ilBuilder.Emit(OpCodes.Sub); + ilBuilder.EmitStloc(counterLocal); + ilBuilder.EmitLdloc(counterLocal); + ilBuilder.Emit(OpCodes.Stfld, counterField); + ilBuilder.EmitLdloc(counterLocal); + ilBuilder.Emit(OpCodes.Ldc_I4_0); + ilBuilder.Emit(OpCodes.Conv_I8); + ilBuilder.Emit(OpCodes.Bge, loopStartLabel); + } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Running/BenchmarkCase.cs b/src/BenchmarkDotNet/Running/BenchmarkCase.cs index b517ab5676..410446c00d 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkCase.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkCase.cs @@ -11,7 +11,7 @@ namespace BenchmarkDotNet.Running public class BenchmarkCase : IComparable, IDisposable { public Descriptor Descriptor { get; } - public Job Job { get; } + public Job Job { get; private set; } public ParameterInstances Parameters { get; } public ImmutableConfig Config { get; } @@ -32,6 +32,11 @@ public Runtime GetRuntime() => Job.Environment.HasValue(EnvironmentMode.RuntimeC ? Job.Environment.Runtime : RuntimeInformation.GetCurrentRuntime(); + internal void ForceUnrollFactorForAsync() + { + Job = Job.WithUnrollFactor(1); + } + public void Dispose() => Parameters.Dispose(); public int CompareTo(BenchmarkCase other) => string.Compare(FolderInfo, other.FolderInfo, StringComparison.Ordinal); diff --git a/src/BenchmarkDotNet/Templates/BenchmarkType.txt b/src/BenchmarkDotNet/Templates/BenchmarkType.txt index d8f15f9138..584cce4d91 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkType.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkType.txt @@ -1,5 +1,5 @@ // the type name must be in sync with WindowsDisassembler.BuildArguments - public unsafe class Runnable_$ID$ : global::$WorkloadTypeName$ + public unsafe partial class Runnable_$ID$ : global::$WorkloadTypeName$ { public static void Run(BenchmarkDotNet.Engines.IHost host, System.String benchmarkName) { @@ -64,12 +64,13 @@ overheadDelegate = __Overhead; workloadDelegate = $WorkloadMethodDelegate$; $InitializeArgumentFields$ + __SetContinuation(); } - private System.Action globalSetupAction; - private System.Action globalCleanupAction; - private System.Action iterationSetupAction; - private System.Action iterationCleanupAction; + private System.Func globalSetupAction; + private System.Func globalCleanupAction; + private System.Func iterationSetupAction; + private System.Func iterationCleanupAction; private BenchmarkDotNet.Autogenerated.Runnable_$ID$.OverheadDelegate overheadDelegate; private BenchmarkDotNet.Autogenerated.Runnable_$ID$.WorkloadDelegate workloadDelegate; $DeclareArgumentFields$ @@ -109,56 +110,210 @@ $OverheadImplementation$ } -#if RETURNS_CONSUMABLE_$ID$ + partial void __SetContinuation(); + +#if RETURNS_AWAITABLE_$ID$ + + private readonly BenchmarkDotNet.Helpers.AutoResetValueTaskSource valueTaskSource = new BenchmarkDotNet.Helpers.AutoResetValueTaskSource(); + private System.Int64 repeatsRemaining; + private System.Action continuation; + private Perfolizer.Horology.StartedClock startedClock; + private $AwaiterTypeName$ currentAwaiter; + + partial void __SetContinuation() => continuation = __Continuation; + +#if NETCOREAPP3_0_OR_GREATER + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] +#endif + // Awaits are not unrolled. + private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) + { + return OverheadActionImpl(invokeCount, clock); + } + +#if NETCOREAPP3_0_OR_GREATER + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] +#endif + private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) + { + return OverheadActionImpl(invokeCount, clock); + } + +#if NETCOREAPP3_0_OR_GREATER + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] +#endif + private System.Threading.Tasks.ValueTask OverheadActionImpl(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) + { + repeatsRemaining = invokeCount; + $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); + startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); + try + { + $LoadArguments$ + while (--repeatsRemaining >= 0) + { + value = overheadDelegate($PassArguments$); + } + } + catch (System.Exception) + { + BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + throw; + } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); + } + +#if NETCOREAPP3_0_OR_GREATER + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] +#endif + private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) + { + return WorkloadActionImpl(invokeCount, clock); + } + +#if NETCOREAPP3_0_OR_GREATER + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] +#endif + private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) + { + return WorkloadActionImpl(invokeCount, clock); + } + +#if NETCOREAPP3_0_OR_GREATER + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] +#endif + private System.Threading.Tasks.ValueTask WorkloadActionImpl(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) + { + repeatsRemaining = invokeCount; + startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); + __RunTask(); + return new System.Threading.Tasks.ValueTask(valueTaskSource, valueTaskSource.Version); + } + +#if NETCOREAPP3_0_OR_GREATER + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] +#endif + private void __RunTask() + { + try + { + $LoadArguments$ + while (--repeatsRemaining >= 0) + { + currentAwaiter = workloadDelegate($PassArguments$).GetAwaiter(); + if (!currentAwaiter.IsCompleted) + { + currentAwaiter.UnsafeOnCompleted(continuation); + return; + } + currentAwaiter.GetResult(); + } + } + catch (System.Exception e) + { + __SetException(e); + return; + } + var clockspan = startedClock.GetElapsed(); + currentAwaiter = default($AwaiterTypeName$); + startedClock = default(Perfolizer.Horology.StartedClock); + valueTaskSource.SetResult(clockspan); + } + +#if NETCOREAPP3_0_OR_GREATER + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] +#endif + private void __Continuation() + { + try + { + currentAwaiter.GetResult(); + } + catch (System.Exception e) + { + __SetException(e); + return; + } + __RunTask(); + } + + private void __SetException(System.Exception e) + { + currentAwaiter = default($AwaiterTypeName$); + startedClock = default(Perfolizer.Horology.StartedClock); + valueTaskSource.SetException(e); + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + public $WorkloadMethodReturnType$ $DisassemblerEntryMethodName$() + { + if (NotEleven == 11) + { + $LoadArguments$ + return $WorkloadMethodCall$; + } + + return default($WorkloadMethodReturnType$); + } + +#elif RETURNS_CONSUMABLE_$ID$ private BenchmarkDotNet.Engines.Consumer consumer = new BenchmarkDotNet.Engines.Consumer(); #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { consumer.Consume(overheadDelegate($PassArguments$));@Unroll@ } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { consumer.Consume(overheadDelegate($PassArguments$)); } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { consumer.Consume(workloadDelegate($PassArguments$)$ConsumeField$);@Unroll@ } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { consumer.Consume(workloadDelegate($PassArguments$)$ConsumeField$); } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] @@ -178,57 +333,65 @@ #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $OverheadMethodReturnTypeName$ result = default($OverheadMethodReturnTypeName$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { result = overheadDelegate($PassArguments$);@Unroll@ } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(result); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $OverheadMethodReturnTypeName$ result = default($OverheadMethodReturnTypeName$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { result = overheadDelegate($PassArguments$); } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(result); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $WorkloadMethodReturnType$ result = default($WorkloadMethodReturnType$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { result = workloadDelegate($PassArguments$);@Unroll@ } NonGenericKeepAliveWithoutBoxing(result); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $WorkloadMethodReturnType$ result = default($WorkloadMethodReturnType$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { result = workloadDelegate($PassArguments$); } NonGenericKeepAliveWithoutBoxing(result); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } // we must not simply use DeadCodeEliminationHelper.KeepAliveWithoutBoxing because it's generic method @@ -253,29 +416,33 @@ #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { value = overheadDelegate($PassArguments$);@Unroll@ } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { value = overheadDelegate($PassArguments$); } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } private $WorkloadMethodReturnType$ workloadDefaultValueHolder = default($WorkloadMethodReturnType$); @@ -283,29 +450,33 @@ #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { alias = workloadDelegate($PassArguments$);@Unroll@ } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(ref alias); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { alias = workloadDelegate($PassArguments$); } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(ref alias); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] @@ -324,29 +495,33 @@ #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { value = overheadDelegate($PassArguments$);@Unroll@ } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { value = overheadDelegate($PassArguments$); } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } private $WorkloadMethodReturnType$ workloadDefaultValueHolder = default($WorkloadMethodReturnType$); @@ -354,29 +529,33 @@ #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { alias = workloadDelegate($PassArguments$);@Unroll@ } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxingReadonly(alias); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { alias = workloadDelegate($PassArguments$); } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxingReadonly(alias); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] @@ -395,49 +574,57 @@ #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { overheadDelegate($PassArguments$);@Unroll@ } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { overheadDelegate($PassArguments$); } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { workloadDelegate($PassArguments$);@Unroll@ } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { workloadDelegate($PassArguments$); } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/ConsumableTypeInfo.cs b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/ConsumableTypeInfo.cs index 8c3bf24eac..62cc39bc3b 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/ConsumableTypeInfo.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/ConsumableTypeInfo.cs @@ -17,7 +17,7 @@ public ConsumableTypeInfo(Type methodReturnType) if (methodReturnType == null) throw new ArgumentNullException(nameof(methodReturnType)); - OriginMethodReturnType = methodReturnType; + WorkloadMethodReturnType = methodReturnType; // Only support (Value)Task for parity with other toolchains (and so we can use AwaitHelper). IsAwaitable = methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask) @@ -25,25 +25,15 @@ public ConsumableTypeInfo(Type methodReturnType) && (methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) || methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>))); - if (!IsAwaitable) - { - WorkloadMethodReturnType = methodReturnType; - } - else - { - WorkloadMethodReturnType = methodReturnType - .GetMethod(nameof(Task.GetAwaiter), BindingFlagsPublicInstance) - .ReturnType - .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlagsPublicInstance) - .ReturnType; - GetResultMethod = Helpers.AwaitHelper.GetGetResultMethod(methodReturnType); - } - if (WorkloadMethodReturnType == null) throw new InvalidOperationException("Bug: (WorkloadMethodReturnType == null"); var consumableField = default(FieldInfo); - if (WorkloadMethodReturnType == typeof(void)) + if (IsAwaitable) + { + OverheadMethodReturnType = WorkloadMethodReturnType; + } + else if (WorkloadMethodReturnType == typeof(void)) { IsVoid = true; OverheadMethodReturnType = WorkloadMethodReturnType; @@ -69,16 +59,11 @@ public ConsumableTypeInfo(Type methodReturnType) throw new InvalidOperationException("Bug: (OverheadResultType == null"); } - [NotNull] - public Type OriginMethodReturnType { get; } [NotNull] public Type WorkloadMethodReturnType { get; } [NotNull] public Type OverheadMethodReturnType { get; } - [CanBeNull] - public MethodInfo GetResultMethod { get; } - public bool IsVoid { get; } public bool IsByRef { get; } public bool IsConsumable { get; } diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/ConsumableConsumeEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/ConsumableConsumeEmitter.cs index 76a2a5f505..b92c0228aa 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/ConsumableConsumeEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/ConsumableConsumeEmitter.cs @@ -70,7 +70,7 @@ protected override void EmitDisassemblyDiagnoserReturnDefaultOverride(ILGenerato ilBuilder.EmitReturnDefault(ConsumableInfo.WorkloadMethodReturnType, disassemblyDiagnoserLocal); } - protected override void OnEmitCtorBodyOverride(ConstructorBuilder constructorBuilder, ILGenerator ilBuilder) + protected override void OnEmitCtorBodyOverride(ConstructorBuilder constructorBuilder, ILGenerator ilBuilder, RunnableEmitter runnableEmitter) { var ctor = typeof(Consumer).GetConstructor(Array.Empty()); if (ctor == null) diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/ConsumeEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/ConsumeEmitter.cs index 62fe06c649..7a8529dc74 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/ConsumeEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/ConsumeEmitter.cs @@ -1,7 +1,12 @@ using System; using System.Reflection; using System.Reflection.Emit; +using System.Threading.Tasks; +using BenchmarkDotNet.Helpers.Reflection.Emit; using JetBrains.Annotations; +using Perfolizer.Horology; +using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; +using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableReflectionHelpers; namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation { @@ -12,6 +17,8 @@ public static ConsumeEmitter GetConsumeEmitter(ConsumableTypeInfo consumableType if (consumableTypeInfo == null) throw new ArgumentNullException(nameof(consumableTypeInfo)); + if (consumableTypeInfo.IsAwaitable) + return new TaskConsumeEmitter(consumableTypeInfo); if (consumableTypeInfo.IsVoid) return new VoidConsumeEmitter(consumableTypeInfo); if (consumableTypeInfo.IsByRef) @@ -97,14 +104,14 @@ protected virtual void OnEmitMembersOverride(TypeBuilder runnableBuilder) { } - public void OnEmitCtorBody(ConstructorBuilder constructorBuilder, ILGenerator ilBuilder) + public void OnEmitCtorBody(ConstructorBuilder constructorBuilder, ILGenerator ilBuilder, RunnableEmitter runnableEmitter) { AssertNoBuilder(); - OnEmitCtorBodyOverride(constructorBuilder, ilBuilder); + OnEmitCtorBodyOverride(constructorBuilder, ilBuilder, runnableEmitter); } - protected virtual void OnEmitCtorBodyOverride(ConstructorBuilder constructorBuilder, ILGenerator ilBuilder) + protected virtual void OnEmitCtorBodyOverride(ConstructorBuilder constructorBuilder, ILGenerator ilBuilder, RunnableEmitter runnableEmitter) { } @@ -223,5 +230,117 @@ public void EmitActionAfterCall(ILGenerator ilBuilder) protected virtual void EmitActionAfterCallOverride(ILGenerator ilBuilder) { } + + public virtual MethodBuilder EmitActionImpl(RunnableEmitter runnableEmitter, string methodName, RunnableActionKind actionKind, int unrollFactor) + { + FieldInfo actionDelegateField; + MethodInfo actionInvokeMethod; + switch (actionKind) + { + case RunnableActionKind.Overhead: + actionDelegateField = runnableEmitter.overheadDelegateField; + actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(runnableEmitter.overheadDelegateType); + break; + case RunnableActionKind.Workload: + actionDelegateField = runnableEmitter.workloadDelegateField; + actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(runnableEmitter.workloadDelegateType); + break; + default: + throw new ArgumentOutOfRangeException(nameof(actionKind), actionKind, null); + } + + /* + .method private hidebysig + instance valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1 WorkloadActionNoUnroll ( + int64 invokeCount, + class Perfolizer.Horology.IClock clock + ) cil managed + */ + var toArg = new EmitParameterInfo(0, InvokeCountParamName, typeof(long)); + var clockArg = new EmitParameterInfo(1, ClockParamName, typeof(IClock)); + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefineNonVirtualInstanceMethod( + methodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)), + toArg, clockArg); + toArg.SetMember(actionMethodBuilder); + clockArg.SetMember(actionMethodBuilder); + + // Emit impl + var ilBuilder = actionMethodBuilder.GetILGenerator(); + BeginEmitAction(actionMethodBuilder, ilBuilder, actionInvokeMethod, actionKind); + + // init locals + var argLocals = runnableEmitter.EmitDeclareArgLocals(ilBuilder); + DeclareActionLocals(ilBuilder); + var startedClockLocal = ilBuilder.DeclareLocal(typeof(StartedClock)); + var indexLocal = ilBuilder.DeclareLocal(typeof(long)); + + // load fields + runnableEmitter.EmitLoadArgFieldsToLocals(ilBuilder, argLocals); + EmitActionBeforeLoop(ilBuilder); + + // start clock + /* + // var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); + IL_0000: ldarg.2 + IL_0001: call valuetype Perfolizer.Horology.StartedClock Perfolizer.Horology.ClockExtensions::Start(class Perfolizer.Horology.IClock) + IL_0006: stloc.0 + */ + ilBuilder.EmitLdarg(clockArg); + ilBuilder.Emit(OpCodes.Call, runnableEmitter.startClockMethod); + ilBuilder.EmitStloc(startedClockLocal); + + // loop + var loopStartLabel = ilBuilder.DefineLabel(); + var loopHeadLabel = ilBuilder.DefineLabel(); + ilBuilder.EmitLoopBeginFromLocToArg(loopStartLabel, loopHeadLabel, indexLocal, toArg); + { + /* + // overheadDelegate(); + IL_0005: ldarg.0 + IL_0006: ldfld class BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate BenchmarkDotNet.Autogenerated.Runnable_0::overheadDelegate + IL_000b: callvirt instance void BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate::Invoke() + // -or- + // consumer.Consume(overheadDelegate(_argField)); + IL_000c: ldarg.0 + IL_000d: ldfld class [BenchmarkDotNet]BenchmarkDotNet.Engines.Consumer BenchmarkDotNet.Autogenerated.Runnable_0::consumer + IL_0012: ldarg.0 + IL_0013: ldfld class BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate BenchmarkDotNet.Autogenerated.Runnable_0::overheadDelegate + IL_0018: ldloc.0 + IL_0019: callvirt instance int32 BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate::Invoke(int64) + IL_001e: callvirt instance void [BenchmarkDotNet]BenchmarkDotNet.Engines.Consumer::Consume(int32) + */ + for (int u = 0; u < unrollFactor; u++) + { + EmitActionBeforeCall(ilBuilder); + + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, actionDelegateField); + ilBuilder.EmitInstanceCallThisValueOnStack(null, actionInvokeMethod, argLocals); + + EmitActionAfterCall(ilBuilder); + } + } + ilBuilder.EmitLoopEndFromLocToArg(loopStartLabel, loopHeadLabel, indexLocal, toArg); + + EmitActionAfterLoop(ilBuilder); + CompleteEmitAction(ilBuilder); + + /* + // return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); + IL_0021: ldloca.s 0 + IL_0023: call instance valuetype Perfolizer.Horology.ClockSpan Perfolizer.Horology.StartedClock::GetElapsed() + IL_0028: newobj instance void valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1::.ctor(!0) + IL_002d: ret + */ + ilBuilder.EmitLdloca(startedClockLocal); + ilBuilder.Emit(OpCodes.Call, runnableEmitter.getElapsedMethod); + var ctor = typeof(ValueTask).GetConstructor(new[] { typeof(ClockSpan) }); + ilBuilder.Emit(OpCodes.Newobj, ctor); + ilBuilder.Emit(OpCodes.Ret); + + return actionMethodBuilder; + } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs index df45229474..020029e087 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs @@ -7,6 +7,7 @@ using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Security; +using System.Threading.Tasks; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Helpers.Reflection.Emit; @@ -15,6 +16,7 @@ using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.Results; using JetBrains.Annotations; +using Perfolizer.Horology; using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableReflectionHelpers; @@ -240,9 +242,9 @@ private static void EmitNoArgsMethodCallPopReturn( private int jobUnrollFactor; private int dummyUnrollFactor; - private Type overheadDelegateType; - private Type workloadDelegateType; - private TypeBuilder runnableBuilder; + public Type overheadDelegateType; + public Type workloadDelegateType; + public TypeBuilder runnableBuilder; private ConsumableTypeInfo consumableInfo; private ConsumeEmitter consumeEmitter; private ConsumableTypeInfo globalSetupReturnInfo; @@ -254,8 +256,8 @@ private static void EmitNoArgsMethodCallPopReturn( private FieldBuilder globalCleanupActionField; private FieldBuilder iterationSetupActionField; private FieldBuilder iterationCleanupActionField; - private FieldBuilder overheadDelegateField; - private FieldBuilder workloadDelegateField; + public FieldBuilder overheadDelegateField; + public FieldBuilder workloadDelegateField; private FieldBuilder notElevenField; private FieldBuilder dummyVarField; @@ -265,7 +267,6 @@ private static void EmitNoArgsMethodCallPopReturn( private MethodBuilder dummy1Method; private MethodBuilder dummy2Method; private MethodBuilder dummy3Method; - private MethodInfo workloadImplementationMethod; private MethodBuilder overheadImplementationMethod; private MethodBuilder overheadActionUnrollMethod; private MethodBuilder overheadActionNoUnrollMethod; @@ -281,6 +282,9 @@ private static void EmitNoArgsMethodCallPopReturn( private MethodBuilder runMethod; // ReSharper restore NotAccessedField.Local + public readonly MethodInfo getElapsedMethod; + public readonly MethodInfo startClockMethod; + private RunnableEmitter([NotNull] BuildPartition buildPartition, [NotNull] ModuleBuilder moduleBuilder) { if (buildPartition == null) @@ -290,6 +294,8 @@ private RunnableEmitter([NotNull] BuildPartition buildPartition, [NotNull] Modul this.buildPartition = buildPartition; this.moduleBuilder = moduleBuilder; + getElapsedMethod = typeof(StartedClock).GetMethod(nameof(StartedClock.GetElapsed), BindingFlagsPublicInstance); + startClockMethod = typeof(ClockExtensions).GetMethod(nameof(ClockExtensions.Start), BindingFlagsPublicStatic); } [NotNull] @@ -324,7 +330,6 @@ private Type EmitRunnableCore(BenchmarkBuildInfo newBenchmark) overheadActionNoUnrollMethod = EmitOverheadAction(OverheadActionNoUnrollMethodName, 1); // Workload impl - workloadImplementationMethod = EmitWorkloadImplementation(WorkloadImplementationMethodName); workloadActionUnrollMethod = EmitWorkloadAction(WorkloadActionUnrollMethodName, jobUnrollFactor); workloadActionNoUnrollMethod = EmitWorkloadAction(WorkloadActionNoUnrollMethodName, 1); @@ -354,12 +359,18 @@ private void InitForEmitRunnable(BenchmarkBuildInfo newBenchmark) // Init current state argFields = new List(); benchmark = newBenchmark; + dummyUnrollFactor = DummyUnrollFactor; + + consumableInfo = new ConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.WorkloadMethod.ReturnType); + if (consumableInfo.IsAwaitable) + { + benchmark.BenchmarkCase.ForceUnrollFactorForAsync(); + } + jobUnrollFactor = benchmark.BenchmarkCase.Job.ResolveValue( RunMode.UnrollFactorCharacteristic, buildPartition.Resolver); - dummyUnrollFactor = DummyUnrollFactor; - consumableInfo = new ConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.WorkloadMethod.ReturnType); consumeEmitter = ConsumeEmitter.GetConsumeEmitter(consumableInfo); globalSetupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.GlobalSetupMethod?.ReturnType); globalCleanupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.GlobalCleanupMethod?.ReturnType); @@ -426,13 +437,13 @@ private Type EmitWorkloadDelegateType() private void DefineFields() { globalSetupActionField = - runnableBuilder.DefineField(GlobalSetupActionFieldName, typeof(Action), FieldAttributes.Private); + runnableBuilder.DefineField(GlobalSetupActionFieldName, typeof(Func), FieldAttributes.Private); globalCleanupActionField = - runnableBuilder.DefineField(GlobalCleanupActionFieldName, typeof(Action), FieldAttributes.Private); + runnableBuilder.DefineField(GlobalCleanupActionFieldName, typeof(Func), FieldAttributes.Private); iterationSetupActionField = - runnableBuilder.DefineField(IterationSetupActionFieldName, typeof(Action), FieldAttributes.Private); + runnableBuilder.DefineField(IterationSetupActionFieldName, typeof(Func), FieldAttributes.Private); iterationCleanupActionField = - runnableBuilder.DefineField(IterationCleanupActionFieldName, typeof(Action), FieldAttributes.Private); + runnableBuilder.DefineField(IterationCleanupActionFieldName, typeof(Func), FieldAttributes.Private); overheadDelegateField = runnableBuilder.DefineField(OverheadDelegateFieldName, overheadDelegateType, FieldAttributes.Private); workloadDelegateField = @@ -577,148 +588,17 @@ private MethodBuilder EmitOverheadImplementation(string methodName) return methodBuilder; } - private MethodInfo EmitWorkloadImplementation(string methodName) - { - // Shortcut: DO NOT emit method if the result type is not awaitable - if (!consumableInfo.IsAwaitable) - return Descriptor.WorkloadMethod; - - var workloadInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(workloadDelegateType); - - //.method private hidebysig - // instance int32 __Workload(int64 arg0) cil managed - var args = workloadInvokeMethod.GetParameters(); - var methodBuilder = runnableBuilder.DefineNonVirtualInstanceMethod( - methodName, - MethodAttributes.Private, - workloadInvokeMethod.ReturnParameter, - args); - args = methodBuilder.GetEmitParameters(args); - - var ilBuilder = methodBuilder.GetILGenerator(); - - /* - IL_0026: ldarg.0 - IL_0027: ldloc.0 - IL_0028: ldloc.1 - IL_0029: ldloc.2 - IL_002a: ldloc.3 - IL_002b: call instance class [System.Private.CoreLib]System.Threading.Tasks.Task`1 BenchmarkDotNet.Helpers.Runnable_0::WorkloadMethod(string, string, string, string) - */ - if (!Descriptor.WorkloadMethod.IsStatic) - ilBuilder.Emit(OpCodes.Ldarg_0); - ilBuilder.EmitLdargs(args); - ilBuilder.Emit(OpCodes.Call, Descriptor.WorkloadMethod); - - /* - // BenchmarkDotNet.Helpers.AwaitHelper.GetResult(...); - IL_000e: call !!0 BenchmarkDotNet.Helpers.AwaitHelper::GetResult(valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1) - */ - - ilBuilder.Emit(OpCodes.Call, consumableInfo.GetResultMethod); - - /* - IL_0014: ret - */ - ilBuilder.Emit(OpCodes.Ret); - - return methodBuilder; - } - private MethodBuilder EmitOverheadAction(string methodName, int unrollFactor) { - return EmitActionImpl(methodName, RunnableActionKind.Overhead, unrollFactor); + return consumeEmitter.EmitActionImpl(this, methodName, RunnableActionKind.Overhead, unrollFactor); } private MethodBuilder EmitWorkloadAction(string methodName, int unrollFactor) { - return EmitActionImpl(methodName, RunnableActionKind.Workload, unrollFactor); - } - - private MethodBuilder EmitActionImpl(string methodName, RunnableActionKind actionKind, int unrollFactor) - { - FieldInfo actionDelegateField; - MethodInfo actionInvokeMethod; - switch (actionKind) - { - case RunnableActionKind.Overhead: - actionDelegateField = overheadDelegateField; - actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(overheadDelegateType); - break; - case RunnableActionKind.Workload: - actionDelegateField = workloadDelegateField; - actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(workloadDelegateType); - break; - default: - throw new ArgumentOutOfRangeException(nameof(actionKind), actionKind, null); - } - - // .method private hidebysig - // instance void OverheadActionUnroll(int64 invokeCount) cil managed - var toArg = new EmitParameterInfo(0, InvokeCountParamName, typeof(long)); - var actionMethodBuilder = runnableBuilder.DefineNonVirtualInstanceMethod( - methodName, - MethodAttributes.Private, - EmitParameterInfo.CreateReturnVoidParameter(), - toArg); - toArg.SetMember(actionMethodBuilder); - - // Emit impl - var ilBuilder = actionMethodBuilder.GetILGenerator(); - consumeEmitter.BeginEmitAction(actionMethodBuilder, ilBuilder, actionInvokeMethod, actionKind); - - // init locals - var argLocals = EmitDeclareArgLocals(ilBuilder); - consumeEmitter.DeclareActionLocals(ilBuilder); - var indexLocal = ilBuilder.DeclareLocal(typeof(long)); - - // load fields - EmitLoadArgFieldsToLocals(ilBuilder, argLocals); - consumeEmitter.EmitActionBeforeLoop(ilBuilder); - - // loop - var loopStartLabel = ilBuilder.DefineLabel(); - var loopHeadLabel = ilBuilder.DefineLabel(); - ilBuilder.EmitLoopBeginFromLocToArg(loopStartLabel, loopHeadLabel, indexLocal, toArg); - { - /* - // overheadDelegate(); - IL_0005: ldarg.0 - IL_0006: ldfld class BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate BenchmarkDotNet.Autogenerated.Runnable_0::overheadDelegate - IL_000b: callvirt instance void BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate::Invoke() - // -or- - // consumer.Consume(overheadDelegate(_argField)); - IL_000c: ldarg.0 - IL_000d: ldfld class [BenchmarkDotNet]BenchmarkDotNet.Engines.Consumer BenchmarkDotNet.Autogenerated.Runnable_0::consumer - IL_0012: ldarg.0 - IL_0013: ldfld class BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate BenchmarkDotNet.Autogenerated.Runnable_0::overheadDelegate - IL_0018: ldloc.0 - IL_0019: callvirt instance int32 BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate::Invoke(int64) - IL_001e: callvirt instance void [BenchmarkDotNet]BenchmarkDotNet.Engines.Consumer::Consume(int32) - */ - for (int u = 0; u < unrollFactor; u++) - { - consumeEmitter.EmitActionBeforeCall(ilBuilder); - - ilBuilder.Emit(OpCodes.Ldarg_0); - ilBuilder.Emit(OpCodes.Ldfld, actionDelegateField); - ilBuilder.EmitInstanceCallThisValueOnStack(null, actionInvokeMethod, argLocals); - - consumeEmitter.EmitActionAfterCall(ilBuilder); - } - } - ilBuilder.EmitLoopEndFromLocToArg(loopStartLabel, loopHeadLabel, indexLocal, toArg); - - consumeEmitter.EmitActionAfterLoop(ilBuilder); - consumeEmitter.CompleteEmitAction(ilBuilder); - - // IL_003a: ret - ilBuilder.EmitVoidReturn(actionMethodBuilder); - - return actionMethodBuilder; + return consumeEmitter.EmitActionImpl(this, methodName, RunnableActionKind.Workload, unrollFactor); } - private IReadOnlyList EmitDeclareArgLocals(ILGenerator ilBuilder, bool skipFirst = false) + public IReadOnlyList EmitDeclareArgLocals(ILGenerator ilBuilder, bool skipFirst = false) { // NB: c# compiler does not store first arg in locals for static calls /* @@ -746,7 +626,7 @@ .locals init ( return argLocals; } - private void EmitLoadArgFieldsToLocals(ILGenerator ilBuilder, IReadOnlyList argLocals, bool skipFirstArg = false) + public void EmitLoadArgFieldsToLocals(ILGenerator ilBuilder, IReadOnlyList argLocals, bool skipFirstArg = false) { // NB: c# compiler does not store first arg in locals for static calls int localsOffset = argFields.Count > 0 && skipFirstArg ? -1 : 0; @@ -868,15 +748,6 @@ .locals init ( ilBuilder.EmitLdLocals(argLocals); ilBuilder.Emit(OpCodes.Call, workloadMethod); - if (consumableInfo.IsAwaitable) - { - /* - // BenchmarkDotNet.Helpers.AwaitHelper.GetResult(...); - IL_000e: call !!0 BenchmarkDotNet.Helpers.AwaitHelper::GetResult(valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1) - */ - ilBuilder.Emit(OpCodes.Call, consumableInfo.GetResultMethod); - } - /* IL_0018: ret */ @@ -900,7 +771,7 @@ .locals init ( private void EmitSetupCleanupMethods() { // Emit Setup/Cleanup methods - // We emit empty method instead of EmptyAction = "() => { }" + // We emit simple method instead of simple Action = "() => new System.Threading.Tasks.ValueTask()" globalSetupMethod = EmitWrapperMethod(GlobalSetupMethodName, Descriptor.GlobalSetupMethod, globalSetupReturnInfo); globalCleanupMethod = EmitWrapperMethod(GlobalCleanupMethodName, Descriptor.GlobalCleanupMethod, globalCleanupReturnInfo); iterationSetupMethod = EmitWrapperMethod(IterationSetupMethodName, Descriptor.IterationSetupMethod, iterationSetupReturnInfo); @@ -909,23 +780,37 @@ private void EmitSetupCleanupMethods() private MethodBuilder EmitWrapperMethod(string methodName, MethodInfo optionalTargetMethod, ConsumableTypeInfo returnTypeInfo) { - var methodBuilder = runnableBuilder.DefinePrivateVoidInstanceMethod(methodName); + var methodBuilder = runnableBuilder.DefineNonVirtualInstanceMethod( + methodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask))); var ilBuilder = methodBuilder.GetILGenerator(); - if (optionalTargetMethod != null) + if (returnTypeInfo?.IsAwaitable == true) { - if (returnTypeInfo?.IsAwaitable == true) - { - EmitAwaitableSetupTeardown(methodBuilder, optionalTargetMethod, ilBuilder, returnTypeInfo); - } - else + EmitAwaitableSetupTeardown(methodBuilder, optionalTargetMethod, ilBuilder, returnTypeInfo); + } + else + { + var valueTaskLocal = ilBuilder.DeclareLocal(typeof(ValueTask)); + + if (optionalTargetMethod != null) { EmitNoArgsMethodCallPopReturn(methodBuilder, optionalTargetMethod, ilBuilder, forceDirectCall: true); } + /* + // return new ValueTask(); + IL_0000: ldloca.s 0 + IL_0002: initobj [System.Private.CoreLib]System.Threading.Tasks.ValueTask + IL_0008: ldloc.0 + */ + ilBuilder.EmitLdloca(valueTaskLocal); + ilBuilder.Emit(OpCodes.Initobj, typeof(ValueTask)); + ilBuilder.EmitLdloc(valueTaskLocal); } - ilBuilder.EmitVoidReturn(methodBuilder); + ilBuilder.Emit(OpCodes.Ret); return methodBuilder; } @@ -939,20 +824,19 @@ private void EmitAwaitableSetupTeardown( if (targetMethod == null) throw new ArgumentNullException(nameof(targetMethod)); - if (returnTypeInfo.WorkloadMethodReturnType == typeof(void)) - { - ilBuilder.Emit(OpCodes.Ldarg_0); - } + // BenchmarkDotNet.Helpers.AwaitHelper.ToValueTaskVoid(workloadDelegate()); /* // call for instance // GlobalSetup(); - IL_0006: ldarg.0 - IL_0007: call instance void [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::GlobalSetup() + IL_0000: ldarg.0 + IL_0001: call instance class [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::GlobalSetup() + IL_0006: call valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask BenchmarkDotNet.Helpers.AwaitHelper::ToValueTaskVoid(class [System.Private.CoreLib]System.Threading.Tasks.Task) */ /* // call for static // GlobalSetup(); - IL_0006: call string [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::GlobalCleanup() + IL_0000: call class [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::GlobalCleanup() + IL_0005: call valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask BenchmarkDotNet.Helpers.AwaitHelper::ToValueTaskVoid(class [System.Private.CoreLib]System.Threading.Tasks.Task) */ if (targetMethod.IsStatic) { @@ -969,14 +853,7 @@ private void EmitAwaitableSetupTeardown( ilBuilder.Emit(OpCodes.Ldarg_0); ilBuilder.Emit(OpCodes.Call, targetMethod); } - - /* - // BenchmarkDotNet.Helpers.AwaitHelper.GetResult(...); - IL_000e: call !!0 BenchmarkDotNet.Helpers.AwaitHelper::GetResult(valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1) - */ - - ilBuilder.Emit(OpCodes.Call, consumableInfo.GetResultMethod); - ilBuilder.Emit(OpCodes.Pop); + ilBuilder.Emit(OpCodes.Call, Helpers.AwaitHelper.GetToValueTaskMethod(returnTypeInfo.WorkloadMethodReturnType)); } private void EmitCtorBody() @@ -985,18 +862,14 @@ private void EmitCtorBody() ilBuilder.EmitCallBaseParameterlessCtor(ctorMethod); - consumeEmitter.OnEmitCtorBody(ctorMethod, ilBuilder); + consumeEmitter.OnEmitCtorBody(ctorMethod, ilBuilder, this); ilBuilder.EmitSetDelegateToThisField(globalSetupActionField, globalSetupMethod); ilBuilder.EmitSetDelegateToThisField(globalCleanupActionField, globalCleanupMethod); ilBuilder.EmitSetDelegateToThisField(iterationSetupActionField, iterationSetupMethod); ilBuilder.EmitSetDelegateToThisField(iterationCleanupActionField, iterationCleanupMethod); ilBuilder.EmitSetDelegateToThisField(overheadDelegateField, overheadImplementationMethod); - - if (workloadImplementationMethod == null) - ilBuilder.EmitSetDelegateToThisField(workloadDelegateField, Descriptor.WorkloadMethod); - else - ilBuilder.EmitSetDelegateToThisField(workloadDelegateField, workloadImplementationMethod); + ilBuilder.EmitSetDelegateToThisField(workloadDelegateField, Descriptor.WorkloadMethod); ilBuilder.EmitCtorReturn(ctorMethod); } diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/TaskConsumeEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/TaskConsumeEmitter.cs new file mode 100644 index 0000000000..355dc353f4 --- /dev/null +++ b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/TaskConsumeEmitter.cs @@ -0,0 +1,658 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Helpers.Reflection.Emit; +using Perfolizer.Horology; +using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; +using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableReflectionHelpers; + +namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation +{ + internal class TaskConsumeEmitter : ConsumeEmitter + { + private MethodInfo overheadKeepAliveWithoutBoxingMethod; + private MethodInfo getResultMethod; + + private LocalBuilder disassemblyDiagnoserLocal; + /* + private readonly BenchmarkDotNet.Helpers.AutoResetValueTaskSource valueTaskSource = new BenchmarkDotNet.Helpers.AutoResetValueTaskSource(); + private System.Int64 repeatsRemaining; + private readonly System.Action continuation; + private Perfolizer.Horology.StartedClock startedClock; + private $AwaiterTypeName$ currentAwaiter; + */ + private FieldBuilder valueTaskSourceField; + private FieldBuilder repeatsRemainingField; + private FieldBuilder continuationField; + private FieldBuilder startedClockField; + private FieldBuilder currentAwaiterField; + + private MethodBuilder overheadActionImplMethod; + private MethodBuilder workloadActionImplMethod; + private MethodBuilder setContinuationMethod; + private MethodBuilder runTaskMethod; + private MethodBuilder continuationMethod; + private MethodBuilder setExceptionMethod; + + public TaskConsumeEmitter(ConsumableTypeInfo consumableTypeInfo) : base(consumableTypeInfo) + { + } + + protected override void OnDefineFieldsOverride(TypeBuilder runnableBuilder) + { + overheadKeepAliveWithoutBoxingMethod = typeof(DeadCodeEliminationHelper).GetMethods() + .First(m => m.Name == nameof(DeadCodeEliminationHelper.KeepAliveWithoutBoxing) + && !m.GetParameterTypes().First().IsByRef) + .MakeGenericMethod(ConsumableInfo.OverheadMethodReturnType); + + valueTaskSourceField = runnableBuilder.DefineField(ValueTaskSourceFieldName, typeof(Helpers.AutoResetValueTaskSource), FieldAttributes.Private | FieldAttributes.InitOnly); + repeatsRemainingField = runnableBuilder.DefineField(RepeatsRemainingFieldName, typeof(long), FieldAttributes.Private); + continuationField = runnableBuilder.DefineField(ContinuationFieldName, typeof(Action), FieldAttributes.Private); + startedClockField = runnableBuilder.DefineField(StartedClockFieldName, typeof(StartedClock), FieldAttributes.Private); + // (Value)TaskAwaiter() + currentAwaiterField = runnableBuilder.DefineField(CurrentAwaiterFieldName, + ConsumableInfo.WorkloadMethodReturnType.GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance).ReturnType, + FieldAttributes.Private); + getResultMethod = currentAwaiterField.FieldType.GetMethod(nameof(TaskAwaiter.GetResult), BindingFlagsAllInstance); + } + + protected override void DeclareDisassemblyDiagnoserLocalsOverride(ILGenerator ilBuilder) + { + // optional local if default(T) uses .initobj + disassemblyDiagnoserLocal = ilBuilder.DeclareOptionalLocalForReturnDefault(ConsumableInfo.WorkloadMethodReturnType); + } + + protected override void EmitDisassemblyDiagnoserReturnDefaultOverride(ILGenerator ilBuilder) + { + ilBuilder.EmitReturnDefault(ConsumableInfo.WorkloadMethodReturnType, disassemblyDiagnoserLocal); + } + + protected override void OnEmitCtorBodyOverride(ConstructorBuilder constructorBuilder, ILGenerator ilBuilder, RunnableEmitter runnableEmitter) + { + var ctor = typeof(Helpers.AutoResetValueTaskSource).GetConstructor(Array.Empty()); + if (ctor == null) + throw new InvalidOperationException($"Cannot get default .ctor for {typeof(Helpers.AutoResetValueTaskSource)}"); + + /* + // valueTaskSourceField = new BenchmarkDotNet.Helpers.AutoResetValueTaskSource(); + IL_0000: ldarg.0 + IL_0001: newobj instance void class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1::.ctor() + IL_0006: stfld class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1 BenchmarkDotNet.Autogenerated.Runnable_0::valueTaskSource + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Newobj, ctor); + ilBuilder.Emit(OpCodes.Stfld, valueTaskSourceField); + /* + // __SetContinuation(); + IL_0006: ldarg.0 + IL_0007: call instance void BenchmarkDotNet.Autogenerated.Runnable_0::__SetContinuation() + */ + setContinuationMethod = EmitSetContinuationImpl(runnableEmitter); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Call, setContinuationMethod); + } + + public override MethodBuilder EmitActionImpl(RunnableEmitter runnableEmitter, string methodName, RunnableActionKind actionKind, int unrollFactor) + { + MethodBuilder actionImpl = actionKind switch + { + RunnableActionKind.Overhead => EmitOverheadActionImpl(runnableEmitter), + RunnableActionKind.Workload => EmitWorkloadActionImpl(runnableEmitter), + _ => throw new ArgumentOutOfRangeException(nameof(actionKind), actionKind, null), + }; + + /* + .method private hidebysig + instance valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1 WorkloadActionNoUnroll ( + int64 invokeCount, + class Perfolizer.Horology.IClock clock + ) cil managed + */ + var toArg = new EmitParameterInfo(0, InvokeCountParamName, typeof(long)); + var clockArg = new EmitParameterInfo(1, ClockParamName, typeof(IClock)); + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefineNonVirtualInstanceMethod( + methodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)), + toArg, clockArg); + toArg.SetMember(actionMethodBuilder); + clockArg.SetMember(actionMethodBuilder); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + + /* + // return WorkloadActionImpl(invokeCount, clock); + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: ldarg.2 + IL_0003: call instance valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1 BenchmarkDotNet.Autogenerated.Runnable_0::WorkloadActionImpl(int64, class Perfolizer.Horology.IClock) + IL_0008: ret + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdarg(toArg); + ilBuilder.EmitLdarg(clockArg); + ilBuilder.Emit(OpCodes.Call, actionImpl); + ilBuilder.Emit(OpCodes.Ret); + + return actionMethodBuilder; + } + + private MethodBuilder EmitOverheadActionImpl(RunnableEmitter runnableEmitter) + { + if (overheadActionImplMethod != null) + { + return overheadActionImplMethod; + } + + FieldInfo actionDelegateField = runnableEmitter.overheadDelegateField; + MethodInfo actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(runnableEmitter.overheadDelegateType); + + /* + .method private hidebysig + instance valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1 OverheadActionImpl ( + int64 invokeCount, + class Perfolizer.Horology.IClock clock + ) cil managed + */ + var toArg = new EmitParameterInfo(0, InvokeCountParamName, typeof(long)); + var clockArg = new EmitParameterInfo(1, ClockParamName, typeof(IClock)); + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefineNonVirtualInstanceMethod( + OverheadActionImplMethodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)), + toArg, clockArg); + toArg.SetMember(actionMethodBuilder); + clockArg.SetMember(actionMethodBuilder); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + + // init locals + var valueLocal = ilBuilder.DeclareLocal(ConsumableInfo.OverheadMethodReturnType); + var argLocals = runnableEmitter.EmitDeclareArgLocals(ilBuilder); + var indexLocal = ilBuilder.DeclareLocal(typeof(long)); + + /* + // repeatsRemaining = invokeCount; + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: stfld int64 BenchmarkDotNet.Autogenerated.Runnable_0::repeatsRemaining + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdarg(toArg); + ilBuilder.Emit(OpCodes.Stfld, repeatsRemainingField); + /* + // Task value = default; + IL_0007: ldnull + IL_0008: stloc.0 + */ + ilBuilder.EmitSetLocalToDefault(valueLocal); + /* + // startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); + IL_0009: ldarg.0 + IL_000a: ldarg.2 + IL_000b: call valuetype Perfolizer.Horology.StartedClock Perfolizer.Horology.ClockExtensions::Start(class Perfolizer.Horology.IClock) + IL_0010: stfld valuetype Perfolizer.Horology.StartedClock BenchmarkDotNet.Autogenerated.Runnable_0::startedClock + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdarg(clockArg); + ilBuilder.Emit(OpCodes.Call, runnableEmitter.startClockMethod); + ilBuilder.Emit(OpCodes.Stfld, startedClockField); + + // try { ... } + ilBuilder.BeginExceptionBlock(); + { + // load fields + runnableEmitter.EmitLoadArgFieldsToLocals(ilBuilder, argLocals); + + // while (--repeatsRemaining >= 0) { ... } + var loopStartLabel = ilBuilder.DefineLabel(); + var loopHeadLabel = ilBuilder.DefineLabel(); + ilBuilder.EmitLoopBeginFromFldTo0(loopStartLabel, loopHeadLabel); + { + /* + // value = overheadDelegate(); + IL_0017: ldarg.0 + IL_0018: ldfld class BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate BenchmarkDotNet.Autogenerated.Runnable_0::overheadDelegate + IL_001d: callvirt instance void BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate::Invoke() + IL_0022: stloc.0 + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, actionDelegateField); + ilBuilder.EmitInstanceCallThisValueOnStack(null, actionInvokeMethod, argLocals); + ilBuilder.EmitStloc(valueLocal); + } + ilBuilder.EmitLoopEndFromFldTo0(loopStartLabel, loopHeadLabel, repeatsRemainingField, indexLocal); + } + // catch (System.Exception) { ... } + ilBuilder.BeginCatchBlock(typeof(Exception)); + { + // IL_003b: pop + ilBuilder.Emit(OpCodes.Pop); + /* + // BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + IL_003c: ldloc.0 + IL_003d: call void BenchmarkDotNet.Engines.DeadCodeEliminationHelper::KeepAliveWithoutBoxing>(!!0) + */ + ilBuilder.EmitStaticCall(overheadKeepAliveWithoutBoxingMethod, valueLocal); + // IL_0042: rethrow + ilBuilder.Emit(OpCodes.Rethrow); + } + ilBuilder.EndExceptionBlock(); + + /* + // return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); + IL_0044: ldarg.0 + IL_0045: ldflda valuetype Perfolizer.Horology.StartedClock BenchmarkDotNet.Autogenerated.Runnable_0::startedClock + IL_004a: call instance valuetype Perfolizer.Horology.ClockSpan Perfolizer.Horology.StartedClock::GetElapsed() + IL_004f: newobj instance void valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1::.ctor(!0) + IL_0054: ret + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, startedClockField); + ilBuilder.Emit(OpCodes.Call, runnableEmitter.getElapsedMethod); + var ctor = typeof(ValueTask).GetConstructor(new[] { typeof(ClockSpan) }); + ilBuilder.Emit(OpCodes.Newobj, ctor); + ilBuilder.Emit(OpCodes.Ret); + + return overheadActionImplMethod = actionMethodBuilder; + } + + private MethodBuilder EmitWorkloadActionImpl(RunnableEmitter runnableEmitter) + { + if (workloadActionImplMethod != null) + { + return workloadActionImplMethod; + } + + setExceptionMethod = EmitSetExceptionImpl(runnableEmitter); + runTaskMethod = EmitRunTaskImpl(runnableEmitter); + continuationMethod = EmitContinuationImpl(runnableEmitter); + + /* + .method private hidebysig + instance valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1 WorkloadActionImpl ( + int64 invokeCount, + class Perfolizer.Horology.IClock clock + ) cil managed + */ + var toArg = new EmitParameterInfo(0, InvokeCountParamName, typeof(long)); + var clockArg = new EmitParameterInfo(1, ClockParamName, typeof(IClock)); + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefineNonVirtualInstanceMethod( + WorkloadActionImplMethodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)), + toArg, clockArg); + toArg.SetMember(actionMethodBuilder); + clockArg.SetMember(actionMethodBuilder); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + + /* + // repeatsRemaining = invokeCount; + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: stfld int64 BenchmarkDotNet.Autogenerated.Runnable_0::repeatsRemaining + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdarg(toArg); + ilBuilder.Emit(OpCodes.Stfld, repeatsRemainingField); + /* + // startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); + IL_0012: ldarg.0 + IL_0013: ldarg.2 + IL_0014: call valuetype Perfolizer.Horology.StartedClock Perfolizer.Horology.ClockExtensions::Start(class Perfolizer.Horology.IClock) + IL_0019: stfld valuetype Perfolizer.Horology.StartedClock BenchmarkDotNet.Autogenerated.Runnable_0::startedClock + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdarg(clockArg); + ilBuilder.Emit(OpCodes.Call, runnableEmitter.startClockMethod); + ilBuilder.Emit(OpCodes.Stfld, startedClockField); + /* + // __RunTask(); + IL_001e: ldarg.0 + IL_001f: call instance void BenchmarkDotNet.Autogenerated.Runnable_0::__RunTask() + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Call, runTaskMethod); + /* + // return new System.Threading.Tasks.ValueTask(valueTaskSource, valueTaskSource.Version); + IL_0024: ldarg.0 + IL_0025: ldfld class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1 BenchmarkDotNet.Autogenerated.Runnable_0::valueTaskSource + IL_002a: ldarg.0 + IL_002b: ldfld class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1 BenchmarkDotNet.Autogenerated.Runnable_0::valueTaskSource + IL_0030: callvirt instance int16 class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1::get_Version() + IL_0035: newobj instance void valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1::.ctor(class [System.Private.CoreLib]System.Threading.Tasks.Sources.IValueTaskSource`1, int16) + IL_003a: ret + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, valueTaskSourceField); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, valueTaskSourceField); + var getVersionMethod = valueTaskSourceField.FieldType.GetProperty(nameof(Helpers.AutoResetValueTaskSource.Version), BindingFlagsPublicInstance).GetGetMethod(true); + ilBuilder.Emit(OpCodes.Callvirt, getVersionMethod); + var ctor = actionMethodBuilder.ReturnType.GetConstructor(new[] { valueTaskSourceField.FieldType, getVersionMethod.ReturnType }); + ilBuilder.Emit(OpCodes.Newobj, ctor); + ilBuilder.Emit(OpCodes.Ret); + + return workloadActionImplMethod = actionMethodBuilder; + } + + private MethodBuilder EmitSetContinuationImpl(RunnableEmitter runnableEmitter) + { + /* + .method private hidebysig + instance void __SetContinuation () cil managed + */ + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefinePrivateVoidInstanceMethod(SetContinuationMethodName); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + // continuation = __Continuation; + ilBuilder.EmitSetDelegateToThisField(continuationField, continuationMethod); + ilBuilder.Emit(OpCodes.Ret); + + return actionMethodBuilder; + } + + private MethodBuilder EmitRunTaskImpl(RunnableEmitter runnableEmitter) + { + /* + .method private hidebysig + instance void __RunTask () cil managed + */ + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefineNonVirtualInstanceMethod( + RunTaskMethodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnVoidParameter()); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + + FieldInfo actionDelegateField = runnableEmitter.workloadDelegateField; + MethodInfo actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(runnableEmitter.workloadDelegateType); + + // init locals + //.locals init ( + // [0] valuetype Perfolizer.Horology.ClockSpan clockspan, + // // [1] valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1, // If ValueTask + // [1] int64, + // [2] class [System.Private.CoreLib]System.Exception e + //) + var clockspanLocal = ilBuilder.DeclareLocal(typeof(ClockSpan)); + var argLocals = runnableEmitter.EmitDeclareArgLocals(ilBuilder); + LocalBuilder maybeValueTaskLocal = actionInvokeMethod.ReturnType.IsValueType + ? ilBuilder.DeclareLocal(actionInvokeMethod.ReturnType) + : null; + var indexLocal = ilBuilder.DeclareLocal(typeof(long)); + var exceptionLocal = ilBuilder.DeclareLocal(typeof(Exception)); + + var returnLabel = ilBuilder.DefineLabel(); + + // try { ... } + ilBuilder.BeginExceptionBlock(); + { + // load fields + runnableEmitter.EmitLoadArgFieldsToLocals(ilBuilder, argLocals); + + // while (--repeatsRemaining >= 0) { ... } + var loopStartLabel = ilBuilder.DefineLabel(); + var loopHeadLabel = ilBuilder.DefineLabel(); + ilBuilder.EmitLoopBeginFromFldTo0(loopStartLabel, loopHeadLabel); + { + /* + // currentAwaiter = workloadDelegate().GetAwaiter(); + IL_0002: ldarg.0 + IL_0003: ldarg.0 + IL_0004: ldfld class [System.Private.CoreLib]System.Func`1> BenchmarkDotNet.Autogenerated.Runnable_0::workloadDelegate + IL_0009: callvirt instance !0 class [System.Private.CoreLib]System.Func`1>::Invoke() + IL_000e: stloc.1 + IL_000f: ldloca.s 1 + IL_0011: call instance valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1::GetAwaiter() + IL_0016: stfld valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_0::currentAwaiter + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, actionDelegateField); + ilBuilder.EmitInstanceCallThisValueOnStack(null, actionInvokeMethod, argLocals); + ilBuilder.EmitInstanceCallThisValueOnStack(maybeValueTaskLocal, ConsumableInfo.WorkloadMethodReturnType.GetMethod(nameof(Task.GetAwaiter), BindingFlagsAllInstance)); + ilBuilder.Emit(OpCodes.Stfld, currentAwaiterField); + /* + // if (!currentAwaiter.IsCompleted) + IL_001b: ldarg.0 + IL_001c: ldflda valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_0::currentAwaiter + IL_0021: call instance bool valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1::get_IsCompleted() + IL_0026: brtrue.s IL_003b + */ + var isCompletedLabel = ilBuilder.DefineLabel(); + + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, currentAwaiterField); + ilBuilder.Emit(OpCodes.Call, currentAwaiterField.FieldType.GetProperty(nameof(TaskAwaiter.IsCompleted), BindingFlagsAllInstance).GetGetMethod(true)); + ilBuilder.Emit(OpCodes.Brtrue, isCompletedLabel); + { + /* + // currentAwaiter.UnsafeOnCompleted(continuation); + IL_0028: ldarg.0 + IL_0029: ldflda valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_0::currentAwaiter + IL_002e: ldarg.0 + IL_002f: ldfld class [System.Private.CoreLib]System.Action BenchmarkDotNet.Autogenerated.Runnable_0::continuation + IL_0034: call instance void valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1::UnsafeOnCompleted(class [System.Private.CoreLib]System.Action) + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, currentAwaiterField); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, continuationField); + ilBuilder.Emit(OpCodes.Call, currentAwaiterField.FieldType.GetMethod(nameof(TaskAwaiter.UnsafeOnCompleted), BindingFlagsAllInstance)); + // return; + ilBuilder.Emit(OpCodes.Leave, returnLabel); + } + ilBuilder.MarkLabel(isCompletedLabel); + /* + // currentAwaiter.GetResult(); + IL_003b: ldarg.0 + IL_003c: ldflda valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_0::currentAwaiter + IL_0041: call instance !0 valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1::GetResult() + IL_0046: pop + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, currentAwaiterField); + ilBuilder.Emit(OpCodes.Call, getResultMethod); + if (getResultMethod.ReturnType != typeof(void)) + { + ilBuilder.Emit(OpCodes.Pop); + } + } + ilBuilder.EmitLoopEndFromFldTo0(loopStartLabel, loopHeadLabel, repeatsRemainingField, indexLocal); + } + // catch (System.Exception) { ... } + ilBuilder.BeginCatchBlock(typeof(Exception)); + { + /* + // __SetException(e); + IL_005f: stloc.3 + IL_0060: ldarg.0 + IL_0061: ldloc.3 + IL_0062: call instance void BenchmarkDotNet.Autogenerated.Runnable_0::__SetException(class [System.Private.CoreLib]System.Exception) + */ + ilBuilder.EmitStloc(exceptionLocal); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdloc(exceptionLocal); + ilBuilder.Emit(OpCodes.Call, setExceptionMethod); + // return; + ilBuilder.Emit(OpCodes.Leave, returnLabel); + } + ilBuilder.EndExceptionBlock(); + + /* + // var clockspan = startedClock.GetElapsed(); + IL_0069: ldarg.0 + IL_006a: ldflda valuetype Perfolizer.Horology.StartedClock BenchmarkDotNet.Autogenerated.Runnable_0::startedClock + IL_006f: call instance valuetype Perfolizer.Horology.ClockSpan Perfolizer.Horology.StartedClock::GetElapsed() + IL_0074: stloc.0 + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, startedClockField); + ilBuilder.Emit(OpCodes.Call, runnableEmitter.getElapsedMethod); + ilBuilder.EmitStloc(clockspanLocal); + /* + // currentAwaiter = default; + IL_0075: ldarg.0 + IL_0076: ldflda valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_0::currentAwaiter + IL_007b: initobj valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, currentAwaiterField); + ilBuilder.Emit(OpCodes.Initobj, currentAwaiterField.FieldType); + /* + // startedClock = default(Perfolizer.Horology.StartedClock); + IL_0081: ldarg.0 + IL_0082: ldflda valuetype Perfolizer.Horology.StartedClock BenchmarkDotNet.Autogenerated.Runnable_0::startedClock + IL_0087: initobj Perfolizer.Horology.StartedClock + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, startedClockField); + ilBuilder.Emit(OpCodes.Initobj, startedClockField.FieldType); + /* + // valueTaskSource.SetResult(clockspan); + IL_008d: ldarg.0 + IL_008e: ldfld class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1 BenchmarkDotNet.Autogenerated.Runnable_0::valueTaskSource + IL_0093: ldloc.0 + IL_0094: callvirt instance void class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1::SetResult(!0) + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, valueTaskSourceField); + ilBuilder.EmitLdloc(clockspanLocal); + ilBuilder.Emit(OpCodes.Callvirt, valueTaskSourceField.FieldType.GetMethod(nameof(Helpers.AutoResetValueTaskSource.SetResult), BindingFlagsPublicInstance)); + + ilBuilder.MarkLabel(returnLabel); + ilBuilder.Emit(OpCodes.Ret); + + return actionMethodBuilder; + } + + private MethodBuilder EmitContinuationImpl(RunnableEmitter runnableEmitter) + { + /* + .method private hidebysig + instance void __Continuation () cil managed + */ + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefineNonVirtualInstanceMethod( + ContinuationMethodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnVoidParameter()); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + + // init locals + var exceptionLocal = ilBuilder.DeclareLocal(typeof(Exception)); + + var returnLabel = ilBuilder.DefineLabel(); + + // try { ... } + ilBuilder.BeginExceptionBlock(); + { + /* + // currentAwaiter.GetResult(); + IL_0000: ldarg.0 + IL_0001: ldflda valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_0::currentAwaiter + IL_0006: call instance !0 valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1::GetResult() + IL_000b: pop + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, currentAwaiterField); + ilBuilder.Emit(OpCodes.Call, getResultMethod); + if (getResultMethod.ReturnType != typeof(void)) + { + ilBuilder.Emit(OpCodes.Pop); + } + } + // catch (System.Exception e) { ... } + ilBuilder.BeginCatchBlock(typeof(Exception)); + { + // IL_000e: stloc.0 + ilBuilder.EmitStloc(exceptionLocal); + /* + // __SetException(e); + IL_000f: ldarg.0 + IL_0010: ldloc.0 + IL_0011: call instance void BenchmarkDotNet.Autogenerated.Runnable_0::__SetException(class [System.Private.CoreLib]System.Exception) + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdloc(exceptionLocal); + ilBuilder.Emit(OpCodes.Call, setExceptionMethod); + // return; + ilBuilder.Emit(OpCodes.Leave, returnLabel); + } + ilBuilder.EndExceptionBlock(); + + /* + // __RunTask(); + IL_0018: ldarg.0 + IL_0019: call instance void BenchmarkDotNet.Autogenerated.Runnable_0::__RunTask() + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Call, runTaskMethod); + + // return; + ilBuilder.MarkLabel(returnLabel); + ilBuilder.Emit(OpCodes.Ret); + + return actionMethodBuilder; + } + + private MethodBuilder EmitSetExceptionImpl(RunnableEmitter runnableEmitter) + { + /* + .method private hidebysig + instance void __SetException ( + class [System.Private.CoreLib]System.Exception e + ) cil managed + */ + var exceptionArg = new EmitParameterInfo(0, "e", typeof(Exception)); + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefineNonVirtualInstanceMethod( + SetExceptionMethodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnVoidParameter(), + exceptionArg); + exceptionArg.SetMember(actionMethodBuilder); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + /* + // currentAwaiter = default; + IL_0000: ldarg.0 + IL_0001: ldflda valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_0::currentAwaiter + IL_0006: initobj valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, currentAwaiterField); + ilBuilder.Emit(OpCodes.Initobj, currentAwaiterField.FieldType); + /* + // startedClock = default(Perfolizer.Horology.StartedClock); + IL_000c: ldarg.0 + IL_000d: ldflda valuetype Perfolizer.Horology.StartedClock BenchmarkDotNet.Autogenerated.Runnable_0::startedClock + IL_0012: initobj Perfolizer.Horology.StartedClock + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, startedClockField); + ilBuilder.Emit(OpCodes.Initobj, startedClockField.FieldType); + /* + // valueTaskSource.SetException(e); + IL_0018: ldarg.0 + IL_0019: ldfld class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1 BenchmarkDotNet.Autogenerated.Runnable_0::valueTaskSource + IL_001e: ldarg.1 + IL_001f: callvirt instance void class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1::SetException(class [System.Private.CoreLib]System.Exception) + IL_0024: ret + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, valueTaskSourceField); + ilBuilder.EmitLdarg(exceptionArg); + var setExceptionMethod = valueTaskSourceField.FieldType.GetMethod(nameof(Helpers.AutoResetValueTaskSource.SetException), BindingFlagsPublicInstance); + ilBuilder.Emit(OpCodes.Callvirt, setExceptionMethod); + ilBuilder.Emit(OpCodes.Ret); + + return actionMethodBuilder; + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableConstants.cs b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableConstants.cs index c6e8cd8ae1..63237ee10b 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableConstants.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableConstants.cs @@ -37,12 +37,25 @@ public class RunnableConstants public const string WorkloadActionNoUnrollMethodName = "WorkloadActionNoUnroll"; public const string ForDisassemblyDiagnoserMethodName = "__ForDisassemblyDiagnoser__"; public const string InvokeCountParamName = "invokeCount"; + public const string ClockParamName = "clock"; public const string ConsumerFieldName = "consumer"; public const string NonGenericKeepAliveWithoutBoxingMethodName = "NonGenericKeepAliveWithoutBoxing"; public const string DummyParamName = "_"; public const string WorkloadDefaultValueHolderFieldName = "workloadDefaultValueHolder"; + public const string ValueTaskSourceFieldName = "valueTaskSource"; + public const string RepeatsRemainingFieldName = "repeatsRemaining"; + public const string ContinuationFieldName = "continuation"; + public const string StartedClockFieldName = "startedClock"; + public const string CurrentAwaiterFieldName = "currentAwaiter"; + public const string OverheadActionImplMethodName = "OverheadActionImpl"; + public const string WorkloadActionImplMethodName = "WorkloadActionImpl"; + public const string SetContinuationMethodName = "__SetContinuation"; + public const string RunTaskMethodName = "__RunTask"; + public const string ContinuationMethodName = "__Continuation"; + public const string SetExceptionMethodName = "__SetException"; + public const string GlobalSetupMethodName = "GlobalSetup"; public const string GlobalCleanupMethodName = "GlobalCleanup"; public const string IterationSetupMethodName = "IterationSetup"; diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableReflectionHelpers.cs b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableReflectionHelpers.cs index f8cfd02ad1..d1fa515d00 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableReflectionHelpers.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Runnable/RunnableReflectionHelpers.cs @@ -1,8 +1,10 @@ using System; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using BenchmarkDotNet.Parameters; using BenchmarkDotNet.Running; +using Perfolizer.Horology; using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation @@ -104,9 +106,9 @@ public static void SetParameter( } } - public static Action CallbackFromField(T instance, string memberName) + public static Func CallbackFromField(T instance, string memberName) { - return GetFieldValueCore(instance, memberName); + return GetFieldValueCore>(instance, memberName); } public static Action CallbackFromMethod(T instance, string memberName) @@ -114,9 +116,9 @@ public static Action CallbackFromMethod(T instance, string memberName) return GetDelegateCore(instance, memberName); } - public static Action LoopCallbackFromMethod(T instance, string memberName) + public static Func> LoopCallbackFromMethod(T instance, string memberName) { - return GetDelegateCore>(instance, memberName); + return GetDelegateCore>>(instance, memberName); } private static TResult GetFieldValueCore(T instance, string memberName) diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkAction.cs b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkAction.cs index df1911d0b0..01b69fccae 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkAction.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkAction.cs @@ -1,6 +1,7 @@ using System; - +using System.Threading.Tasks; using JetBrains.Annotations; +using Perfolizer.Horology; namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit { @@ -8,16 +9,9 @@ namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit [PublicAPI] public abstract class BenchmarkAction { - /// Gets or sets invoke single callback. - /// Invoke single callback. - public Action InvokeSingle { get; protected set; } - - /// Gets or sets invoke multiple times callback. - /// Invoke multiple times callback. - public Action InvokeMultiple { get; protected set; } - - /// Gets the last run result. - /// The last run result. + public Func InvokeSingle { get; protected set; } + public Func> InvokeUnroll { get; protected set; } + public Func> InvokeNoUnroll { get; protected set; } public virtual object LastRunResult => null; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory.cs b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory.cs index 130c67dbf3..603466ed05 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory.cs @@ -101,6 +101,21 @@ private static void FallbackMethod() { } private static readonly MethodInfo FallbackSignature = new Action(FallbackMethod).GetMethodInfo(); private static readonly MethodInfo DummyMethod = typeof(DummyInstance).GetMethod(nameof(DummyInstance.Dummy)); + internal static int GetUnrollFactor(BenchmarkCase benchmarkCase) + { + // Only support (Value)Task for async benchmarks. + var methodReturnType = benchmarkCase.Descriptor.WorkloadMethod.ReturnType; + bool isAwaitable = methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask) + || (methodReturnType.GetTypeInfo().IsGenericType + && (methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) + || methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>))); + if (isAwaitable) + { + benchmarkCase.ForceUnrollFactorForAsync(); + } + return benchmarkCase.Job.ResolveValue(Jobs.RunMode.UnrollFactorCharacteristic, Environments.EnvironmentResolver.Instance); + } + /// Creates run benchmark action. /// Descriptor info. /// Instance of target. diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs index 2ed1f7ccc6..1f548ad5ec 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/BenchmarkActionFactory_Implementations.cs @@ -1,5 +1,8 @@ -using System; +using BenchmarkDotNet.Helpers; +using Perfolizer.Horology; +using System; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading.Tasks; namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit @@ -22,19 +25,36 @@ internal class BenchmarkActionVoid : BenchmarkActionBase public BenchmarkActionVoid(object instance, MethodInfo method, int unrollFactor) { callback = CreateWorkloadOrOverhead(instance, method, OverheadStatic, OverheadInstance); - InvokeSingle = callback; + InvokeSingle = InvokeSingleHardcoded; unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + InvokeUnroll = InvokeUnrollHardcoded; + InvokeNoUnroll = InvokeNoUnrollHardcoded; } private static void OverheadStatic() { } private void OverheadInstance() { } - private void InvokeMultipleHardcoded(long repeatCount) + private ValueTask InvokeSingleHardcoded() { + callback(); + return new ValueTask(); + } + + private ValueTask InvokeUnrollHardcoded(long repeatCount, IClock clock) + { + var startedClock = clock.Start(); for (long i = 0; i < repeatCount; i++) unrolledCallback(); + return new ValueTask(startedClock.GetElapsed()); + } + + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + { + var startedClock = clock.Start(); + for (long i = 0; i < repeatCount; i++) + callback(); + return new ValueTask(startedClock.GetElapsed()); } } @@ -50,18 +70,34 @@ public BenchmarkAction(object instance, MethodInfo method, int unrollFactor) InvokeSingle = InvokeSingleHardcoded; unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + InvokeUnroll = InvokeUnrollHardcoded; + InvokeNoUnroll = InvokeNoUnrollHardcoded; } private static T OverheadStatic() => default; private T OverheadInstance() => default; - private void InvokeSingleHardcoded() => result = callback(); + private ValueTask InvokeSingleHardcoded() + { + result = callback(); + return new ValueTask(); + } - private void InvokeMultipleHardcoded(long repeatCount) + private ValueTask InvokeUnrollHardcoded(long repeatCount, IClock clock) { + var startedClock = clock.Start(); for (long i = 0; i < repeatCount; i++) result = unrolledCallback(); + unrolledCallback(); + return new ValueTask(startedClock.GetElapsed()); + } + + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + { + var startedClock = clock.Start(); + for (long i = 0; i < repeatCount; i++) + callback(); + return new ValueTask(startedClock.GetElapsed()); } public override object LastRunResult => result; @@ -69,80 +105,234 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionTask : BenchmarkActionBase { - private readonly Func startTaskCallback; - private readonly Action callback; - private readonly Action unrolledCallback; + private readonly Func callback; + private readonly AutoResetValueTaskSource valueTaskSource = new AutoResetValueTaskSource(); + private long repeatsRemaining; + private readonly Action continuation; + private StartedClock startedClock; + private TaskAwaiter currentAwaiter; public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) { + continuation = Continuation; bool isIdle = method == null; if (!isIdle) { - startTaskCallback = CreateWorkload>(instance, method); - callback = ExecuteBlocking; + callback = CreateWorkload>(instance, method); + InvokeSingle = InvokeSingleHardcoded; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; } else { callback = Overhead; + InvokeSingle = InvokeSingleHardcodedOverhead; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcodedOverhead; } + } - InvokeSingle = callback; + private Task Overhead() => default; - unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + private ValueTask InvokeSingleHardcodedOverhead() + { + callback(); + return new ValueTask(); + } + private ValueTask InvokeNoUnrollHardcodedOverhead(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + Task value = default; + startedClock = clock.Start(); + try + { + while (--repeatsRemaining >= 0) + { + value = callback(); + } + } + catch (Exception) + { + Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + throw; + } + return new ValueTask(startedClock.GetElapsed()); } - // must be kept in sync with VoidDeclarationsProvider.IdleImplementation - private void Overhead() { } + private ValueTask InvokeSingleHardcoded() + { + return AwaitHelper.ToValueTaskVoid(callback()); + } - // must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate - private void ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + startedClock = clock.Start(); + RunTask(); + return new ValueTask(valueTaskSource, valueTaskSource.Version); + } - private void InvokeMultipleHardcoded(long repeatCount) + private void RunTask() { - for (long i = 0; i < repeatCount; i++) - unrolledCallback(); + try + { + while (--repeatsRemaining >= 0) + { + currentAwaiter = callback().GetAwaiter(); + if (!currentAwaiter.IsCompleted) + { + currentAwaiter.UnsafeOnCompleted(continuation); + return; + } + currentAwaiter.GetResult(); + } + } + catch (Exception e) + { + SetException(e); + return; + } + var clockspan = startedClock.GetElapsed(); + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetResult(clockspan); + } + + private void Continuation() + { + try + { + currentAwaiter.GetResult(); + } + catch (Exception e) + { + SetException(e); + return; + } + RunTask(); + } + + private void SetException(Exception e) + { + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetException(e); } } internal class BenchmarkActionTask : BenchmarkActionBase { - private readonly Func> startTaskCallback; - private readonly Func callback; - private readonly Func unrolledCallback; + private readonly Func> callback; + private readonly AutoResetValueTaskSource valueTaskSource = new AutoResetValueTaskSource(); + private long repeatsRemaining; + private readonly Action continuation; + private StartedClock startedClock; + private TaskAwaiter currentAwaiter; private T result; public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) { - bool isOverhead = method == null; - if (!isOverhead) + continuation = Continuation; + bool isIdle = method == null; + if (!isIdle) { - startTaskCallback = CreateWorkload>>(instance, method); - callback = ExecuteBlocking; + callback = CreateWorkload>>(instance, method); + InvokeSingle = InvokeSingleHardcoded; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; } else { callback = Overhead; + InvokeSingle = InvokeSingleHardcodedOverhead; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcodedOverhead; } + } - InvokeSingle = InvokeSingleHardcoded; + private Task Overhead() => default; - unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + private ValueTask InvokeSingleHardcodedOverhead() + { + callback(); + return new ValueTask(); } - private T Overhead() => default; + private ValueTask InvokeNoUnrollHardcodedOverhead(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + Task value = default; + startedClock = clock.Start(); + try + { + while (--repeatsRemaining >= 0) + { + value = callback(); + } + } + catch (Exception) + { + Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + throw; + } + return new ValueTask(startedClock.GetElapsed()); + } - // must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate - private T ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); + private ValueTask InvokeSingleHardcoded() + { + return AwaitHelper.ToValueTaskVoid(callback()); + } - private void InvokeSingleHardcoded() => result = callback(); + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + startedClock = clock.Start(); + RunTask(); + return new ValueTask(valueTaskSource, valueTaskSource.Version); + } - private void InvokeMultipleHardcoded(long repeatCount) + private void RunTask() { - for (long i = 0; i < repeatCount; i++) - result = unrolledCallback(); + try + { + while (--repeatsRemaining >= 0) + { + currentAwaiter = callback().GetAwaiter(); + if (!currentAwaiter.IsCompleted) + { + currentAwaiter.UnsafeOnCompleted(continuation); + return; + } + result = currentAwaiter.GetResult(); + } + } + catch (Exception e) + { + SetException(e); + return; + } + var clockspan = startedClock.GetElapsed(); + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetResult(clockspan); + } + + private void Continuation() + { + try + { + result = currentAwaiter.GetResult(); + } + catch (Exception e) + { + SetException(e); + return; + } + RunTask(); + } + + private void SetException(Exception e) + { + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetException(e); } public override object LastRunResult => result; @@ -150,81 +340,234 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionValueTask : BenchmarkActionBase { - private readonly Func startTaskCallback; - private readonly Action callback; - private readonly Action unrolledCallback; + private readonly Func callback; + private readonly AutoResetValueTaskSource valueTaskSource = new AutoResetValueTaskSource(); + private long repeatsRemaining; + private readonly Action continuation; + private StartedClock startedClock; + private ValueTaskAwaiter currentAwaiter; public BenchmarkActionValueTask(object instance, MethodInfo method, int unrollFactor) { + continuation = Continuation; bool isIdle = method == null; if (!isIdle) { - startTaskCallback = CreateWorkload>(instance, method); - callback = ExecuteBlocking; + callback = CreateWorkload>(instance, method); + InvokeSingle = InvokeSingleHardcoded; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; } else { callback = Overhead; + InvokeSingle = InvokeSingleHardcodedOverhead; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcodedOverhead; } + } - InvokeSingle = callback; + private ValueTask Overhead() => default; - unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + private ValueTask InvokeSingleHardcodedOverhead() + { + callback(); + return new ValueTask(); + } + + private ValueTask InvokeNoUnrollHardcodedOverhead(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + ValueTask value = default; + startedClock = clock.Start(); + try + { + while (--repeatsRemaining >= 0) + { + value = callback(); + } + } + catch (Exception) + { + Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + throw; + } + return new ValueTask(startedClock.GetElapsed()); + } + private ValueTask InvokeSingleHardcoded() + { + return AwaitHelper.ToValueTaskVoid(callback()); } - // must be kept in sync with VoidDeclarationsProvider.IdleImplementation - private void Overhead() { } + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + startedClock = clock.Start(); + RunTask(); + return new ValueTask(valueTaskSource, valueTaskSource.Version); + } - // must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate - private void ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); + private void RunTask() + { + try + { + while (--repeatsRemaining >= 0) + { + currentAwaiter = callback().GetAwaiter(); + if (!currentAwaiter.IsCompleted) + { + currentAwaiter.UnsafeOnCompleted(continuation); + return; + } + currentAwaiter.GetResult(); + } + } + catch (Exception e) + { + SetException(e); + return; + } + var clockspan = startedClock.GetElapsed(); + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetResult(clockspan); + } - private void InvokeMultipleHardcoded(long repeatCount) + private void Continuation() { - for (long i = 0; i < repeatCount; i++) - unrolledCallback(); + try + { + currentAwaiter.GetResult(); + } + catch (Exception e) + { + SetException(e); + return; + } + RunTask(); + } + + private void SetException(Exception e) + { + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetException(e); } } internal class BenchmarkActionValueTask : BenchmarkActionBase { - private readonly Func> startTaskCallback; - private readonly Func callback; - private readonly Func unrolledCallback; + private readonly Func> callback; + private readonly AutoResetValueTaskSource valueTaskSource = new AutoResetValueTaskSource(); + private long repeatsRemaining; + private readonly Action continuation; + private StartedClock startedClock; + private ValueTaskAwaiter currentAwaiter; private T result; public BenchmarkActionValueTask(object instance, MethodInfo method, int unrollFactor) { - bool isOverhead = method == null; - if (!isOverhead) + continuation = Continuation; + bool isIdle = method == null; + if (!isIdle) { - startTaskCallback = CreateWorkload>>(instance, method); - callback = ExecuteBlocking; + callback = CreateWorkload>>(instance, method); + InvokeSingle = InvokeSingleHardcoded; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; } else { callback = Overhead; + InvokeSingle = InvokeSingleHardcodedOverhead; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcodedOverhead; } + } - InvokeSingle = InvokeSingleHardcoded; + private ValueTask Overhead() => default; + private ValueTask InvokeSingleHardcodedOverhead() + { + callback(); + return new ValueTask(); + } - unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + private ValueTask InvokeNoUnrollHardcodedOverhead(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + ValueTask value = default; + startedClock = clock.Start(); + try + { + while (--repeatsRemaining >= 0) + { + value = callback(); + } + } + catch (Exception) + { + Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + throw; + } + return new ValueTask(startedClock.GetElapsed()); + } + + private ValueTask InvokeSingleHardcoded() + { + return AwaitHelper.ToValueTaskVoid(callback()); } - private T Overhead() => default; + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + startedClock = clock.Start(); + RunTask(); + return new ValueTask(valueTaskSource, valueTaskSource.Version); + } - // must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate - private T ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); + private void RunTask() + { + try + { + while (--repeatsRemaining >= 0) + { + currentAwaiter = callback().GetAwaiter(); + if (!currentAwaiter.IsCompleted) + { + currentAwaiter.UnsafeOnCompleted(continuation); + return; + } + result = currentAwaiter.GetResult(); + } + } + catch (Exception e) + { + SetException(e); + return; + } + var clockspan = startedClock.GetElapsed(); + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetResult(clockspan); + } - private void InvokeSingleHardcoded() => result = callback(); + private void Continuation() + { + try + { + result = currentAwaiter.GetResult(); + } + catch (Exception e) + { + SetException(e); + return; + } + RunTask(); + } - private void InvokeMultipleHardcoded(long repeatCount) + private void SetException(Exception e) { - for (long i = 0; i < repeatCount; i++) - result = unrolledCallback(); + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetException(e); } public override object LastRunResult => result; diff --git a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/InProcessNoEmitRunner.cs b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/InProcessNoEmitRunner.cs index e890000683..a6f5e37619 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/InProcessNoEmitRunner.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess.NoEmit/InProcessNoEmitRunner.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using System.Threading.Tasks; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Exporters; @@ -7,6 +8,7 @@ using BenchmarkDotNet.Running; using JetBrains.Annotations; +using Perfolizer.Horology; namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit { @@ -102,9 +104,9 @@ private static class Runnable { public static void RunCore(IHost host, BenchmarkCase benchmarkCase) { + int unrollFactor = BenchmarkActionFactory.GetUnrollFactor(benchmarkCase); var target = benchmarkCase.Descriptor; var job = benchmarkCase.Job; // TODO: filter job (same as SourceCodePresenter does)? - int unrollFactor = benchmarkCase.Job.ResolveValue(RunMode.UnrollFactorCharacteristic, EnvironmentResolver.Instance); // DONTTOUCH: these should be allocated together var instance = Activator.CreateInstance(benchmarkCase.Descriptor.Type); @@ -129,21 +131,13 @@ public static void RunCore(IHost host, BenchmarkCase benchmarkCase) var engineParameters = new EngineParameters { Host = host, - WorkloadActionNoUnroll = invocationCount => - { - for (int i = 0; i < invocationCount; i++) - workloadAction.InvokeSingle(); - }, - WorkloadActionUnroll = workloadAction.InvokeMultiple, - Dummy1Action = dummy1.InvokeSingle, - Dummy2Action = dummy2.InvokeSingle, - Dummy3Action = dummy3.InvokeSingle, - OverheadActionNoUnroll = invocationCount => - { - for (int i = 0; i < invocationCount; i++) - overheadAction.InvokeSingle(); - }, - OverheadActionUnroll = overheadAction.InvokeMultiple, + WorkloadActionNoUnroll = workloadAction.InvokeNoUnroll, + WorkloadActionUnroll = workloadAction.InvokeUnroll, + Dummy1Action = () => dummy1.InvokeSingle(), + Dummy2Action = () => dummy2.InvokeSingle(), + Dummy3Action = () => dummy3.InvokeSingle(), + OverheadActionNoUnroll = overheadAction.InvokeNoUnroll, + OverheadActionUnroll = overheadAction.InvokeUnroll, GlobalSetupAction = globalSetupAction.InvokeSingle, GlobalCleanupAction = globalCleanupAction.InvokeSingle, IterationSetupAction = iterationSetupAction.InvokeSingle, diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkAction.cs b/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkAction.cs index 2cc2363f49..ca87993536 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkAction.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkAction.cs @@ -1,4 +1,6 @@ -using System; +using Perfolizer.Horology; +using System; +using System.Threading.Tasks; namespace BenchmarkDotNet.Toolchains.InProcess { @@ -6,16 +8,9 @@ namespace BenchmarkDotNet.Toolchains.InProcess [Obsolete("Please use BenchmarkDotNet.Toolchains.InProcess.NoEmit.* classes")] public abstract class BenchmarkAction { - /// Gets or sets invoke single callback. - /// Invoke single callback. - public Action InvokeSingle { get; protected set; } - - /// Gets or sets invoke multiple times callback. - /// Invoke multiple times callback. - public Action InvokeMultiple { get; protected set; } - - /// Gets the last run result. - /// The last run result. + public Func InvokeSingle { get; protected set; } + public Func> InvokeUnroll { get; protected set; } + public Func> InvokeNoUnroll { get; protected set; } public virtual object LastRunResult => null; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory.cs b/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory.cs index 987a4c81b6..250e040786 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory.cs @@ -100,6 +100,21 @@ private static void FallbackMethod() { } private static readonly MethodInfo FallbackSignature = new Action(FallbackMethod).GetMethodInfo(); private static readonly MethodInfo DummyMethod = typeof(DummyInstance).GetMethod(nameof(DummyInstance.Dummy)); + internal static int GetUnrollFactor(BenchmarkCase benchmarkCase) + { + // Only support (Value)Task for async benchmarks. + var methodReturnType = benchmarkCase.Descriptor.WorkloadMethod.ReturnType; + bool isAwaitable = methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask) + || (methodReturnType.GetTypeInfo().IsGenericType + && (methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) + || methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>))); + if (isAwaitable) + { + benchmarkCase.ForceUnrollFactorForAsync(); + } + return benchmarkCase.Job.ResolveValue(Jobs.RunMode.UnrollFactorCharacteristic, Environments.EnvironmentResolver.Instance); + } + /// Creates run benchmark action. /// Descriptor info. /// Instance of target. diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs b/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs index 5ebd1999c5..2c77990b4f 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs @@ -1,5 +1,8 @@ -using System; +using BenchmarkDotNet.Helpers; +using Perfolizer.Horology; +using System; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading.Tasks; namespace BenchmarkDotNet.Toolchains.InProcess @@ -18,30 +21,56 @@ internal class BenchmarkActionVoid : BenchmarkActionBase { private readonly Action callback; private readonly Action unrolledCallback; + private readonly Action emittedUnrolledCallback; public BenchmarkActionVoid(object instance, MethodInfo method, BenchmarkActionCodegen codegenMode, int unrollFactor) { callback = CreateWorkloadOrOverhead(instance, method, OverheadStatic, OverheadInstance); - InvokeSingle = callback; + InvokeSingle = InvokeSingleHardcoded; if (UseFallbackCode(codegenMode, unrollFactor)) { unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + InvokeUnroll = InvokeUnrollHardcoded; } else { - InvokeMultiple = EmitInvokeMultiple(this, nameof(callback), null, unrollFactor); + emittedUnrolledCallback = EmitInvokeMultiple(this, nameof(callback), null, unrollFactor); + InvokeUnroll = InvokeEmittedUnrollHardcoded; } + InvokeNoUnroll = InvokeNoUnrollHardcoded; } private static void OverheadStatic() { } private void OverheadInstance() { } - private void InvokeMultipleHardcoded(long repeatCount) + private ValueTask InvokeSingleHardcoded() { + callback(); + return new ValueTask(); + } + + private ValueTask InvokeUnrollHardcoded(long repeatCount, IClock clock) + { + var startedClock = clock.Start(); for (long i = 0; i < repeatCount; i++) unrolledCallback(); + return new ValueTask(startedClock.GetElapsed()); + } + + private ValueTask InvokeEmittedUnrollHardcoded(long repeatCount, IClock clock) + { + var startedClock = clock.Start(); + emittedUnrolledCallback(repeatCount); + return new ValueTask(startedClock.GetElapsed()); + } + + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + { + var startedClock = clock.Start(); + for (long i = 0; i < repeatCount; i++) + callback(); + return new ValueTask(startedClock.GetElapsed()); } } @@ -49,6 +78,7 @@ internal class BenchmarkAction : BenchmarkActionBase { private readonly Func callback; private readonly Func unrolledCallback; + private readonly Action emittedUnrolledCallback; private T result; public BenchmarkAction(object instance, MethodInfo method, BenchmarkActionCodegen codegenMode, int unrollFactor) @@ -59,23 +89,46 @@ public BenchmarkAction(object instance, MethodInfo method, BenchmarkActionCodege if (UseFallbackCode(codegenMode, unrollFactor)) { unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + InvokeUnroll = InvokeUnrollHardcoded; } else { - InvokeMultiple = EmitInvokeMultiple(this, nameof(callback), nameof(result), unrollFactor); + emittedUnrolledCallback = EmitInvokeMultiple(this, nameof(callback), nameof(result), unrollFactor); + InvokeUnroll = InvokeEmittedUnrollHardcoded; } + InvokeNoUnroll = InvokeNoUnrollHardcoded; } private static T OverheadStatic() => default; private T OverheadInstance() => default; - private void InvokeSingleHardcoded() => result = callback(); + private ValueTask InvokeSingleHardcoded() + { + result = callback(); + return new ValueTask(); + } - private void InvokeMultipleHardcoded(long repeatCount) + private ValueTask InvokeUnrollHardcoded(long repeatCount, IClock clock) { + var startedClock = clock.Start(); for (long i = 0; i < repeatCount; i++) result = unrolledCallback(); + return new ValueTask(startedClock.GetElapsed()); + } + + private ValueTask InvokeEmittedUnrollHardcoded(long repeatCount, IClock clock) + { + var startedClock = clock.Start(); + emittedUnrolledCallback(repeatCount); + return new ValueTask(startedClock.GetElapsed()); + } + + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + { + var startedClock = clock.Start(); + for (long i = 0; i < repeatCount; i++) + callback(); + return new ValueTask(startedClock.GetElapsed()); } public override object LastRunResult => result; @@ -83,93 +136,234 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionTask : BenchmarkActionBase { - private readonly Func startTaskCallback; - private readonly Action callback; - private readonly Action unrolledCallback; + private readonly Func callback; + private readonly AutoResetValueTaskSource valueTaskSource = new AutoResetValueTaskSource(); + private long repeatsRemaining; + private readonly Action continuation; + private StartedClock startedClock; + private TaskAwaiter currentAwaiter; public BenchmarkActionTask(object instance, MethodInfo method, BenchmarkActionCodegen codegenMode, int unrollFactor) { + continuation = Continuation; bool isIdle = method == null; if (!isIdle) { - startTaskCallback = CreateWorkload>(instance, method); - callback = ExecuteBlocking; + callback = CreateWorkload>(instance, method); + InvokeSingle = InvokeSingleHardcoded; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; } else { callback = Overhead; + InvokeSingle = InvokeSingleHardcodedOverhead; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcodedOverhead; } + } - InvokeSingle = callback; + private Task Overhead() => default; - if (UseFallbackCode(codegenMode, unrollFactor)) + private ValueTask InvokeSingleHardcodedOverhead() + { + callback(); + return new ValueTask(); + } + + private ValueTask InvokeNoUnrollHardcodedOverhead(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + Task value = default; + startedClock = clock.Start(); + try { - unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + while (--repeatsRemaining >= 0) + { + value = callback(); + } } - else + catch (Exception) { - InvokeMultiple = EmitInvokeMultiple(this, nameof(callback), null, unrollFactor); + Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + throw; } + return new ValueTask(startedClock.GetElapsed()); } - // must be kept in sync with VoidDeclarationsProvider.IdleImplementation - private void Overhead() { } + private ValueTask InvokeSingleHardcoded() + { + return AwaitHelper.ToValueTaskVoid(callback()); + } - // must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate - private void ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + startedClock = clock.Start(); + RunTask(); + return new ValueTask(valueTaskSource, valueTaskSource.Version); + } - private void InvokeMultipleHardcoded(long repeatCount) + private void RunTask() { - for (long i = 0; i < repeatCount; i++) - unrolledCallback(); + try + { + while (--repeatsRemaining >= 0) + { + currentAwaiter = callback().GetAwaiter(); + if (!currentAwaiter.IsCompleted) + { + currentAwaiter.UnsafeOnCompleted(continuation); + return; + } + currentAwaiter.GetResult(); + } + } + catch (Exception e) + { + SetException(e); + return; + } + var clockspan = startedClock.GetElapsed(); + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetResult(clockspan); + } + + private void Continuation() + { + try + { + currentAwaiter.GetResult(); + } + catch (Exception e) + { + SetException(e); + return; + } + RunTask(); + } + + private void SetException(Exception e) + { + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetException(e); } } internal class BenchmarkActionTask : BenchmarkActionBase { - private readonly Func> startTaskCallback; - private readonly Func callback; - private readonly Func unrolledCallback; + private readonly Func> callback; + private readonly AutoResetValueTaskSource valueTaskSource = new AutoResetValueTaskSource(); + private long repeatsRemaining; + private readonly Action continuation; + private StartedClock startedClock; + private TaskAwaiter currentAwaiter; private T result; public BenchmarkActionTask(object instance, MethodInfo method, BenchmarkActionCodegen codegenMode, int unrollFactor) { - bool isOverhead = method == null; - if (!isOverhead) + continuation = Continuation; + bool isIdle = method == null; + if (!isIdle) { - startTaskCallback = CreateWorkload>>(instance, method); - callback = ExecuteBlocking; + callback = CreateWorkload>>(instance, method); + InvokeSingle = InvokeSingleHardcoded; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; } else { callback = Overhead; + InvokeSingle = InvokeSingleHardcodedOverhead; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcodedOverhead; } + } - InvokeSingle = InvokeSingleHardcoded; + private Task Overhead() => default; - if (UseFallbackCode(codegenMode, unrollFactor)) + private ValueTask InvokeSingleHardcodedOverhead() + { + callback(); + return new ValueTask(); + } + + private ValueTask InvokeNoUnrollHardcodedOverhead(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + Task value = default; + startedClock = clock.Start(); + try { - unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + while (--repeatsRemaining >= 0) + { + value = callback(); + } } - else + catch (Exception) { - InvokeMultiple = EmitInvokeMultiple(this, nameof(callback), nameof(result), unrollFactor); + Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + throw; } + return new ValueTask(startedClock.GetElapsed()); } - private T Overhead() => default; + private ValueTask InvokeSingleHardcoded() + { + return AwaitHelper.ToValueTaskVoid(callback()); + } + + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + startedClock = clock.Start(); + RunTask(); + return new ValueTask(valueTaskSource, valueTaskSource.Version); + } - // must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate - private T ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback()); + private void RunTask() + { + try + { + while (--repeatsRemaining >= 0) + { + currentAwaiter = callback().GetAwaiter(); + if (!currentAwaiter.IsCompleted) + { + currentAwaiter.UnsafeOnCompleted(continuation); + return; + } + result = currentAwaiter.GetResult(); + } + } + catch (Exception e) + { + SetException(e); + return; + } + var clockspan = startedClock.GetElapsed(); + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetResult(clockspan); + } - private void InvokeSingleHardcoded() => result = callback(); + private void Continuation() + { + try + { + result = currentAwaiter.GetResult(); + } + catch (Exception e) + { + SetException(e); + return; + } + RunTask(); + } - private void InvokeMultipleHardcoded(long repeatCount) + private void SetException(Exception e) { - for (long i = 0; i < repeatCount; i++) - result = unrolledCallback(); + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetException(e); } public override object LastRunResult => result; @@ -177,93 +371,234 @@ private void InvokeMultipleHardcoded(long repeatCount) internal class BenchmarkActionValueTask : BenchmarkActionBase { - private readonly Func startTaskCallback; - private readonly Action callback; - private readonly Action unrolledCallback; + private readonly Func callback; + private readonly AutoResetValueTaskSource valueTaskSource = new AutoResetValueTaskSource(); + private long repeatsRemaining; + private readonly Action continuation; + private StartedClock startedClock; + private ValueTaskAwaiter currentAwaiter; public BenchmarkActionValueTask(object instance, MethodInfo method, BenchmarkActionCodegen codegenMode, int unrollFactor) { + continuation = Continuation; bool isIdle = method == null; if (!isIdle) { - startTaskCallback = CreateWorkload>(instance, method); - callback = ExecuteBlocking; + callback = CreateWorkload>(instance, method); + InvokeSingle = InvokeSingleHardcoded; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; } else { callback = Overhead; + InvokeSingle = InvokeSingleHardcodedOverhead; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcodedOverhead; } + } - InvokeSingle = callback; + private ValueTask Overhead() => default; - if (UseFallbackCode(codegenMode, unrollFactor)) + private ValueTask InvokeSingleHardcodedOverhead() + { + callback(); + return new ValueTask(); + } + + private ValueTask InvokeNoUnrollHardcodedOverhead(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + ValueTask value = default; + startedClock = clock.Start(); + try { - unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + while (--repeatsRemaining >= 0) + { + value = callback(); + } } - else + catch (Exception) { - InvokeMultiple = EmitInvokeMultiple(this, nameof(callback), null, unrollFactor); + Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + throw; } + return new ValueTask(startedClock.GetElapsed()); } - // must be kept in sync with VoidDeclarationsProvider.IdleImplementation - private void Overhead() { } + private ValueTask InvokeSingleHardcoded() + { + return AwaitHelper.ToValueTaskVoid(callback()); + } - // must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate - private void ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + startedClock = clock.Start(); + RunTask(); + return new ValueTask(valueTaskSource, valueTaskSource.Version); + } - private void InvokeMultipleHardcoded(long repeatCount) + private void RunTask() { - for (long i = 0; i < repeatCount; i++) - unrolledCallback(); + try + { + while (--repeatsRemaining >= 0) + { + currentAwaiter = callback().GetAwaiter(); + if (!currentAwaiter.IsCompleted) + { + currentAwaiter.UnsafeOnCompleted(continuation); + return; + } + currentAwaiter.GetResult(); + } + } + catch (Exception e) + { + SetException(e); + return; + } + var clockspan = startedClock.GetElapsed(); + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetResult(clockspan); + } + + private void Continuation() + { + try + { + currentAwaiter.GetResult(); + } + catch (Exception e) + { + SetException(e); + return; + } + RunTask(); + } + + private void SetException(Exception e) + { + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetException(e); } } internal class BenchmarkActionValueTask : BenchmarkActionBase { - private readonly Func> startTaskCallback; - private readonly Func callback; - private readonly Func unrolledCallback; + private readonly Func> callback; + private readonly AutoResetValueTaskSource valueTaskSource = new AutoResetValueTaskSource(); + private long repeatsRemaining; + private readonly Action continuation; + private StartedClock startedClock; + private ValueTaskAwaiter currentAwaiter; private T result; public BenchmarkActionValueTask(object instance, MethodInfo method, BenchmarkActionCodegen codegenMode, int unrollFactor) { - bool isOverhead = method == null; - if (!isOverhead) + continuation = Continuation; + bool isIdle = method == null; + if (!isIdle) { - startTaskCallback = CreateWorkload>>(instance, method); - callback = ExecuteBlocking; + callback = CreateWorkload>>(instance, method); + InvokeSingle = InvokeSingleHardcoded; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; } else { callback = Overhead; + InvokeSingle = InvokeSingleHardcodedOverhead; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcodedOverhead; } + } - InvokeSingle = InvokeSingleHardcoded; + private ValueTask Overhead() => default; - if (UseFallbackCode(codegenMode, unrollFactor)) + private ValueTask InvokeSingleHardcodedOverhead() + { + callback(); + return new ValueTask(); + } + + private ValueTask InvokeNoUnrollHardcodedOverhead(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + ValueTask value = default; + startedClock = clock.Start(); + try { - unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + while (--repeatsRemaining >= 0) + { + value = callback(); + } } - else + catch (Exception) { - InvokeMultiple = EmitInvokeMultiple(this, nameof(callback), nameof(result), unrollFactor); + Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + throw; } + return new ValueTask(startedClock.GetElapsed()); + } + + private ValueTask InvokeSingleHardcoded() + { + return AwaitHelper.ToValueTaskVoid(callback()); } - private T Overhead() => default; + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + startedClock = clock.Start(); + RunTask(); + return new ValueTask(valueTaskSource, valueTaskSource.Version); + } - // must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate - private T ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback()); + private void RunTask() + { + try + { + while (--repeatsRemaining >= 0) + { + currentAwaiter = callback().GetAwaiter(); + if (!currentAwaiter.IsCompleted) + { + currentAwaiter.UnsafeOnCompleted(continuation); + return; + } + result = currentAwaiter.GetResult(); + } + } + catch (Exception e) + { + SetException(e); + return; + } + var clockspan = startedClock.GetElapsed(); + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetResult(clockspan); + } - private void InvokeSingleHardcoded() => result = callback(); + private void Continuation() + { + try + { + result = currentAwaiter.GetResult(); + } + catch (Exception e) + { + SetException(e); + return; + } + RunTask(); + } - private void InvokeMultipleHardcoded(long repeatCount) + private void SetException(Exception e) { - for (long i = 0; i < repeatCount; i++) - result = unrolledCallback(); + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetException(e); } public override object LastRunResult => result; diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/InProcessRunner.cs b/src/BenchmarkDotNet/Toolchains/InProcess/InProcessRunner.cs index aedd8f6788..2c72dd9aaf 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/InProcessRunner.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/InProcessRunner.cs @@ -101,9 +101,9 @@ private static class Runnable { public static void RunCore(IHost host, BenchmarkCase benchmarkCase, BenchmarkActionCodegen codegenMode) { + int unrollFactor = BenchmarkActionFactory.GetUnrollFactor(benchmarkCase); var target = benchmarkCase.Descriptor; var job = benchmarkCase.Job; // TODO: filter job (same as SourceCodePresenter does)? - int unrollFactor = benchmarkCase.Job.ResolveValue(RunMode.UnrollFactorCharacteristic, EnvironmentResolver.Instance); // DONTTOUCH: these should be allocated together var instance = Activator.CreateInstance(benchmarkCase.Descriptor.Type); @@ -128,21 +128,13 @@ public static void RunCore(IHost host, BenchmarkCase benchmarkCase, BenchmarkAct var engineParameters = new EngineParameters { Host = host, - WorkloadActionNoUnroll = invocationCount => - { - for (int i = 0; i < invocationCount; i++) - workloadAction.InvokeSingle(); - }, - WorkloadActionUnroll = workloadAction.InvokeMultiple, - Dummy1Action = dummy1.InvokeSingle, - Dummy2Action = dummy2.InvokeSingle, - Dummy3Action = dummy3.InvokeSingle, - OverheadActionNoUnroll = invocationCount => - { - for (int i = 0; i < invocationCount; i++) - overheadAction.InvokeSingle(); - }, - OverheadActionUnroll = overheadAction.InvokeMultiple, + WorkloadActionNoUnroll = workloadAction.InvokeNoUnroll, + WorkloadActionUnroll = workloadAction.InvokeUnroll, + Dummy1Action = () => dummy1.InvokeSingle(), + Dummy2Action = () => dummy2.InvokeSingle(), + Dummy3Action = () => dummy3.InvokeSingle(), + OverheadActionNoUnroll = overheadAction.InvokeNoUnroll, + OverheadActionUnroll = overheadAction.InvokeUnroll, GlobalSetupAction = globalSetupAction.InvokeSingle, GlobalCleanupAction = globalCleanupAction.InvokeSingle, IterationSetupAction = iterationSetupAction.InvokeSingle, diff --git a/src/BenchmarkDotNet/Toolchains/Roslyn/Generator.cs b/src/BenchmarkDotNet/Toolchains/Roslyn/Generator.cs index ef11c17d91..aee6d271ae 100644 --- a/src/BenchmarkDotNet/Toolchains/Roslyn/Generator.cs +++ b/src/BenchmarkDotNet/Toolchains/Roslyn/Generator.cs @@ -55,8 +55,10 @@ internal static IEnumerable GetAllReferences(BenchmarkCase benchmarkCa .Concat( new[] { - benchmarkCase.Descriptor.Type.GetTypeInfo().Assembly, // this assembly does not has to have a reference to BenchmarkDotNet (e.g. custom framework for benchmarking that internally uses BenchmarkDotNet - typeof(BenchmarkCase).Assembly // BenchmarkDotNet + benchmarkCase.Descriptor.Type.GetTypeInfo().Assembly, // this assembly does not have to have a reference to BenchmarkDotNet (e.g. custom framework for benchmarking that internally uses BenchmarkDotNet + typeof(BenchmarkCase).Assembly, // BenchmarkDotNet + typeof(System.Threading.Tasks.ValueTask).Assembly, // TaskExtensions + typeof(Perfolizer.Horology.IClock).Assembly // Perfolizer }) .Distinct(); } diff --git a/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs b/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs index 314e183d2b..afb609ddf5 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs @@ -97,10 +97,10 @@ public class AllSetupAndCleanupAttributeBenchmarksTask private int cleanupCounter; [IterationSetup] - public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")"); + public Task IterationSetup() => Console.Out.WriteLineAsync(IterationSetupCalled + " (" + ++setupCounter + ")"); [IterationCleanup] - public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + public Task IterationCleanup() => Console.Out.WriteLineAsync(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); [GlobalSetup] public Task GlobalSetup() => Console.Out.WriteLineAsync(GlobalSetupCalled); @@ -118,10 +118,20 @@ public class AllSetupAndCleanupAttributeBenchmarksGenericTask private int cleanupCounter; [IterationSetup] - public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")"); + public async Task IterationSetup() + { + await Console.Out.WriteLineAsync(IterationSetupCalled + " (" + ++setupCounter + ")"); + + return 42; + } [IterationCleanup] - public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + public async Task IterationCleanup() + { + await Console.Out.WriteLineAsync(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + + return 42; + } [GlobalSetup] public async Task GlobalSetup() @@ -149,10 +159,10 @@ public class AllSetupAndCleanupAttributeBenchmarksValueTask private int cleanupCounter; [IterationSetup] - public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")"); + public ValueTask IterationSetup() => new ValueTask(Console.Out.WriteLineAsync(IterationSetupCalled + " (" + ++setupCounter + ")")); [IterationCleanup] - public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + public ValueTask IterationCleanup() => new ValueTask(Console.Out.WriteLineAsync(IterationCleanupCalled + " (" + ++cleanupCounter + ")")); [GlobalSetup] public ValueTask GlobalSetup() => new ValueTask(Console.Out.WriteLineAsync(GlobalSetupCalled)); @@ -170,10 +180,20 @@ public class AllSetupAndCleanupAttributeBenchmarksGenericValueTask private int cleanupCounter; [IterationSetup] - public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")"); + public async ValueTask IterationSetup() + { + await Console.Out.WriteLineAsync(IterationSetupCalled + " (" + ++setupCounter + ")"); + + return 42; + } [IterationCleanup] - public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + public async ValueTask IterationCleanup() + { + await Console.Out.WriteLineAsync(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + + return 42; + } [GlobalSetup] public async ValueTask GlobalSetup() @@ -202,10 +222,20 @@ public class AllSetupAndCleanupAttributeBenchmarksValueTaskSource private int cleanupCounter; [IterationSetup] - public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")"); + public ValueTask IterationSetup() + { + valueTaskSource.Reset(); + Console.Out.WriteLineAsync(IterationSetupCalled + " (" + ++setupCounter + ")").ContinueWith(_ => valueTaskSource.SetResult(42)); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } [IterationCleanup] - public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + public ValueTask IterationCleanup() + { + valueTaskSource.Reset(); + Console.Out.WriteLineAsync(IterationCleanupCalled + " (" + ++cleanupCounter + ")").ContinueWith(_ => valueTaskSource.SetResult(42)); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } [GlobalSetup] public ValueTask GlobalSetup() @@ -234,10 +264,20 @@ public class AllSetupAndCleanupAttributeBenchmarksGenericValueTaskSource private int cleanupCounter; [IterationSetup] - public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")"); + public ValueTask IterationSetup() + { + valueTaskSource.Reset(); + Console.Out.WriteLineAsync(IterationSetupCalled + " (" + ++setupCounter + ")").ContinueWith(_ => valueTaskSource.SetResult(42)); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } [IterationCleanup] - public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + public ValueTask IterationCleanup() + { + valueTaskSource.Reset(); + Console.Out.WriteLineAsync(IterationCleanupCalled + " (" + ++cleanupCounter + ")").ContinueWith(_ => valueTaskSource.SetResult(42)); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } [GlobalSetup] public ValueTask GlobalSetup() diff --git a/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs b/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs index 29707c917f..89b16b729c 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs @@ -9,6 +9,8 @@ using BenchmarkDotNet.Reports; using BenchmarkDotNet.Characteristics; using Perfolizer.Mathematics.OutlierDetection; +using System.Threading.Tasks; +using Perfolizer.Horology; namespace BenchmarkDotNet.IntegrationTests { @@ -86,10 +88,10 @@ public void WriteLine() { } public void WriteLine(string line) { } public Job TargetJob { get; } public long OperationsPerInvoke { get; } - public Action GlobalSetupAction { get; set; } - public Action GlobalCleanupAction { get; set; } - public Action WorkloadAction { get; } - public Action OverheadAction { get; } + public Func GlobalSetupAction { get; set; } + public Func GlobalCleanupAction { get; set; } + public Func> WorkloadAction { get; } + public Func> OverheadAction { get; } public IResolver Resolver { get; } public Measurement RunIteration(IterationData data) { throw new NotImplementedException(); } diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs index 65cd9c752e..1cc4f8b033 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Collections.Generic; @@ -31,7 +32,9 @@ public class NaiveRunnableEmitDiff { { OpCodes.Br_S, OpCodes.Br }, { OpCodes.Blt_S, OpCodes.Blt }, - { OpCodes.Bne_Un_S, OpCodes.Bne_Un } + { OpCodes.Bne_Un_S, OpCodes.Bne_Un }, + { OpCodes.Bge_S, OpCodes.Bge }, + { OpCodes.Brtrue_S, OpCodes.Brtrue }, }; public static void RunDiff(string roslynAssemblyPath, string emittedAssemblyPath, ILogger logger) @@ -63,7 +66,15 @@ private static bool AreSameTypeIgnoreNested(TypeReference left, TypeReference ri private static bool AreSameSignature(MethodReference left, MethodReference right) { - return (left.Name == right.Name || (left.Name.StartsWith("<.ctor>") && right.Name == "__Workload")) + var lookup = new HashSet() + { + RunnableConstants.WorkloadImplementationMethodName, + RunnableConstants.GlobalSetupMethodName, + RunnableConstants.GlobalCleanupMethodName, + RunnableConstants.IterationSetupMethodName, + RunnableConstants.IterationCleanupMethodName + }; + return (left.Name == right.Name || (left.Name.StartsWith("<.ctor>") && lookup.Contains(right.Name))) && AreSameTypeIgnoreNested(left.ReturnType, right.ReturnType) && left.Parameters.Count == right.Parameters.Count && left.Parameters @@ -80,7 +91,9 @@ private static List GetOpInstructions(MethodDefinition method) var result = new List(bodyInstructions.Count); foreach (var instruction in bodyInstructions) { - if (compareNops || instruction.OpCode != OpCodes.Nop) + // Skip leave instructions since the IlBuilder forces them differently than Roslyn. + if (instruction.OpCode != OpCodes.Leave && instruction.OpCode != OpCodes.Leave_S + && (compareNops || instruction.OpCode != OpCodes.Nop)) result.Add(instruction); } @@ -289,14 +302,17 @@ private static void DiffMembers(TypeDefinition type1, TypeDefinition type2, ILog } var methods2ByName = type2.Methods.ToLookup(f => f.Name); + var methods2ByComparison = new HashSet(type2.Methods); foreach (var method1 in type1.Methods) { logger.Write($" method {method1.FullName}"); var method2 = methods2ByName[method1.Name].SingleOrDefault(m => AreSameSignature(method1, m)); if (method2 == null) - method2 = type2.Methods.SingleOrDefault(m => AreSameSignature(method1, m)); - if (method2 == null) + method2 = methods2ByComparison.FirstOrDefault(m => AreSameSignature(method1, m)); + if (method2 != null) + methods2ByComparison.Remove(method2); + else method2 = methods2ByName[method1.Name].SingleOrDefault(); if (Diff(method1, method2)) diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs index 8c0782f074..1054e613ab 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs @@ -237,6 +237,10 @@ public static ValueTask InvokeOnceStaticValueTaskOfT() [Theory] [InlineData(typeof(IterationSetupCleanup))] + [InlineData(typeof(IterationSetupCleanupTask))] + [InlineData(typeof(IterationSetupCleanupValueTask))] + [InlineData(typeof(IterationSetupCleanupValueTaskSource))] + [InlineData(typeof(GlobalSetupCleanup))] [InlineData(typeof(GlobalSetupCleanupTask))] [InlineData(typeof(GlobalSetupCleanupValueTask))] [InlineData(typeof(GlobalSetupCleanupValueTaskSource))] @@ -273,6 +277,108 @@ public class IterationSetupCleanup public void Cleanup() => Interlocked.Increment(ref Counters.CleanupCounter); } + public class IterationSetupCleanupTask + { + [IterationSetup] + public static async Task IterationSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.SetupCounter); + } + + [IterationCleanup] + public async Task IterationCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.CleanupCounter); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counters.BenchmarkCounter); + } + } + + public class IterationSetupCleanupValueTask + { + [IterationSetup] + public static async ValueTask IterationSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.SetupCounter); + } + + [IterationCleanup] + public async ValueTask IterationCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.CleanupCounter); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counters.BenchmarkCounter); + } + } + + public class IterationSetupCleanupValueTaskSource + { + private readonly static ValueTaskSource valueTaskSource = new (); + + [IterationSetup] + public static ValueTask IterationSetup() + { + valueTaskSource.Reset(); + Task.Delay(1).ContinueWith(_ => + { + Interlocked.Increment(ref Counters.SetupCounter); + valueTaskSource.SetResult(42); + }); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } + + [IterationCleanup] + public ValueTask IterationCleanup() + { + valueTaskSource.Reset(); + Task.Delay(1).ContinueWith(_ => + { + Interlocked.Increment(ref Counters.CleanupCounter); + valueTaskSource.SetResult(42); + }); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counters.BenchmarkCounter); + } + } + + public class GlobalSetupCleanup + { + [GlobalSetup] + public static void GlobalSetup() + { + Interlocked.Increment(ref Counters.SetupCounter); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + Interlocked.Increment(ref Counters.CleanupCounter); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counters.BenchmarkCounter); + } + } + public class GlobalSetupCleanupTask { [GlobalSetup] diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs index 544b847304..044985d35e 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs @@ -12,8 +12,10 @@ using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Running; using BenchmarkDotNet.Tests.Loggers; +using BenchmarkDotNet.Tests.Mocks; using BenchmarkDotNet.Toolchains.InProcess; using JetBrains.Annotations; +using Perfolizer.Horology; using Xunit; using Xunit.Abstractions; @@ -31,6 +33,7 @@ public InProcessTest(ITestOutputHelper output) : base(output) private const string StringResult = "42"; private const int UnrollFactor = 16; + private readonly IClock clock = new MockClock(TimeInterval.Millisecond.ToFrequency()); [Fact] public void BenchmarkActionGlobalSetupSupported() => TestInvoke(x => BenchmarkAllCases.GlobalSetup(), UnrollFactor); @@ -60,16 +63,28 @@ public InProcessTest(ITestOutputHelper output) : base(output) public void BenchmarkActionValueTaskOfTSupported() => TestInvoke(x => x.InvokeOnceValueTaskOfT(), UnrollFactor, DecimalResult); [Fact] - public void BenchmarkActionGlobalSetupTaskSupported() => TestInvokeSetupCleanupTask(x => BenchmarkSetupCleanupTask.GlobalSetup(), UnrollFactor); + public void BenchmarkActionGlobalSetupTaskSupported() => TestInvokeSetupCleanupTask(x => BenchmarkSetupCleanupTask.GlobalSetup()); [Fact] - public void BenchmarkActionGlobalCleanupTaskSupported() => TestInvokeSetupCleanupTask(x => x.GlobalCleanup(), UnrollFactor); + public void BenchmarkActionGlobalCleanupTaskSupported() => TestInvokeSetupCleanupTask(x => x.GlobalCleanup()); [Fact] - public void BenchmarkActionGlobalSetupValueTaskSupported() => TestInvokeSetupCleanupValueTask(x => BenchmarkSetupCleanupValueTask.GlobalSetup(), UnrollFactor); + public void BenchmarkActionIterationSetupTaskSupported() => TestInvokeSetupCleanupTask(x => BenchmarkSetupCleanupTask.GlobalSetup()); [Fact] - public void BenchmarkActionGlobalCleanupValueTaskSupported() => TestInvokeSetupCleanupValueTask(x => x.GlobalCleanup(), UnrollFactor); + public void BenchmarkActionIterationCleanupTaskSupported() => TestInvokeSetupCleanupTask(x => x.GlobalCleanup()); + + [Fact] + public void BenchmarkActionGlobalSetupValueTaskSupported() => TestInvokeSetupCleanupValueTask(x => BenchmarkSetupCleanupValueTask.GlobalSetup()); + + [Fact] + public void BenchmarkActionGlobalCleanupValueTaskSupported() => TestInvokeSetupCleanupValueTask(x => x.GlobalCleanup()); + + [Fact] + public void BenchmarkActionIterationSetupValueTaskSupported() => TestInvokeSetupCleanupValueTask(x => BenchmarkSetupCleanupValueTask.GlobalSetup()); + + [Fact] + public void BenchmarkActionIterationCleanupValueTaskSupported() => TestInvokeSetupCleanupValueTask(x => x.GlobalCleanup()); [AssertionMethod] private void TestInvoke(Expression> methodCall, int unrollFactor) @@ -115,6 +130,16 @@ private void TestInvoke(Expression> methodCall, in var targetMethod = ((MethodCallExpression) methodCall.Body).Method; var descriptor = new Descriptor(typeof(BenchmarkAllCases), targetMethod); + var methodReturnType = typeof(T); + bool isAwaitable = methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask) + || (methodReturnType.GetTypeInfo().IsGenericType + && (methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) + || methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>))); + if (isAwaitable) + { + unrollFactor = 1; + } + // Run mode var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkAllCases(), BenchmarkActionCodegen.ReflectionEmit, unrollFactor); TestInvoke(action, unrollFactor, false, expectedResult, ref BenchmarkAllCases.Counter); @@ -123,14 +148,14 @@ private void TestInvoke(Expression> methodCall, in // Idle mode - bool isValueTask = typeof(T).IsConstructedGenericType && typeof(T).GetGenericTypeDefinition() == typeof(ValueTask<>); + bool isValueTask = methodReturnType.IsConstructedGenericType && methodReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>); object idleExpected; if (isValueTask) - idleExpected = GetDefault(typeof(T).GetGenericArguments()[0]); - else if (expectedResult == null || typeof(T) == typeof(Task) || typeof(T) == typeof(ValueTask)) + idleExpected = GetDefault(methodReturnType.GetGenericArguments()[0]); + else if (expectedResult == null || methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask)) idleExpected = null; - else if (typeof(T).GetTypeInfo().IsValueType) + else if (methodReturnType.GetTypeInfo().IsValueType) idleExpected = 0; else idleExpected = GetDefault(expectedResult.GetType()); @@ -142,10 +167,11 @@ private void TestInvoke(Expression> methodCall, in } [AssertionMethod] - private void TestInvokeSetupCleanupTask(Expression> methodCall, int unrollFactor) + private void TestInvokeSetupCleanupTask(Expression> methodCall) { var targetMethod = ((MethodCallExpression) methodCall.Body).Method; var descriptor = new Descriptor(typeof(BenchmarkSetupCleanupTask), targetMethod, targetMethod, targetMethod, targetMethod, targetMethod); + int unrollFactor = 1; // Run mode var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkSetupCleanupTask(), BenchmarkActionCodegen.ReflectionEmit, unrollFactor); @@ -180,10 +206,11 @@ private void TestInvokeSetupCleanupTask(Expression> methodCall, int unrollFactor) + private void TestInvokeSetupCleanupValueTask(Expression> methodCall) { var targetMethod = ((MethodCallExpression) methodCall.Body).Method; var descriptor = new Descriptor(typeof(BenchmarkSetupCleanupValueTask), targetMethod, targetMethod, targetMethod, targetMethod, targetMethod); + int unrollFactor = 1; // Run mode var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkSetupCleanupValueTask(), BenchmarkActionCodegen.ReflectionEmit, unrollFactor); @@ -235,20 +262,20 @@ private void TestInvoke(BenchmarkAction benchmarkAction, int unrollFactor, bool if (isIdle) { - benchmarkAction.InvokeSingle(); + Helpers.AwaitHelper.GetResult(benchmarkAction.InvokeSingle()); Assert.Equal(0, counter); - benchmarkAction.InvokeMultiple(0); + Helpers.AwaitHelper.GetResult(benchmarkAction.InvokeUnroll(0, clock)); Assert.Equal(0, counter); - benchmarkAction.InvokeMultiple(11); + Helpers.AwaitHelper.GetResult(benchmarkAction.InvokeUnroll(11, clock)); Assert.Equal(0, counter); } else { - benchmarkAction.InvokeSingle(); + Helpers.AwaitHelper.GetResult(benchmarkAction.InvokeSingle()); Assert.Equal(1, counter); - benchmarkAction.InvokeMultiple(0); + Helpers.AwaitHelper.GetResult(benchmarkAction.InvokeUnroll(0, clock)); Assert.Equal(1, counter); - benchmarkAction.InvokeMultiple(11); + Helpers.AwaitHelper.GetResult(benchmarkAction.InvokeUnroll(11, clock)); Assert.Equal(1 + unrollFactor * 11, counter); } @@ -403,6 +430,20 @@ public async Task GlobalCleanup() Interlocked.Increment(ref Counter); } + [IterationSetup] + public static async Task IterationSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + + [IterationCleanup] + public async Task IterationCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + [Benchmark] public void InvokeOnceVoid() { @@ -429,6 +470,20 @@ public async ValueTask GlobalCleanup() Interlocked.Increment(ref Counter); } + [IterationSetup] + public static async ValueTask IterationSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + + [IterationCleanup] + public async ValueTask IterationCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + [Benchmark] public void InvokeOnceVoid() { diff --git a/tests/BenchmarkDotNet.Tests/Engine/EngineFactoryTests.cs b/tests/BenchmarkDotNet.Tests/Engine/EngineFactoryTests.cs index 19a0e0583f..e3845613d1 100644 --- a/tests/BenchmarkDotNet.Tests/Engine/EngineFactoryTests.cs +++ b/tests/BenchmarkDotNet.Tests/Engine/EngineFactoryTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; +using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Jobs; @@ -21,32 +22,72 @@ public class EngineFactoryTests private IResolver DefaultResolver => BenchmarkRunnerClean.DefaultResolver; - private void GlobalSetup() => timesGlobalSetupCalled++; - private void IterationSetup() => timesIterationSetupCalled++; - private void IterationCleanup() => timesIterationCleanupCalled++; - private void GlobalCleanup() => timesGlobalCleanupCalled++; + private ValueTask GlobalSetup() + { + timesGlobalSetupCalled++; + return new ValueTask(); + } + private ValueTask IterationSetup() + { + timesIterationSetupCalled++; + return new ValueTask(); + } + private ValueTask IterationCleanup() + { + timesIterationCleanupCalled++; + return new ValueTask(); + } + private ValueTask GlobalCleanup() + { + timesGlobalCleanupCalled++; + return new ValueTask(); + } - private void Throwing(long _) => throw new InvalidOperationException("must NOT be called"); + private ValueTask Throwing(long _, IClock __) => throw new InvalidOperationException("must NOT be called"); - private void VeryTimeConsumingSingle(long _) + private ValueTask VeryTimeConsumingSingle(long _, IClock clock) { + var startedClock = clock.Start(); timesBenchmarkCalled++; Thread.Sleep(IterationTime); + return new ValueTask(startedClock.GetElapsed()); } - private void TimeConsumingOnlyForTheFirstCall(long _) + private ValueTask TimeConsumingOnlyForTheFirstCall(long _, IClock clock) { + var startedClock = clock.Start(); if (timesBenchmarkCalled++ == 0) { Thread.Sleep(IterationTime); } + return new ValueTask(startedClock.GetElapsed()); } - private void InstantNoUnroll(long invocationCount) => timesBenchmarkCalled += (int) invocationCount; - private void InstantUnroll(long _) => timesBenchmarkCalled += 16; + private ValueTask InstantNoUnroll(long invocationCount, IClock clock) + { + var startedClock = clock.Start(); + timesBenchmarkCalled += (int) invocationCount; + return new ValueTask(startedClock.GetElapsed()); + } + private ValueTask InstantUnroll(long _, IClock clock) + { + var startedClock = clock.Start(); + timesBenchmarkCalled += 16; + return new ValueTask(startedClock.GetElapsed()); + } - private void OverheadNoUnroll(long invocationCount) => timesOverheadCalled += (int) invocationCount; - private void OverheadUnroll(long _) => timesOverheadCalled += 16; + private ValueTask OverheadNoUnroll(long invocationCount, IClock clock) + { + var startedClock = clock.Start(); + timesOverheadCalled += (int) invocationCount; + return new ValueTask(startedClock.GetElapsed()); + } + private ValueTask OverheadUnroll(long _, IClock clock) + { + var startedClock = clock.Start(); + timesOverheadCalled += 16; + return new ValueTask(startedClock.GetElapsed()); + } private static readonly Dictionary JobsWhichDontRequireJitting = new Dictionary { @@ -197,22 +238,26 @@ public void MediumTimeConsumingBenchmarksShouldStartPilotFrom2AndIncrementItWith var mediumTime = TimeSpan.FromMilliseconds(IterationTime.TotalMilliseconds / times); - void MediumNoUnroll(long invocationCount) + ValueTask MediumNoUnroll(long invocationCount, IClock clock) { + var startedClock = clock.Start(); for (int i = 0; i < invocationCount; i++) { timesBenchmarkCalled++; Thread.Sleep(mediumTime); } + return new ValueTask(startedClock.GetElapsed()); } - void MediumUnroll(long _) + ValueTask MediumUnroll(long _, IClock clock) { + var startedClock = clock.Start(); timesBenchmarkCalled += unrollFactor; for (int i = 0; i < unrollFactor; i++) // the real unroll factor obviously does not use loop ;) Thread.Sleep(mediumTime); + return new ValueTask(startedClock.GetElapsed()); } var engineParameters = CreateEngineParameters(mainNoUnroll: MediumNoUnroll, mainUnroll: MediumUnroll, job: Job.Default); @@ -239,7 +284,7 @@ void MediumUnroll(long _) Assert.Equal(1, timesGlobalCleanupCalled); } - private EngineParameters CreateEngineParameters(Action mainNoUnroll, Action mainUnroll, Job job) + private EngineParameters CreateEngineParameters(Func> mainNoUnroll, Func> mainUnroll, Job job) => new EngineParameters { Dummy1Action = () => { }, diff --git a/tests/BenchmarkDotNet.Tests/Mocks/MockEngine.cs b/tests/BenchmarkDotNet.Tests/Mocks/MockEngine.cs index 6b1fad64a6..5b784a2691 100644 --- a/tests/BenchmarkDotNet.Tests/Mocks/MockEngine.cs +++ b/tests/BenchmarkDotNet.Tests/Mocks/MockEngine.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Jobs; @@ -32,16 +33,16 @@ public MockEngine(ITestOutputHelper output, Job job, Func GlobalSetupAction { get; set; } [UsedImplicitly] - public Action GlobalCleanupAction { get; set; } + public Func GlobalCleanupAction { get; set; } [UsedImplicitly] public bool IsDiagnoserAttached { get; set; } - public Action WorkloadAction { get; } = _ => { }; - public Action OverheadAction { get; } = _ => { }; + public Func> WorkloadAction { get; } = (invokeCount, clock) => default; + public Func> OverheadAction { get; } = (invokeCount, clock) => default; [UsedImplicitly] public IEngineFactory Factory => null; From 85a6c475b76684ea5893b6084c08b11a0e7a40c8 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 21 Jan 2023 10:33:07 -0500 Subject: [PATCH 07/13] Clean up duplicated code. --- src/BenchmarkDotNet/Helpers/AwaitHelper.cs | 64 +++++++--------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs index 072d903410..12c86bd595 100644 --- a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs +++ b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs @@ -115,62 +115,36 @@ public static async ValueTask ToValueTaskVoid(ValueTask task) _ = await task.ConfigureAwait(false); } - internal static MethodInfo GetGetResultMethod(Type taskType) - { - if (!taskType.IsGenericType) - { - return typeof(AwaitHelper).GetMethod(nameof(AwaitHelper.GetResult), BindingFlags.Public | BindingFlags.Static, null, new Type[1] { taskType }, null); - } + internal static MethodInfo GetGetResultMethod(Type taskType) => GetMethod(taskType, nameof(AwaitHelper.GetResult)); - Type compareType = taskType.GetGenericTypeDefinition() == typeof(ValueTask<>) ? typeof(ValueTask<>) - : typeof(Task).IsAssignableFrom(taskType.GetGenericTypeDefinition()) ? typeof(Task<>) - : null; - if (compareType == null) - { - return null; - } - return typeof(AwaitHelper).GetMethods(BindingFlags.Public | BindingFlags.Static) - .First(m => - { - if (m.Name != nameof(AwaitHelper.GetResult)) return false; - Type paramType = m.GetParameters().First().ParameterType; - // We have to compare the types indirectly, == check doesn't work. - return paramType.Assembly == compareType.Assembly && paramType.Namespace == compareType.Namespace && paramType.Name == compareType.Name; - }) - .MakeGenericMethod(new[] { GetTaskResultType(taskType) }); - } + internal static MethodInfo GetToValueTaskMethod(Type taskType) => GetMethod(taskType, nameof(AwaitHelper.ToValueTaskVoid)); - internal static MethodInfo GetToValueTaskMethod(Type taskType) + private static MethodInfo GetMethod(Type taskType, string methodName) { if (!taskType.IsGenericType) { - return typeof(AwaitHelper).GetMethod(nameof(AwaitHelper.ToValueTaskVoid), BindingFlags.Public | BindingFlags.Static, null, new Type[1] { taskType }, null); + return typeof(AwaitHelper).GetMethod(methodName, BindingFlags.Public | BindingFlags.Static, null, new Type[1] { taskType }, null); } Type compareType = taskType.GetGenericTypeDefinition() == typeof(ValueTask<>) ? typeof(ValueTask<>) - : typeof(Task).IsAssignableFrom(taskType.GetGenericTypeDefinition()) ? typeof(Task<>) + : typeof(Task).IsAssignableFrom(taskType) ? typeof(Task<>) : null; - if (compareType == null) - { - return null; - } return compareType == null ? null : typeof(AwaitHelper).GetMethods(BindingFlags.Public | BindingFlags.Static) - .First(m => - { - if (m.Name != nameof(AwaitHelper.ToValueTaskVoid)) return false; - Type paramType = m.GetParameters().First().ParameterType; - // We have to compare the types indirectly, == check doesn't work. - return paramType.Assembly == compareType.Assembly && paramType.Namespace == compareType.Namespace && paramType.Name == compareType.Name; - }) - .MakeGenericMethod(new[] { GetTaskResultType(taskType) }); + .First(m => + { + if (m.Name != methodName) return false; + Type paramType = m.GetParameters().First().ParameterType; + // We have to compare the types indirectly, == check doesn't work. + return paramType.Assembly == compareType.Assembly && paramType.Namespace == compareType.Namespace && paramType.Name == compareType.Name; + }) + .MakeGenericMethod(new[] + { + taskType + .GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance).ReturnType + .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlags.Public | BindingFlags.Instance).ReturnType + }); } - - private static Type GetTaskResultType(Type taskType) => taskType - .GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance) - .ReturnType - .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlags.Public | BindingFlags.Instance) - .ReturnType; } -} +} \ No newline at end of file From 3c398d0454dbb527b44b3b8b255e08d16a0e3432 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 24 Jun 2023 09:17:34 -0400 Subject: [PATCH 08/13] Experimental custom async method builder support. --- .../Engines/AsyncConsumerStateMachine.cs | 196 ++++++++ src/BenchmarkDotNet/Engines/AsyncConsumers.cs | 157 ++++++ .../BenchmarkActionFactory_Implementations.cs | 460 ++---------------- 3 files changed, 395 insertions(+), 418 deletions(-) create mode 100644 src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs create mode 100644 src/BenchmarkDotNet/Engines/AsyncConsumers.cs diff --git a/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs b/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs new file mode 100644 index 0000000000..ea1580a763 --- /dev/null +++ b/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs @@ -0,0 +1,196 @@ +using BenchmarkDotNet.Helpers; +using Perfolizer.Horology; +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Engines +{ + // Using an interface instead of delegates allows the JIT to inline the call when it's used as a generic struct. + public interface IBenchmarkFunc + { + TResult InvokeWorkload(); + TResult InvokeOverhead(); + } + + public class AsyncBenchmarkRunner : ICriticalNotifyCompletion, IDisposable + where TBenchmarkFunc : struct, IBenchmarkFunc + // Struct constraint allows us to create the default value and forces the JIT to generate specialized code that can be inlined. + where TAsyncConsumer : struct, IAsyncConsumer + where TAwaiter : ICriticalNotifyCompletion + { + // Using struct rather than class forces the JIT to generate specialized code that can be inlined. + // Also C# compiler uses struct state machines in Release mode, so we want to do the same. + private struct StateMachine : IAsyncStateMachine + { + internal AsyncBenchmarkRunner owner; + internal TAsyncConsumer asyncConsumer; + + public void MoveNext() => owner.MoveNext(ref this); + + void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) => asyncConsumer.SetStateMachine(stateMachine); + } + + private readonly AutoResetValueTaskSource valueTaskSource = new (); + private TBenchmarkFunc benchmarkFunc; + private int state = -1; + private long repeatsRemaining; + private StartedClock startedClock; + private TAwaiter currentAwaiter; + private Action continuation; + + public AsyncBenchmarkRunner(TBenchmarkFunc benchmarkFunc) + { + this.benchmarkFunc = benchmarkFunc; + // Initialize the state machine and consumer before the actual workload starts. + StateMachine stateMachine = default; + stateMachine.asyncConsumer = new TAsyncConsumer(); + stateMachine.asyncConsumer.CreateAsyncMethodBuilder(); + stateMachine.owner = this; + stateMachine.asyncConsumer.Start(ref stateMachine); + } + + public ValueTask InvokeWorkload(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + Action action = continuation; + continuation = null; + startedClock = clock.Start(); + // The continuation callback moves the state machine forward through the builder in the TAsyncConsumer. + action(); + return new ValueTask(valueTaskSource, valueTaskSource.Version); + } + + void ICriticalNotifyCompletion.UnsafeOnCompleted(Action continuation) + => this.continuation = continuation; + + void INotifyCompletion.OnCompleted(Action continuation) + => this.continuation = continuation; + + private void MoveNext(ref StateMachine stateMachine) + { + try + { + if (state < 0) + { + if (state == -1) + { + // This is called when we call asyncConsumer.Start, so we just hook up the continuation + // to the owner so the state machine can be moved forward when the benchmark starts. + state = 0; + var _this = this; + stateMachine.asyncConsumer.AwaitOnCompleted(ref _this, ref stateMachine); + return; + } + // This has been disposed, we complete the consumer. + stateMachine.asyncConsumer.SetResult(); + return; + } + + if (state == 1) + { + state = 0; + stateMachine.asyncConsumer.GetResult(ref currentAwaiter); + } + + while (--repeatsRemaining >= 0) + { + var awaitable = benchmarkFunc.InvokeWorkload(); + currentAwaiter = stateMachine.asyncConsumer.GetAwaiter(ref awaitable); + if (!stateMachine.asyncConsumer.GetIsCompleted(ref currentAwaiter)) + { + state = 1; + stateMachine.asyncConsumer.AwaitOnCompleted(ref currentAwaiter, ref stateMachine); + return; + } + stateMachine.asyncConsumer.GetResult(ref currentAwaiter); + } + } + catch (Exception e) + { + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetException(e); + return; + } + var clockspan = startedClock.GetElapsed(); + currentAwaiter = default; + startedClock = default; + { + // We hook up the continuation to the owner so the state machine can be moved forward when the next benchmark iteration starts. + stateMachine.asyncConsumer.AwaitOnCompleted(ref stateMachine.owner, ref stateMachine); + } + valueTaskSource.SetResult(clockspan); + } + + public void Dispose() + { + benchmarkFunc = default; + Action action = continuation; + continuation = null; + // Set the state and invoke the callback for the state machine to advance to complete the consumer. + state = -2; + action(); + } + + public ValueTask InvokeOverhead(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + TAwaitable value = default; + startedClock = clock.Start(); + try + { + while (--repeatsRemaining >= 0) + { + value = benchmarkFunc.InvokeOverhead(); + } + } + catch (Exception e) + { + currentAwaiter = default; + startedClock = default; + valueTaskSource.SetException(e); + DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + throw; + } + var clockspan = startedClock.GetElapsed(); + currentAwaiter = default; + startedClock = default; + return new ValueTask(clockspan); + } + + public ValueTask InvokeSingle() + { + var asyncConsumer = new TAsyncConsumer(); + var awaitable = benchmarkFunc.InvokeWorkload(); + var awaiter = asyncConsumer.GetAwaiter(ref awaitable); + if (asyncConsumer.GetIsCompleted(ref awaiter)) + { + try + { + asyncConsumer.GetResult(ref awaiter); + } + catch (Exception e) + { + return new ValueTask(Task.FromException(e)); + } + return new ValueTask(); + } + var taskCompletionSource = new TaskCompletionSource(); + awaiter.UnsafeOnCompleted(() => + { + try + { + asyncConsumer.GetResult(ref awaiter); + } + catch (Exception e) + { + taskCompletionSource.SetException(e); + return; + } + taskCompletionSource.SetResult(true); + }); + return new ValueTask(taskCompletionSource.Task); + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Engines/AsyncConsumers.cs b/src/BenchmarkDotNet/Engines/AsyncConsumers.cs new file mode 100644 index 0000000000..f36ead91ef --- /dev/null +++ b/src/BenchmarkDotNet/Engines/AsyncConsumers.cs @@ -0,0 +1,157 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Engines +{ + public interface IAwaitableConverter + where TAwaiter : ICriticalNotifyCompletion + { + public TAwaiter GetAwaiter(ref TAwaitable awaitable); + public bool GetIsCompleted(ref TAwaiter awaiter); + public void GetResult(ref TAwaiter awaiter); + } + + public interface IAsyncMethodBuilder + { + public void CreateAsyncMethodBuilder(); + public void Start(ref TStateMachine stateMachine) + where TStateMachine : IAsyncStateMachine; + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine; + public void SetStateMachine(IAsyncStateMachine stateMachine); + public void SetResult(); + } + + public interface IAsyncConsumer : IAwaitableConverter, IAsyncMethodBuilder + where TAwaiter : ICriticalNotifyCompletion + { + } + + // We use ConfigureAwait(false) to prevent dead-locks with InProcess toolchains (it could be ran on a thread with a SynchronizationContext). + // Using struct rather than class forces the JIT to generate specialized code that can be inlined. + public struct TaskConsumer : IAsyncConsumer + { + private AsyncTaskMethodBuilder _builder; + + public void CreateAsyncMethodBuilder() + => _builder = AsyncTaskMethodBuilder.Create(); + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => _builder.Start(ref stateMachine); + + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + => _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + + public void SetResult() + => _builder.SetResult(); + + public void SetStateMachine(IAsyncStateMachine stateMachine) + => _builder.SetStateMachine(stateMachine); + + public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter(ref Task awaitable) + => awaitable.ConfigureAwait(false).GetAwaiter(); + + public bool GetIsCompleted(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) + => awaiter.IsCompleted; + + public void GetResult(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) + => awaiter.GetResult(); + } + + public struct TaskConsumer : IAsyncConsumer, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter> + { + private AsyncTaskMethodBuilder _builder; + + public void CreateAsyncMethodBuilder() + => _builder = AsyncTaskMethodBuilder.Create(); + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => _builder.Start(ref stateMachine); + + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + => _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + + public void SetResult() + => _builder.SetResult(); + + public void SetStateMachine(IAsyncStateMachine stateMachine) + => _builder.SetStateMachine(stateMachine); + + public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter(ref Task awaitable) + => awaitable.ConfigureAwait(false).GetAwaiter(); + + public bool GetIsCompleted(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) + => awaiter.IsCompleted; + + public void GetResult(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) + => awaiter.GetResult(); + } + + public struct ValueTaskConsumer : IAsyncConsumer + { + private AsyncValueTaskMethodBuilder _builder; + + public void CreateAsyncMethodBuilder() + => _builder = AsyncValueTaskMethodBuilder.Create(); + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => _builder.Start(ref stateMachine); + + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + => _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + + public void SetResult() + => _builder.SetResult(); + + public void SetStateMachine(IAsyncStateMachine stateMachine) + => _builder.SetStateMachine(stateMachine); + + public ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter GetAwaiter(ref ValueTask awaitable) + => awaitable.ConfigureAwait(false).GetAwaiter(); + + public bool GetIsCompleted(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) + => awaiter.IsCompleted; + + public void GetResult(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) + => awaiter.GetResult(); + } + + public struct ValueTaskConsumer : IAsyncConsumer, ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter> + { + private AsyncValueTaskMethodBuilder _builder; + + public void CreateAsyncMethodBuilder() + => _builder = AsyncValueTaskMethodBuilder.Create(); + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => _builder.Start(ref stateMachine); + + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + => _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + + public void SetResult() + => _builder.SetResult(); + + public void SetStateMachine(IAsyncStateMachine stateMachine) + => _builder.SetStateMachine(stateMachine); + + public ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter GetAwaiter(ref ValueTask awaitable) + => awaitable.ConfigureAwait(false).GetAwaiter(); + + public bool GetIsCompleted(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) + => awaiter.IsCompleted; + + public void GetResult(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) + => awaiter.GetResult(); + } +} diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs index 319f81863d..03b53a5b84 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs @@ -1,4 +1,5 @@ -using BenchmarkDotNet.Helpers; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Helpers; using Perfolizer.Horology; using System; using System.Reflection; @@ -102,257 +103,41 @@ private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock cl public override object LastRunResult => result; } - internal class BenchmarkActionTask : BenchmarkActionBase + internal static class BenchmarkActionAsyncFactory { - private readonly Func callback; - private readonly AutoResetValueTaskSource valueTaskSource = new AutoResetValueTaskSource(); - private long repeatsRemaining; - private readonly Action continuation; - private StartedClock startedClock; - private TaskAwaiter currentAwaiter; - - public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) - { - continuation = Continuation; - bool isIdle = method == null; - if (!isIdle) - { - callback = CreateWorkload>(instance, method); - InvokeSingle = InvokeSingleHardcoded; - InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; - } - else - { - callback = Overhead; - InvokeSingle = InvokeSingleHardcodedOverhead; - InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcodedOverhead; - } - } - - private Task Overhead() => default; - - private ValueTask InvokeSingleHardcodedOverhead() - { - callback(); - return new ValueTask(); - } - - private ValueTask InvokeNoUnrollHardcodedOverhead(long repeatCount, IClock clock) - { - repeatsRemaining = repeatCount; - Task value = default; - startedClock = clock.Start(); - try - { - while (--repeatsRemaining >= 0) - { - value = callback(); - } - } - catch (Exception) - { - Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); - throw; - } - return new ValueTask(startedClock.GetElapsed()); - } - - private ValueTask InvokeSingleHardcoded() - { - return AwaitHelper.ToValueTaskVoid(callback()); - } - - private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) - { - repeatsRemaining = repeatCount; - startedClock = clock.Start(); - RunTask(); - return new ValueTask(valueTaskSource, valueTaskSource.Version); - } - - private void RunTask() - { - try - { - while (--repeatsRemaining >= 0) - { - currentAwaiter = callback().GetAwaiter(); - if (!currentAwaiter.IsCompleted) - { - currentAwaiter.UnsafeOnCompleted(continuation); - return; - } - currentAwaiter.GetResult(); - } - } - catch (Exception e) - { - SetException(e); - return; - } - var clockspan = startedClock.GetElapsed(); - currentAwaiter = default; - startedClock = default; - valueTaskSource.SetResult(clockspan); - } - - private void Continuation() - { - try - { - currentAwaiter.GetResult(); - } - catch (Exception e) - { - SetException(e); - return; - } - RunTask(); - } - - private void SetException(Exception e) + internal static BenchmarkActionBase Create(Type consumerType, Type awaitableType, Type awaiterType, object instance, MethodInfo method, int unrollFactor) { - currentAwaiter = default; - startedClock = default; - valueTaskSource.SetException(e); + return (BenchmarkActionBase) Activator.CreateInstance( + typeof(BenchmarkActionAsync<,,>).MakeGenericType(consumerType, awaitableType, awaiterType), + instance, + method, + unrollFactor); } } - internal class BenchmarkActionTask : BenchmarkActionBase + internal class BenchmarkActionAsync : BenchmarkActionBase + where TAsyncConsumer : struct, IAsyncConsumer + where TAwaiter : ICriticalNotifyCompletion { - private readonly Func> callback; - private readonly AutoResetValueTaskSource valueTaskSource = new AutoResetValueTaskSource(); - private long repeatsRemaining; - private readonly Action continuation; - private StartedClock startedClock; - private TaskAwaiter currentAwaiter; - private T result; - - public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) + // IBenchmarkFunc implemented via struct instead of on the class so that it can be inlined. + private readonly struct AsyncBenchmarkFunc : IBenchmarkFunc { - continuation = Continuation; - bool isIdle = method == null; - if (!isIdle) - { - callback = CreateWorkload>>(instance, method); - InvokeSingle = InvokeSingleHardcoded; - InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; - } - else - { - callback = Overhead; - InvokeSingle = InvokeSingleHardcodedOverhead; - InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcodedOverhead; - } - } - - private Task Overhead() => default; + private readonly Func callback; - private ValueTask InvokeSingleHardcodedOverhead() - { - callback(); - return new ValueTask(); + internal AsyncBenchmarkFunc(Func callback) => this.callback = callback; + public TAwaitable InvokeWorkload() => callback(); + public TAwaitable InvokeOverhead() => callback(); } - private ValueTask InvokeNoUnrollHardcodedOverhead(long repeatCount, IClock clock) - { - repeatsRemaining = repeatCount; - Task value = default; - startedClock = clock.Start(); - try - { - while (--repeatsRemaining >= 0) - { - value = callback(); - } - } - catch (Exception) - { - Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); - throw; - } - return new ValueTask(startedClock.GetElapsed()); - } - - private ValueTask InvokeSingleHardcoded() - { - return AwaitHelper.ToValueTaskVoid(callback()); - } + private readonly Func callback; + private readonly AsyncBenchmarkRunner asyncBenchmarkRunner; - private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) - { - repeatsRemaining = repeatCount; - startedClock = clock.Start(); - RunTask(); - return new ValueTask(valueTaskSource, valueTaskSource.Version); - } - - private void RunTask() - { - try - { - while (--repeatsRemaining >= 0) - { - currentAwaiter = callback().GetAwaiter(); - if (!currentAwaiter.IsCompleted) - { - currentAwaiter.UnsafeOnCompleted(continuation); - return; - } - result = currentAwaiter.GetResult(); - } - } - catch (Exception e) - { - SetException(e); - return; - } - var clockspan = startedClock.GetElapsed(); - currentAwaiter = default; - startedClock = default; - valueTaskSource.SetResult(clockspan); - } - - private void Continuation() + public BenchmarkActionAsync(object instance, MethodInfo method, int unrollFactor) { - try - { - result = currentAwaiter.GetResult(); - } - catch (Exception e) - { - SetException(e); - return; - } - RunTask(); - } - - private void SetException(Exception e) - { - currentAwaiter = default; - startedClock = default; - valueTaskSource.SetException(e); - } - - public override object LastRunResult => result; - } - - internal class BenchmarkActionValueTask : BenchmarkActionBase - { - private readonly Func callback; - private readonly AutoResetValueTaskSource valueTaskSource = new AutoResetValueTaskSource(); - private long repeatsRemaining; - private readonly Action continuation; - private StartedClock startedClock; - private ValueTaskAwaiter currentAwaiter; - - public BenchmarkActionValueTask(object instance, MethodInfo method, int unrollFactor) - { - continuation = Continuation; bool isIdle = method == null; if (!isIdle) { - callback = CreateWorkload>(instance, method); + callback = CreateWorkload>(instance, method); InvokeSingle = InvokeSingleHardcoded; InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; } @@ -362,9 +147,10 @@ public BenchmarkActionValueTask(object instance, MethodInfo method, int unrollFa InvokeSingle = InvokeSingleHardcodedOverhead; InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcodedOverhead; } + asyncBenchmarkRunner = new (new AsyncBenchmarkFunc(callback)); } - private ValueTask Overhead() => default; + private TAwaitable Overhead() => default; private ValueTask InvokeSingleHardcodedOverhead() { @@ -373,203 +159,41 @@ private ValueTask InvokeSingleHardcodedOverhead() } private ValueTask InvokeNoUnrollHardcodedOverhead(long repeatCount, IClock clock) - { - repeatsRemaining = repeatCount; - ValueTask value = default; - startedClock = clock.Start(); - try - { - while (--repeatsRemaining >= 0) - { - value = callback(); - } - } - catch (Exception) - { - Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); - throw; - } - return new ValueTask(startedClock.GetElapsed()); - } + => asyncBenchmarkRunner.InvokeOverhead(repeatCount, clock); - private ValueTask InvokeSingleHardcoded() - { - return AwaitHelper.ToValueTaskVoid(callback()); - } + protected virtual ValueTask InvokeSingleHardcoded() + => asyncBenchmarkRunner.InvokeSingle(); private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) - { - repeatsRemaining = repeatCount; - startedClock = clock.Start(); - RunTask(); - return new ValueTask(valueTaskSource, valueTaskSource.Version); - } - - private void RunTask() - { - try - { - while (--repeatsRemaining >= 0) - { - currentAwaiter = callback().GetAwaiter(); - if (!currentAwaiter.IsCompleted) - { - currentAwaiter.UnsafeOnCompleted(continuation); - return; - } - currentAwaiter.GetResult(); - } - } - catch (Exception e) - { - SetException(e); - return; - } - var clockspan = startedClock.GetElapsed(); - currentAwaiter = default; - startedClock = default; - valueTaskSource.SetResult(clockspan); - } - - private void Continuation() - { - try - { - currentAwaiter.GetResult(); - } - catch (Exception e) - { - SetException(e); - return; - } - RunTask(); - } - - private void SetException(Exception e) - { - currentAwaiter = default; - startedClock = default; - valueTaskSource.SetException(e); - } + => asyncBenchmarkRunner.InvokeWorkload(repeatCount, clock); } - internal class BenchmarkActionValueTask : BenchmarkActionBase + internal class BenchmarkActionTask : BenchmarkActionAsync { - private readonly Func> callback; - private readonly AutoResetValueTaskSource valueTaskSource = new AutoResetValueTaskSource(); - private long repeatsRemaining; - private readonly Action continuation; - private StartedClock startedClock; - private ValueTaskAwaiter currentAwaiter; - private T result; - - public BenchmarkActionValueTask(object instance, MethodInfo method, int unrollFactor) + public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) : base(instance, method, unrollFactor) { - continuation = Continuation; - bool isIdle = method == null; - if (!isIdle) - { - callback = CreateWorkload>>(instance, method); - InvokeSingle = InvokeSingleHardcoded; - InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; - } - else - { - callback = Overhead; - InvokeSingle = InvokeSingleHardcodedOverhead; - InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcodedOverhead; - } - } - - private ValueTask Overhead() => default; - - private ValueTask InvokeSingleHardcodedOverhead() - { - callback(); - return new ValueTask(); - } - - private ValueTask InvokeNoUnrollHardcodedOverhead(long repeatCount, IClock clock) - { - repeatsRemaining = repeatCount; - ValueTask value = default; - startedClock = clock.Start(); - try - { - while (--repeatsRemaining >= 0) - { - value = callback(); - } - } - catch (Exception) - { - Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); - throw; - } - return new ValueTask(startedClock.GetElapsed()); - } - - private ValueTask InvokeSingleHardcoded() - { - return AwaitHelper.ToValueTaskVoid(callback()); - } - - private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) - { - repeatsRemaining = repeatCount; - startedClock = clock.Start(); - RunTask(); - return new ValueTask(valueTaskSource, valueTaskSource.Version); } + } - private void RunTask() + internal class BenchmarkActionTask : BenchmarkActionAsync, Task, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter> + { + public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) : base(instance, method, unrollFactor) { - try - { - while (--repeatsRemaining >= 0) - { - currentAwaiter = callback().GetAwaiter(); - if (!currentAwaiter.IsCompleted) - { - currentAwaiter.UnsafeOnCompleted(continuation); - return; - } - result = currentAwaiter.GetResult(); - } - } - catch (Exception e) - { - SetException(e); - return; - } - var clockspan = startedClock.GetElapsed(); - currentAwaiter = default; - startedClock = default; - valueTaskSource.SetResult(clockspan); } + } - private void Continuation() + internal class BenchmarkActionValueTask : BenchmarkActionAsync + { + public BenchmarkActionValueTask(object instance, MethodInfo method, int unrollFactor) : base(instance, method, unrollFactor) { - try - { - result = currentAwaiter.GetResult(); - } - catch (Exception e) - { - SetException(e); - return; - } - RunTask(); } + } - private void SetException(Exception e) + internal class BenchmarkActionValueTask : BenchmarkActionAsync, ValueTask, ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter> + { + public BenchmarkActionValueTask(object instance, MethodInfo method, int unrollFactor) : base(instance, method, unrollFactor) { - currentAwaiter = default; - startedClock = default; - valueTaskSource.SetException(e); } - - public override object LastRunResult => result; } } } \ No newline at end of file From 61de07fd65ab99b13254716d2396d2e0b73abfbf Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 27 Jun 2023 01:16:36 -0400 Subject: [PATCH 09/13] Added config option to add async consumers. Use async consumers in toolchains (WIP). --- src/BenchmarkDotNet/Code/CodeGenerator.cs | 14 +- .../Code/DeclarationsProvider.cs | 22 +++- .../Configs/ConfigExtensions.cs | 29 +++++ src/BenchmarkDotNet/Configs/DebugConfig.cs | 10 ++ src/BenchmarkDotNet/Configs/DefaultConfig.cs | 11 ++ src/BenchmarkDotNet/Configs/IConfig.cs | 1 + .../Configs/ImmutableConfig.cs | 4 + .../Configs/ImmutableConfigBuilder.cs | 2 + src/BenchmarkDotNet/Configs/ManualConfig.cs | 59 +++++++++ .../Engines/AsyncConsumerStateMachine.cs | 7 + src/BenchmarkDotNet/Engines/AsyncConsumers.cs | 3 +- .../Templates/BenchmarkType.txt | 121 +++--------------- 12 files changed, 166 insertions(+), 117 deletions(-) diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs index 7921d715f8..7ea9cd3032 100644 --- a/src/BenchmarkDotNet/Code/CodeGenerator.cs +++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; +using BenchmarkDotNet.Configs; using BenchmarkDotNet.Disassemblers; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Extensions; @@ -33,7 +34,7 @@ internal static string Generate(BuildPartition buildPartition) { var benchmark = buildInfo.BenchmarkCase; - var provider = GetDeclarationsProvider(benchmark.Descriptor); + var provider = GetDeclarationsProvider(benchmark.Descriptor, benchmark.Config); provider.OverrideUnrollFactor(benchmark); @@ -51,7 +52,7 @@ internal static string Generate(BuildPartition buildPartition) .Replace("$WorkloadMethodReturnType$", provider.WorkloadMethodReturnTypeName) .Replace("$WorkloadMethodReturnTypeModifiers$", provider.WorkloadMethodReturnTypeModifiers) .Replace("$OverheadMethodReturnTypeName$", provider.OverheadMethodReturnTypeName) - .Replace("$AwaiterTypeName$", provider.AwaiterTypeName) + .Replace("$AsyncBenchmarkRunnerType$", provider.AsyncBenchmarkRunnerTypeName) .Replace("$GlobalSetupMethodName$", provider.GlobalSetupMethodName) .Replace("$GlobalCleanupMethodName$", provider.GlobalCleanupMethodName) .Replace("$IterationSetupMethodName$", provider.IterationSetupMethodName) @@ -151,16 +152,13 @@ private static string GetJobsSetDefinition(BenchmarkCase benchmarkCase) Replace("; ", ";\n "); } - private static DeclarationsProvider GetDeclarationsProvider(Descriptor descriptor) + private static DeclarationsProvider GetDeclarationsProvider(Descriptor descriptor, IConfig config) { var method = descriptor.WorkloadMethod; - if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(ValueTask) - || method.ReturnType.GetTypeInfo().IsGenericType - && (method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) - || method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>))) + if (config.GetIsAwaitable(method.ReturnType, out var asyncConsumerType)) { - return new TaskDeclarationsProvider(descriptor); + return new AsyncDeclarationsProvider(descriptor, asyncConsumerType); } if (method.ReturnType == typeof(void)) diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs index 5f2eca39a6..fd479ed98b 100644 --- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs +++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Reflection; using System.Threading.Tasks; using BenchmarkDotNet.Engines; @@ -44,7 +45,7 @@ internal abstract class DeclarationsProvider public string OverheadMethodReturnTypeName => OverheadMethodReturnType.GetCorrectCSharpTypeName(); - public virtual string AwaiterTypeName => string.Empty; + public virtual string AsyncBenchmarkRunnerTypeName => null; public virtual void OverrideUnrollFactor(BenchmarkCase benchmarkCase) { } @@ -146,18 +147,29 @@ public ByReadOnlyRefDeclarationsProvider(Descriptor descriptor) : base(descripto public override string WorkloadMethodReturnTypeModifiers => "ref readonly"; } - internal class TaskDeclarationsProvider : DeclarationsProvider + internal class AsyncDeclarationsProvider : DeclarationsProvider { - public TaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { } + private readonly Type asyncConsumerType; + public AsyncDeclarationsProvider(Descriptor descriptor, Type asyncConsumerType) : base(descriptor) => this.asyncConsumerType = asyncConsumerType; public override string ReturnsDefinition => "RETURNS_AWAITABLE"; - public override string AwaiterTypeName => WorkloadMethodReturnType.GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance).ReturnType.GetCorrectCSharpTypeName(); - public override string OverheadImplementation => $"return default({OverheadMethodReturnType.GetCorrectCSharpTypeName()});"; protected override Type OverheadMethodReturnType => WorkloadMethodReturnType; + public override string AsyncBenchmarkRunnerTypeName + { + get + { + string consumerTypeName = asyncConsumerType.GetCorrectCSharpTypeName(); + string awaiterTypeName = asyncConsumerType.GetInterfaces() + .First(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncConsumer<,>)) + .GetGenericArguments()[1].GetCorrectCSharpTypeName(); + return $"BenchmarkDotNet.Engines.AsyncBenchmarkRunner"; + } + } + public override void OverrideUnrollFactor(BenchmarkCase benchmarkCase) => benchmarkCase.ForceUnrollFactorForAsync(); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs index fa0920c7f9..e64d326daf 100644 --- a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs +++ b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs @@ -3,10 +3,12 @@ using System.ComponentModel; using System.Globalization; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Filters; using BenchmarkDotNet.Jobs; @@ -114,6 +116,10 @@ public static class ConfigExtensions [PublicAPI] public static ManualConfig HideColumns(this IConfig config, params string[] columnNames) => config.With(c => c.HideColumns(columnNames)); [PublicAPI] public static ManualConfig HideColumns(this IConfig config, params IColumn[] columns) => config.With(c => c.HideColumns(columns)); [PublicAPI] public static ManualConfig HideColumns(this IConfig config, params IColumnHidingRule[] rules) => config.With(c => c.HideColumns(rules)); + [PublicAPI] public static ManualConfig AddAsyncConsumer(this IConfig config) + where TAwaiter : ICriticalNotifyCompletion + where TAsyncConsumer : struct, IAsyncConsumer + => config.With(c => c.AddAsyncConsumer()); public static ImmutableConfig CreateImmutableConfig(this IConfig config) => ImmutableConfigBuilder.Create(config); @@ -132,5 +138,28 @@ private static ManualConfig With(this IConfig config, Action addAc addAction(manualConfig); return manualConfig; } + + internal static bool GetIsAwaitable(this IConfig config, Type type, out Type asyncConsumerType) + { + var consumerTypes = config.GetAsyncConsumerTypes(); + if (consumerTypes.TryGetValue(type, out asyncConsumerType)) + { + return true; + } + if (type.IsGenericType) + { + var genericType = type.GetGenericArguments()[0]; + foreach (var kvp in consumerTypes) + { + if (kvp.Key.IsGenericType && kvp.Key.MakeGenericType(genericType) == type) + { + // TODO: handle partially closed types. + asyncConsumerType = kvp.Value.MakeGenericType(genericType); + return true; + } + } + } + return false; + } } } diff --git a/src/BenchmarkDotNet/Configs/DebugConfig.cs b/src/BenchmarkDotNet/Configs/DebugConfig.cs index 2cbdff2461..56ec0c4191 100644 --- a/src/BenchmarkDotNet/Configs/DebugConfig.cs +++ b/src/BenchmarkDotNet/Configs/DebugConfig.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Threading.Tasks; using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Filters; using BenchmarkDotNet.Jobs; @@ -65,6 +67,14 @@ public abstract class DebugConfig : IConfig public IEnumerable GetHardwareCounters() => Array.Empty(); public IEnumerable GetFilters() => Array.Empty(); public IEnumerable GetColumnHidingRules() => Array.Empty(); + public IReadOnlyDictionary GetAsyncConsumerTypes() => new Dictionary() + { + // Default consumers for Task and ValueTask. + [typeof(Task)] = typeof(TaskConsumer), + [typeof(Task<>)] = typeof(TaskConsumer<>), + [typeof(ValueTask)] = typeof(ValueTaskConsumer), + [typeof(ValueTask<>)] = typeof(ValueTaskConsumer<>), + }; public IOrderer Orderer => DefaultOrderer.Instance; public ICategoryDiscoverer? CategoryDiscoverer => DefaultCategoryDiscoverer.Instance; diff --git a/src/BenchmarkDotNet/Configs/DefaultConfig.cs b/src/BenchmarkDotNet/Configs/DefaultConfig.cs index 8d1df6285b..3c5d175134 100644 --- a/src/BenchmarkDotNet/Configs/DefaultConfig.cs +++ b/src/BenchmarkDotNet/Configs/DefaultConfig.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Threading.Tasks; using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Exporters.Csv; using BenchmarkDotNet.Filters; @@ -109,5 +111,14 @@ public string ArtifactsPath public IEnumerable GetFilters() => Array.Empty(); public IEnumerable GetColumnHidingRules() => Array.Empty(); + + public IReadOnlyDictionary GetAsyncConsumerTypes() => new Dictionary() + { + // Default consumers for Task and ValueTask. + [typeof(Task)] = typeof(TaskConsumer), + [typeof(Task<>)] = typeof(TaskConsumer<>), + [typeof(ValueTask)] = typeof(ValueTaskConsumer), + [typeof(ValueTask<>)] = typeof(ValueTaskConsumer<>), + }; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Configs/IConfig.cs b/src/BenchmarkDotNet/Configs/IConfig.cs index 46a53692c2..c8b7cfbfe1 100644 --- a/src/BenchmarkDotNet/Configs/IConfig.cs +++ b/src/BenchmarkDotNet/Configs/IConfig.cs @@ -29,6 +29,7 @@ public interface IConfig IEnumerable GetFilters(); IEnumerable GetLogicalGroupRules(); IEnumerable GetColumnHidingRules(); + IReadOnlyDictionary GetAsyncConsumerTypes(); IOrderer? Orderer { get; } ICategoryDiscoverer? CategoryDiscoverer { get; } diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs index b5d84eaf56..be2e886ad6 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs @@ -32,6 +32,7 @@ public sealed class ImmutableConfig : IConfig private readonly ImmutableHashSet filters; private readonly ImmutableArray rules; private readonly ImmutableArray columnHidingRules; + private readonly ImmutableDictionary asyncConsumerTypes; internal ImmutableConfig( ImmutableArray uniqueColumnProviders, @@ -44,6 +45,7 @@ internal ImmutableConfig( ImmutableHashSet uniqueFilters, ImmutableArray uniqueRules, ImmutableArray uniqueColumnHidingRules, + ImmutableDictionary uniqueAsyncConsumerTypes, ImmutableHashSet uniqueRunnableJobs, ConfigUnionRule unionRule, string artifactsPath, @@ -65,6 +67,7 @@ internal ImmutableConfig( filters = uniqueFilters; rules = uniqueRules; columnHidingRules = uniqueColumnHidingRules; + asyncConsumerTypes = uniqueAsyncConsumerTypes; jobs = uniqueRunnableJobs; UnionRule = unionRule; ArtifactsPath = artifactsPath; @@ -97,6 +100,7 @@ internal ImmutableConfig( public IEnumerable GetFilters() => filters; public IEnumerable GetLogicalGroupRules() => rules; public IEnumerable GetColumnHidingRules() => columnHidingRules; + public IReadOnlyDictionary GetAsyncConsumerTypes() => asyncConsumerTypes; public ILogger GetCompositeLogger() => new CompositeLogger(loggers); public IExporter GetCompositeExporter() => new CompositeExporter(exporters); diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs index e85abf926d..f74a5419c2 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs @@ -51,6 +51,7 @@ public static ImmutableConfig Create(IConfig source) var uniqueFilters = source.GetFilters().ToImmutableHashSet(); var uniqueRules = source.GetLogicalGroupRules().ToImmutableArray(); var uniqueHidingRules = source.GetColumnHidingRules().ToImmutableArray(); + var uniqueAsyncConsumerTypes = source.GetAsyncConsumerTypes().ToImmutableDictionary(); var uniqueRunnableJobs = GetRunnableJobs(source.GetJobs()).ToImmutableHashSet(); @@ -65,6 +66,7 @@ public static ImmutableConfig Create(IConfig source) uniqueFilters, uniqueRules, uniqueHidingRules, + uniqueAsyncConsumerTypes, uniqueRunnableJobs, source.UnionRule, source.ArtifactsPath ?? DefaultConfig.Instance.ArtifactsPath, diff --git a/src/BenchmarkDotNet/Configs/ManualConfig.cs b/src/BenchmarkDotNet/Configs/ManualConfig.cs index 27eca81863..b46f4a6307 100644 --- a/src/BenchmarkDotNet/Configs/ManualConfig.cs +++ b/src/BenchmarkDotNet/Configs/ManualConfig.cs @@ -3,9 +3,12 @@ using System.ComponentModel; using System.Globalization; using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Filters; @@ -34,6 +37,14 @@ public class ManualConfig : IConfig private readonly List filters = new List(); private readonly List logicalGroupRules = new List(); private readonly List columnHidingRules = new List(); + private readonly Dictionary asyncConsumerTypes = new Dictionary() + { + // Default consumers for Task and ValueTask. + [typeof(Task)] = typeof(TaskConsumer), + [typeof(Task<>)] = typeof(TaskConsumer<>), + [typeof(ValueTask)] = typeof(ValueTaskConsumer), + [typeof(ValueTask<>)] = typeof(ValueTaskConsumer<>), + }; public IEnumerable GetColumnProviders() => columnProviders; public IEnumerable GetExporters() => exporters; @@ -46,6 +57,7 @@ public class ManualConfig : IConfig public IEnumerable GetFilters() => filters; public IEnumerable GetLogicalGroupRules() => logicalGroupRules; public IEnumerable GetColumnHidingRules() => columnHidingRules; + public IReadOnlyDictionary GetAsyncConsumerTypes() => asyncConsumerTypes; [PublicAPI] public ConfigOptions Options { get; set; } [PublicAPI] public ConfigUnionRule UnionRule { get; set; } = ConfigUnionRule.Union; @@ -106,6 +118,49 @@ public ManualConfig WithBuildTimeout(TimeSpan buildTimeout) return this; } + public ManualConfig AddAsyncConsumer(Type awaitableType, Type asyncConsumerType) + { + // Validate types + if (!asyncConsumerType.IsValueType) + { + throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] must be a struct."); + } + bool consumerIsOpenGeneric = asyncConsumerType.IsGenericTypeDefinition; + bool awaitableisOpenGeneric = awaitableType.IsGenericTypeDefinition; + if (consumerIsOpenGeneric != awaitableisOpenGeneric) + { + throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] or awaitableType [{awaitableType}] is an open generic type, while the other is not. Both types must be open or both must be closed."); + } + int consumerOpenGenericCount = asyncConsumerType.GetGenericArguments().Count(t => t.IsGenericParameter); + if (consumerOpenGenericCount > 1) + { + throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] has more than 1 open generic argument. Only 0 or 1 open generic arguments is supported."); + } + int awaitableOpenGenericCount = awaitableType.GetGenericArguments().Count(t => t.IsGenericParameter); + if (awaitableOpenGenericCount > 1) + { + throw new ArgumentException($"awaitableType [{awaitableType}] has more than 1 open generic argument. Only 0 or 1 open generic arguments is supported."); + } + if (consumerOpenGenericCount != awaitableOpenGenericCount) + { + throw new ArgumentException($"awaitableType [{awaitableType}] does not have the same open generic argument count as awaitableType [{awaitableType}]."); + } + + // If the types are open, make closed types for comparison. + // TODO: handle partially closed types. + var closedAwaitableType = awaitableisOpenGeneric ? awaitableType.MakeGenericType(typeof(int)) : awaitableType; + var closedConsumerType = consumerIsOpenGeneric ? asyncConsumerType.MakeGenericType(typeof(int)) : asyncConsumerType; + var iAsyncConsumerType = closedConsumerType.GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncConsumer<,>)) + ?? throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] does not implement IAsyncConsumer."); + if (iAsyncConsumerType.GetGenericArguments()[0] != closedAwaitableType) + { + throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] does not implement IAsyncConsumer with the expected TAwaitable type [{awaitableType}]."); + } + + asyncConsumerTypes[awaitableType] = asyncConsumerType; + return this; + } + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This method will soon be removed, please start using .AddColumn() instead.")] public void Add(params IColumn[] newColumns) => AddColumn(newColumns); @@ -261,6 +316,10 @@ public void Add(IConfig config) SummaryStyle = config.SummaryStyle ?? SummaryStyle; logicalGroupRules.AddRange(config.GetLogicalGroupRules()); columnHidingRules.AddRange(config.GetColumnHidingRules()); + foreach (var kvp in config.GetAsyncConsumerTypes()) + { + asyncConsumerTypes[kvp.Key] = kvp.Value; + } Options |= config.Options; BuildTimeout = GetBuildTimeout(BuildTimeout, config.BuildTimeout); } diff --git a/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs b/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs index ea1580a763..bebb48158b 100644 --- a/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs +++ b/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs @@ -67,6 +67,9 @@ void ICriticalNotifyCompletion.UnsafeOnCompleted(Action continuation) void INotifyCompletion.OnCompleted(Action continuation) => this.continuation = continuation; +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveOptimization)] +#endif private void MoveNext(ref StateMachine stateMachine) { try @@ -123,6 +126,7 @@ private void MoveNext(ref StateMachine stateMachine) valueTaskSource.SetResult(clockspan); } + // TODO: make sure Dispose is called. public void Dispose() { benchmarkFunc = default; @@ -133,6 +137,9 @@ public void Dispose() action(); } +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveOptimization)] +#endif public ValueTask InvokeOverhead(long repeatCount, IClock clock) { repeatsRemaining = repeatCount; diff --git a/src/BenchmarkDotNet/Engines/AsyncConsumers.cs b/src/BenchmarkDotNet/Engines/AsyncConsumers.cs index f36ead91ef..75f1d34cb7 100644 --- a/src/BenchmarkDotNet/Engines/AsyncConsumers.cs +++ b/src/BenchmarkDotNet/Engines/AsyncConsumers.cs @@ -1,5 +1,4 @@ -using System; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using System.Threading.Tasks; namespace BenchmarkDotNet.Engines diff --git a/src/BenchmarkDotNet/Templates/BenchmarkType.txt b/src/BenchmarkDotNet/Templates/BenchmarkType.txt index 584cce4d91..ff2469f0e6 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkType.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkType.txt @@ -1,5 +1,5 @@ // the type name must be in sync with WindowsDisassembler.BuildArguments - public unsafe partial class Runnable_$ID$ : global::$WorkloadTypeName$ + public unsafe class Runnable_$ID$ : global::$WorkloadTypeName$ { public static void Run(BenchmarkDotNet.Engines.IHost host, System.String benchmarkName) { @@ -64,7 +64,6 @@ overheadDelegate = __Overhead; workloadDelegate = $WorkloadMethodDelegate$; $InitializeArgumentFields$ - __SetContinuation(); } private System.Func globalSetupAction; @@ -110,138 +109,56 @@ $OverheadImplementation$ } - partial void __SetContinuation(); - #if RETURNS_AWAITABLE_$ID$ - private readonly BenchmarkDotNet.Helpers.AutoResetValueTaskSource valueTaskSource = new BenchmarkDotNet.Helpers.AutoResetValueTaskSource(); - private System.Int64 repeatsRemaining; - private System.Action continuation; - private Perfolizer.Horology.StartedClock startedClock; - private $AwaiterTypeName$ currentAwaiter; - - partial void __SetContinuation() => continuation = __Continuation; - -#if NETCOREAPP3_0_OR_GREATER - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] -#endif - // Awaits are not unrolled. - private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) + private struct BenchmarkFunc : IBenchmarkFunc<$WorkloadMethodReturnType$> { - return OverheadActionImpl(invokeCount, clock); - } - -#if NETCOREAPP3_0_OR_GREATER - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] -#endif - private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) - { - return OverheadActionImpl(invokeCount, clock); - } - -#if NETCOREAPP3_0_OR_GREATER - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] -#endif - private System.Threading.Tasks.ValueTask OverheadActionImpl(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) - { - repeatsRemaining = invokeCount; - $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); - startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); - try + $WorkloadMethodReturnType$ InvokeWorkload() { $LoadArguments$ - while (--repeatsRemaining >= 0) - { - value = overheadDelegate($PassArguments$); - } + return workloadDelegate($PassArguments$); } - catch (System.Exception) + + $WorkloadMethodReturnType$ InvokeOverhead() { - BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); - throw; + $LoadArguments$ + return overheadDelegate($PassArguments$); } - return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); - } - -#if NETCOREAPP3_0_OR_GREATER - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] -#endif - private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) - { - return WorkloadActionImpl(invokeCount, clock); } + + private readonly $AsyncBenchmarkRunnerType$ __asyncBenchmarkRunner = new $AsyncBenchmarkRunnerType$(); #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) + // Awaits are not unrolled. + private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { - return WorkloadActionImpl(invokeCount, clock); + return __asyncBenchmarkRunner.InvokeOverhead(invokeCount, clock); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private System.Threading.Tasks.ValueTask WorkloadActionImpl(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) + private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { - repeatsRemaining = invokeCount; - startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); - __RunTask(); - return new System.Threading.Tasks.ValueTask(valueTaskSource, valueTaskSource.Version); + return __asyncBenchmarkRunner.InvokeOverhead(invokeCount, clock); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void __RunTask() + private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { - try - { - $LoadArguments$ - while (--repeatsRemaining >= 0) - { - currentAwaiter = workloadDelegate($PassArguments$).GetAwaiter(); - if (!currentAwaiter.IsCompleted) - { - currentAwaiter.UnsafeOnCompleted(continuation); - return; - } - currentAwaiter.GetResult(); - } - } - catch (System.Exception e) - { - __SetException(e); - return; - } - var clockspan = startedClock.GetElapsed(); - currentAwaiter = default($AwaiterTypeName$); - startedClock = default(Perfolizer.Horology.StartedClock); - valueTaskSource.SetResult(clockspan); + return __asyncBenchmarkRunner.InvokeWorkload(invokeCount, clock); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void __Continuation() - { - try - { - currentAwaiter.GetResult(); - } - catch (System.Exception e) - { - __SetException(e); - return; - } - __RunTask(); - } - - private void __SetException(System.Exception e) + private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { - currentAwaiter = default($AwaiterTypeName$); - startedClock = default(Perfolizer.Horology.StartedClock); - valueTaskSource.SetException(e); + return __asyncBenchmarkRunner.InvokeWorkload(invokeCount, clock); } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] From a4e72c8ec9bc9d29b0d70ce203218e397775c420 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 28 Jun 2023 04:49:33 -0400 Subject: [PATCH 10/13] Corrected AsyncBenchmarkRunner type in generated code. Handle overhead without duplicating the state machine code. --- src/BenchmarkDotNet/Code/CodeGenerator.cs | 3 +- .../Code/DeclarationsProvider.cs | 28 +- src/BenchmarkDotNet/Configs/ManualConfig.cs | 8 +- .../Engines/AsyncConsumerStateMachine.cs | 377 ++++++++++++------ src/BenchmarkDotNet/Engines/AsyncConsumers.cs | 50 +-- .../Templates/BenchmarkType.txt | 56 ++- .../BenchmarkActionFactory_Implementations.cs | 9 +- 7 files changed, 345 insertions(+), 186 deletions(-) diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs index 7ea9cd3032..d97412d34c 100644 --- a/src/BenchmarkDotNet/Code/CodeGenerator.cs +++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs @@ -52,7 +52,8 @@ internal static string Generate(BuildPartition buildPartition) .Replace("$WorkloadMethodReturnType$", provider.WorkloadMethodReturnTypeName) .Replace("$WorkloadMethodReturnTypeModifiers$", provider.WorkloadMethodReturnTypeModifiers) .Replace("$OverheadMethodReturnTypeName$", provider.OverheadMethodReturnTypeName) - .Replace("$AsyncBenchmarkRunnerType$", provider.AsyncBenchmarkRunnerTypeName) + .Replace("$AsyncBenchmarkRunnerType$", provider.GetAsyncBenchmarkRunnerTypeName(buildInfo.Id)) + .Replace("$InitializeAsyncBenchmarkRunnerField$", provider.GetInitializeAsyncBenchmarkRunnerField(buildInfo.Id)) .Replace("$GlobalSetupMethodName$", provider.GlobalSetupMethodName) .Replace("$GlobalCleanupMethodName$", provider.GlobalCleanupMethodName) .Replace("$IterationSetupMethodName$", provider.IterationSetupMethodName) diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs index fd479ed98b..3054ff7652 100644 --- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs +++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs @@ -45,7 +45,9 @@ internal abstract class DeclarationsProvider public string OverheadMethodReturnTypeName => OverheadMethodReturnType.GetCorrectCSharpTypeName(); - public virtual string AsyncBenchmarkRunnerTypeName => null; + public virtual string GetAsyncBenchmarkRunnerTypeName(BenchmarkId id) => null; + + public virtual string GetInitializeAsyncBenchmarkRunnerField(BenchmarkId id) => null; public virtual void OverrideUnrollFactor(BenchmarkCase benchmarkCase) { } @@ -158,16 +160,22 @@ internal class AsyncDeclarationsProvider : DeclarationsProvider protected override Type OverheadMethodReturnType => WorkloadMethodReturnType; - public override string AsyncBenchmarkRunnerTypeName + private string GetRunnableName(BenchmarkId id) => $"BenchmarkDotNet.Autogenerated.Runnable_{id}"; + + public override string GetAsyncBenchmarkRunnerTypeName(BenchmarkId id) { - get - { - string consumerTypeName = asyncConsumerType.GetCorrectCSharpTypeName(); - string awaiterTypeName = asyncConsumerType.GetInterfaces() - .First(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncConsumer<,>)) - .GetGenericArguments()[1].GetCorrectCSharpTypeName(); - return $"BenchmarkDotNet.Engines.AsyncBenchmarkRunner"; - } + string consumerTypeName = asyncConsumerType.GetCorrectCSharpTypeName(); + string awaiterTypeName = asyncConsumerType.GetInterfaces() + .First(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncConsumer<,>)) + .GetGenericArguments()[1].GetCorrectCSharpTypeName(); + string runnableName = GetRunnableName(id); + return $"BenchmarkDotNet.Engines.AsyncBenchmarkRunner<{runnableName}.WorkloadFunc, {runnableName}.OverheadFunc, {consumerTypeName}, {WorkloadMethodReturnTypeName}, {awaiterTypeName}>"; + } + + public override string GetInitializeAsyncBenchmarkRunnerField(BenchmarkId id) + { + string runnableName = GetRunnableName(id); + return $"__asyncBenchmarkRunner = new {GetAsyncBenchmarkRunnerTypeName(id)}(new {runnableName}.WorkloadFunc(this), new {runnableName}.OverheadFunc(this));"; } public override void OverrideUnrollFactor(BenchmarkCase benchmarkCase) => benchmarkCase.ForceUnrollFactorForAsync(); diff --git a/src/BenchmarkDotNet/Configs/ManualConfig.cs b/src/BenchmarkDotNet/Configs/ManualConfig.cs index b46f4a6307..93d0676da4 100644 --- a/src/BenchmarkDotNet/Configs/ManualConfig.cs +++ b/src/BenchmarkDotNet/Configs/ManualConfig.cs @@ -121,9 +121,9 @@ public ManualConfig WithBuildTimeout(TimeSpan buildTimeout) public ManualConfig AddAsyncConsumer(Type awaitableType, Type asyncConsumerType) { // Validate types - if (!asyncConsumerType.IsValueType) + if (asyncConsumerType.IsNotPublic || (!asyncConsumerType.IsValueType && asyncConsumerType.GetConstructor(Array.Empty()) == null)) { - throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] must be a struct."); + throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] is not a public struct, or a public class with a public, parameterless constructor."); } bool consumerIsOpenGeneric = asyncConsumerType.IsGenericTypeDefinition; bool awaitableisOpenGeneric = awaitableType.IsGenericTypeDefinition; @@ -134,12 +134,12 @@ public ManualConfig AddAsyncConsumer(Type awaitableType, Type asyncConsumerType) int consumerOpenGenericCount = asyncConsumerType.GetGenericArguments().Count(t => t.IsGenericParameter); if (consumerOpenGenericCount > 1) { - throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] has more than 1 open generic argument. Only 0 or 1 open generic arguments is supported."); + throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] has more than 1 open generic argument. Only 0 or 1 open generic arguments are supported."); } int awaitableOpenGenericCount = awaitableType.GetGenericArguments().Count(t => t.IsGenericParameter); if (awaitableOpenGenericCount > 1) { - throw new ArgumentException($"awaitableType [{awaitableType}] has more than 1 open generic argument. Only 0 or 1 open generic arguments is supported."); + throw new ArgumentException($"awaitableType [{awaitableType}] has more than 1 open generic argument. Only 0 or 1 open generic arguments are supported."); } if (consumerOpenGenericCount != awaitableOpenGenericCount) { diff --git a/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs b/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs index bebb48158b..a0e71c25c3 100644 --- a/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs +++ b/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs @@ -7,169 +7,264 @@ namespace BenchmarkDotNet.Engines { // Using an interface instead of delegates allows the JIT to inline the call when it's used as a generic struct. - public interface IBenchmarkFunc + public interface IFunc { - TResult InvokeWorkload(); - TResult InvokeOverhead(); + TResult Invoke(); } - public class AsyncBenchmarkRunner : ICriticalNotifyCompletion, IDisposable - where TBenchmarkFunc : struct, IBenchmarkFunc - // Struct constraint allows us to create the default value and forces the JIT to generate specialized code that can be inlined. - where TAsyncConsumer : struct, IAsyncConsumer - where TAwaiter : ICriticalNotifyCompletion + internal sealed class AsyncStateMachineAdvancer : ICriticalNotifyCompletion { - // Using struct rather than class forces the JIT to generate specialized code that can be inlined. - // Also C# compiler uses struct state machines in Release mode, so we want to do the same. - private struct StateMachine : IAsyncStateMachine + // The continuation callback moves the state machine forward through the builder in the TAsyncConsumer. + private Action continuation; + + internal void Advance() { - internal AsyncBenchmarkRunner owner; - internal TAsyncConsumer asyncConsumer; + Action action = continuation; + continuation = null; + action(); + } - public void MoveNext() => owner.MoveNext(ref this); + void ICriticalNotifyCompletion.UnsafeOnCompleted(Action continuation) + => this.continuation = continuation; - void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) => asyncConsumer.SetStateMachine(stateMachine); - } + void INotifyCompletion.OnCompleted(Action continuation) + => this.continuation = continuation; + } + public sealed class AsyncBenchmarkRunner : IDisposable + where TWorkloadFunc : struct, IFunc + where TOverheadFunc : struct, IFunc + where TAsyncConsumer : IAsyncConsumer, new() + where TAwaiter : ICriticalNotifyCompletion + { private readonly AutoResetValueTaskSource valueTaskSource = new (); - private TBenchmarkFunc benchmarkFunc; - private int state = -1; + private readonly TWorkloadFunc workloadFunc; + private readonly TOverheadFunc overheadFunc; private long repeatsRemaining; - private StartedClock startedClock; - private TAwaiter currentAwaiter; - private Action continuation; + private IClock clock; + private AsyncStateMachineAdvancer workloadAsyncStateMachineAdvancer; + private AsyncStateMachineAdvancer overheadAsyncStateMachineAdvancer; + private bool isDisposed; + + public AsyncBenchmarkRunner(TWorkloadFunc workloadFunc, TOverheadFunc overheadFunc) + { + this.workloadFunc = workloadFunc; + this.overheadFunc = overheadFunc; + } - public AsyncBenchmarkRunner(TBenchmarkFunc benchmarkFunc) + private void MaybeInitializeWorkload() { - this.benchmarkFunc = benchmarkFunc; - // Initialize the state machine and consumer before the actual workload starts. - StateMachine stateMachine = default; - stateMachine.asyncConsumer = new TAsyncConsumer(); - stateMachine.asyncConsumer.CreateAsyncMethodBuilder(); + if (workloadAsyncStateMachineAdvancer != null) + { + return; + } + + // Initialize the state machine and consumer before the workload starts. + workloadAsyncStateMachineAdvancer = new (); + StateMachine stateMachine = default; + stateMachine.consumer = new (); + stateMachine.consumer.CreateAsyncMethodBuilder(); stateMachine.owner = this; - stateMachine.asyncConsumer.Start(ref stateMachine); + stateMachine.stateMachineAdvancer = workloadAsyncStateMachineAdvancer; + stateMachine.func = workloadFunc; + stateMachine.state = -1; + stateMachine.consumer.Start(ref stateMachine); } public ValueTask InvokeWorkload(long repeatCount, IClock clock) { + MaybeInitializeWorkload(); repeatsRemaining = repeatCount; - Action action = continuation; - continuation = null; - startedClock = clock.Start(); - // The continuation callback moves the state machine forward through the builder in the TAsyncConsumer. - action(); + // The clock is started inside the state machine. + this.clock = clock; + workloadAsyncStateMachineAdvancer.Advance(); + this.clock = default; return new ValueTask(valueTaskSource, valueTaskSource.Version); } - void ICriticalNotifyCompletion.UnsafeOnCompleted(Action continuation) - => this.continuation = continuation; + private void MaybeInitializeOverhead() + { + if (overheadAsyncStateMachineAdvancer != null) + { + return; + } - void INotifyCompletion.OnCompleted(Action continuation) - => this.continuation = continuation; + // Initialize the state machine and consumer before the overhead starts. + overheadAsyncStateMachineAdvancer = new (); + StateMachine stateMachine = default; + stateMachine.consumer = new () { asyncConsumer = new () }; + stateMachine.consumer.CreateAsyncMethodBuilder(); + stateMachine.owner = this; + stateMachine.stateMachineAdvancer = overheadAsyncStateMachineAdvancer; + stateMachine.func = overheadFunc; + stateMachine.state = -1; + stateMachine.consumer.Start(ref stateMachine); + } + + public ValueTask InvokeOverhead(long repeatCount, IClock clock) + { + MaybeInitializeOverhead(); + repeatsRemaining = repeatCount; + // The clock is started inside the state machine. + this.clock = clock; + overheadAsyncStateMachineAdvancer.Advance(); + this.clock = default; + return new ValueTask(valueTaskSource, valueTaskSource.Version); + } + + // TODO: make sure Dispose is called. + public void Dispose() + { + // Set the isDisposed flag and advance the state machines to complete the consumers. + isDisposed = true; + workloadAsyncStateMachineAdvancer?.Advance(); + overheadAsyncStateMachineAdvancer?.Advance(); + } + + // C# compiler creates struct state machines in Release mode, so we do the same. + private struct StateMachine : IAsyncStateMachine + where TFunc : struct, IFunc + where TConsumer : IAsyncConsumer, new() + { + internal AsyncBenchmarkRunner owner; + internal AsyncStateMachineAdvancer stateMachineAdvancer; + internal TConsumer consumer; + internal TFunc func; + internal int state; + private StartedClock startedClock; + private TAwaiter currentAwaiter; #if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveOptimization)] + [MethodImpl(MethodImplOptions.AggressiveOptimization)] #endif - private void MoveNext(ref StateMachine stateMachine) - { - try + public void MoveNext() { - if (state < 0) + try { - if (state == -1) + if (state < 0) { - // This is called when we call asyncConsumer.Start, so we just hook up the continuation - // to the owner so the state machine can be moved forward when the benchmark starts. + if (state == -1) + { + // This is called when we call asyncConsumer.Start, so we just hook up the continuation + // to the advancer so the state machine can be moved forward when the benchmark starts. + state = -2; + consumer.AwaitOnCompleted(ref stateMachineAdvancer, ref this); + return; + } + + if (owner.isDisposed) + { + // The owner has been disposed, we complete the consumer. + consumer.SetResult(); + return; + } + + // The benchmark has been started, start the clock. state = 0; - var _this = this; - stateMachine.asyncConsumer.AwaitOnCompleted(ref _this, ref stateMachine); - return; + startedClock = owner.clock.Start(); + goto StartLoop; } - // This has been disposed, we complete the consumer. - stateMachine.asyncConsumer.SetResult(); - return; - } - if (state == 1) - { - state = 0; - stateMachine.asyncConsumer.GetResult(ref currentAwaiter); - } + if (state == 1) + { + state = 0; + GetResult(); + } - while (--repeatsRemaining >= 0) - { - var awaitable = benchmarkFunc.InvokeWorkload(); - currentAwaiter = stateMachine.asyncConsumer.GetAwaiter(ref awaitable); - if (!stateMachine.asyncConsumer.GetIsCompleted(ref currentAwaiter)) + StartLoop: + while (--owner.repeatsRemaining >= 0) { - state = 1; - stateMachine.asyncConsumer.AwaitOnCompleted(ref currentAwaiter, ref stateMachine); - return; + var awaitable = func.Invoke(); + GetAwaiter(ref awaitable); + if (!GetIsCompleted()) + { + state = 1; + consumer.AwaitOnCompleted(ref currentAwaiter, ref this); + return; + } + GetResult(); } - stateMachine.asyncConsumer.GetResult(ref currentAwaiter); } - } - catch (Exception e) - { + catch (Exception e) + { + currentAwaiter = default; + startedClock = default; + owner.valueTaskSource.SetException(e); + return; + } + var clockspan = startedClock.GetElapsed(); currentAwaiter = default; startedClock = default; - valueTaskSource.SetException(e); - return; - } - var clockspan = startedClock.GetElapsed(); - currentAwaiter = default; - startedClock = default; - { - // We hook up the continuation to the owner so the state machine can be moved forward when the next benchmark iteration starts. - stateMachine.asyncConsumer.AwaitOnCompleted(ref stateMachine.owner, ref stateMachine); + state = -2; + { + // We hook up the continuation to the advancer so the state machine can be moved forward when the next benchmark iteration starts. + consumer.AwaitOnCompleted(ref stateMachineAdvancer, ref this); + } + owner.valueTaskSource.SetResult(clockspan); } - valueTaskSource.SetResult(clockspan); + + // Make sure the methods are called without inlining. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private void GetAwaiter(ref TAwaitable awaitable) => currentAwaiter = consumer.GetAwaiter(ref awaitable); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private bool GetIsCompleted() => consumer.GetIsCompleted(ref currentAwaiter); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private void GetResult() => consumer.GetResult(ref currentAwaiter); + + void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) => consumer.SetStateMachine(stateMachine); } - // TODO: make sure Dispose is called. - public void Dispose() + private struct OverheadConsumer : IAsyncConsumer { - benchmarkFunc = default; - Action action = continuation; - continuation = null; - // Set the state and invoke the callback for the state machine to advance to complete the consumer. - state = -2; - action(); + internal TAsyncConsumer asyncConsumer; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CreateAsyncMethodBuilder() + => asyncConsumer.CreateAsyncMethodBuilder(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => asyncConsumer.Start(ref stateMachine); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AwaitOnCompleted(ref TAnyAwaiter awaiter, ref TStateMachine stateMachine) + where TAnyAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + => asyncConsumer.AwaitOnCompleted(ref awaiter, ref stateMachine); + + public void SetResult() + => asyncConsumer.SetResult(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetStateMachine(IAsyncStateMachine stateMachine) + => asyncConsumer.SetStateMachine(stateMachine); + + public TAwaiter GetAwaiter(ref TAwaitable awaitable) + => default; + + public bool GetIsCompleted(ref TAwaiter awaiter) + => true; + + public void GetResult(ref TAwaiter awaiter) { } } -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveOptimization)] -#endif - public ValueTask InvokeOverhead(long repeatCount, IClock clock) + public ValueTask InvokeSingle() { - repeatsRemaining = repeatCount; - TAwaitable value = default; - startedClock = clock.Start(); - try + var asyncConsumer = new TAsyncConsumer(); + var awaitable = workloadFunc.Invoke(); + + if (null == default(TAwaitable) && awaitable is Task task) { - while (--repeatsRemaining >= 0) - { - value = benchmarkFunc.InvokeOverhead(); - } + return new ValueTask(task); } - catch (Exception e) + + if (typeof(TAwaitable) == typeof(ValueTask)) { - currentAwaiter = default; - startedClock = default; - valueTaskSource.SetException(e); - DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); - throw; + return (ValueTask) (object) awaitable; } - var clockspan = startedClock.GetElapsed(); - currentAwaiter = default; - startedClock = default; - return new ValueTask(clockspan); - } - public ValueTask InvokeSingle() - { - var asyncConsumer = new TAsyncConsumer(); - var awaitable = benchmarkFunc.InvokeWorkload(); var awaiter = asyncConsumer.GetAwaiter(ref awaitable); if (asyncConsumer.GetIsCompleted(ref awaiter)) { @@ -183,21 +278,59 @@ public ValueTask InvokeSingle() } return new ValueTask(); } - var taskCompletionSource = new TaskCompletionSource(); - awaiter.UnsafeOnCompleted(() => + + ToValueTaskVoidStateMachine stateMachine = default; + stateMachine.builder = AsyncValueTaskMethodBuilder.Create(); + stateMachine.consumer = asyncConsumer; + stateMachine.awaiter = awaiter; + stateMachine.builder.Start(ref stateMachine); + return stateMachine.builder.Task; + + //var taskCompletionSource = new TaskCompletionSource(); + //awaiter.UnsafeOnCompleted(() => + //{ + // try + // { + // asyncConsumer.GetResult(ref awaiter); + // } + // catch (Exception e) + // { + // taskCompletionSource.SetException(e); + // return; + // } + // taskCompletionSource.SetResult(true); + //}); + //return new ValueTask(taskCompletionSource.Task); + } + + private struct ToValueTaskVoidStateMachine : IAsyncStateMachine + { + internal AsyncValueTaskMethodBuilder builder; + internal TAsyncConsumer consumer; + internal TAwaiter awaiter; + private bool isStarted; + + public void MoveNext() { + if (!isStarted) + { + isStarted = true; + builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); + return; + } + try { - asyncConsumer.GetResult(ref awaiter); + consumer.GetResult(ref awaiter); + builder.SetResult(); } catch (Exception e) { - taskCompletionSource.SetException(e); - return; + builder.SetException(e); } - taskCompletionSource.SetResult(true); - }); - return new ValueTask(taskCompletionSource.Task); + } + + void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) => builder.SetStateMachine(stateMachine); } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Engines/AsyncConsumers.cs b/src/BenchmarkDotNet/Engines/AsyncConsumers.cs index 75f1d34cb7..3cc4c8c5ee 100644 --- a/src/BenchmarkDotNet/Engines/AsyncConsumers.cs +++ b/src/BenchmarkDotNet/Engines/AsyncConsumers.cs @@ -3,6 +3,8 @@ namespace BenchmarkDotNet.Engines { + // TODO: handle return types from GetResult. + public interface IAwaitableConverter where TAwaiter : ICriticalNotifyCompletion { @@ -32,24 +34,24 @@ public interface IAsyncConsumer : IAwaitableConverter { - private AsyncTaskMethodBuilder _builder; + private AsyncTaskMethodBuilder builder; public void CreateAsyncMethodBuilder() - => _builder = AsyncTaskMethodBuilder.Create(); + => builder = AsyncTaskMethodBuilder.Create(); public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine - => _builder.Start(ref stateMachine); + => builder.Start(ref stateMachine); public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine - => _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); public void SetResult() - => _builder.SetResult(); + => builder.SetResult(); public void SetStateMachine(IAsyncStateMachine stateMachine) - => _builder.SetStateMachine(stateMachine); + => builder.SetStateMachine(stateMachine); public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter(ref Task awaitable) => awaitable.ConfigureAwait(false).GetAwaiter(); @@ -63,24 +65,24 @@ public void GetResult(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) public struct TaskConsumer : IAsyncConsumer, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter> { - private AsyncTaskMethodBuilder _builder; + private AsyncTaskMethodBuilder builder; public void CreateAsyncMethodBuilder() - => _builder = AsyncTaskMethodBuilder.Create(); + => builder = AsyncTaskMethodBuilder.Create(); public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine - => _builder.Start(ref stateMachine); + => builder.Start(ref stateMachine); public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine - => _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); public void SetResult() - => _builder.SetResult(); + => builder.SetResult(); public void SetStateMachine(IAsyncStateMachine stateMachine) - => _builder.SetStateMachine(stateMachine); + => builder.SetStateMachine(stateMachine); public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter(ref Task awaitable) => awaitable.ConfigureAwait(false).GetAwaiter(); @@ -94,24 +96,24 @@ public void GetResult(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter await public struct ValueTaskConsumer : IAsyncConsumer { - private AsyncValueTaskMethodBuilder _builder; + private AsyncValueTaskMethodBuilder builder; public void CreateAsyncMethodBuilder() - => _builder = AsyncValueTaskMethodBuilder.Create(); + => builder = AsyncValueTaskMethodBuilder.Create(); public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine - => _builder.Start(ref stateMachine); + => builder.Start(ref stateMachine); public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine - => _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); public void SetResult() - => _builder.SetResult(); + => builder.SetResult(); public void SetStateMachine(IAsyncStateMachine stateMachine) - => _builder.SetStateMachine(stateMachine); + => builder.SetStateMachine(stateMachine); public ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter GetAwaiter(ref ValueTask awaitable) => awaitable.ConfigureAwait(false).GetAwaiter(); @@ -125,24 +127,24 @@ public void GetResult(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaite public struct ValueTaskConsumer : IAsyncConsumer, ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter> { - private AsyncValueTaskMethodBuilder _builder; + private AsyncValueTaskMethodBuilder builder; public void CreateAsyncMethodBuilder() - => _builder = AsyncValueTaskMethodBuilder.Create(); + => builder = AsyncValueTaskMethodBuilder.Create(); public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine - => _builder.Start(ref stateMachine); + => builder.Start(ref stateMachine); public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine - => _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); public void SetResult() - => _builder.SetResult(); + => builder.SetResult(); public void SetStateMachine(IAsyncStateMachine stateMachine) - => _builder.SetStateMachine(stateMachine); + => builder.SetStateMachine(stateMachine); public ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter GetAwaiter(ref ValueTask awaitable) => awaitable.ConfigureAwait(false).GetAwaiter(); diff --git a/src/BenchmarkDotNet/Templates/BenchmarkType.txt b/src/BenchmarkDotNet/Templates/BenchmarkType.txt index ff2469f0e6..d3f4aa7209 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkType.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkType.txt @@ -63,6 +63,7 @@ iterationCleanupAction = $IterationCleanupMethodName$; overheadDelegate = __Overhead; workloadDelegate = $WorkloadMethodDelegate$; + $InitializeAsyncBenchmarkRunnerField$ $InitializeArgumentFields$ } @@ -111,51 +112,66 @@ #if RETURNS_AWAITABLE_$ID$ - private struct BenchmarkFunc : IBenchmarkFunc<$WorkloadMethodReturnType$> + private struct WorkloadFunc : BenchmarkDotNet.Engines.IFunc<$WorkloadMethodReturnType$> { - $WorkloadMethodReturnType$ InvokeWorkload() + private readonly BenchmarkDotNet.Autogenerated.Runnable_$ID$ instance; + + public WorkloadFunc(BenchmarkDotNet.Autogenerated.Runnable_$ID$ instance) { - $LoadArguments$ - return workloadDelegate($PassArguments$); + this.instance = instance; } - $WorkloadMethodReturnType$ InvokeOverhead() + public $WorkloadMethodReturnType$ Invoke() { - $LoadArguments$ - return overheadDelegate($PassArguments$); + return instance.__InvokeWorkload(); + } + } + + private struct OverheadFunc : BenchmarkDotNet.Engines.IFunc<$WorkloadMethodReturnType$> + { + private readonly BenchmarkDotNet.Autogenerated.Runnable_$ID$ instance; + + public OverheadFunc(BenchmarkDotNet.Autogenerated.Runnable_$ID$ instance) + { + this.instance = instance; + } + + public $WorkloadMethodReturnType$ Invoke() + { + return instance.__InvokeOverhead(); } } - private readonly $AsyncBenchmarkRunnerType$ __asyncBenchmarkRunner = new $AsyncBenchmarkRunnerType$(); + private readonly $AsyncBenchmarkRunnerType$ __asyncBenchmarkRunner; + + private $WorkloadMethodReturnType$ __InvokeWorkload() + { + $LoadArguments$ + return workloadDelegate($PassArguments$); + } + + private $WorkloadMethodReturnType$ __InvokeOverhead() + { + $LoadArguments$ + return overheadDelegate($PassArguments$); + } -#if NETCOREAPP3_0_OR_GREATER - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] -#endif // Awaits are not unrolled. private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { return __asyncBenchmarkRunner.InvokeOverhead(invokeCount, clock); } -#if NETCOREAPP3_0_OR_GREATER - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] -#endif private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { return __asyncBenchmarkRunner.InvokeOverhead(invokeCount, clock); } -#if NETCOREAPP3_0_OR_GREATER - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] -#endif private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { return __asyncBenchmarkRunner.InvokeWorkload(invokeCount, clock); } -#if NETCOREAPP3_0_OR_GREATER - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] -#endif private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { return __asyncBenchmarkRunner.InvokeWorkload(invokeCount, clock); diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs index 03b53a5b84..1b47950183 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs @@ -120,17 +120,16 @@ internal class BenchmarkActionAsync : Benc where TAwaiter : ICriticalNotifyCompletion { // IBenchmarkFunc implemented via struct instead of on the class so that it can be inlined. - private readonly struct AsyncBenchmarkFunc : IBenchmarkFunc + private readonly struct AsyncBenchmarkFunc : IFunc { private readonly Func callback; internal AsyncBenchmarkFunc(Func callback) => this.callback = callback; - public TAwaitable InvokeWorkload() => callback(); - public TAwaitable InvokeOverhead() => callback(); + public TAwaitable Invoke() => callback(); } private readonly Func callback; - private readonly AsyncBenchmarkRunner asyncBenchmarkRunner; + private readonly AsyncBenchmarkRunner asyncBenchmarkRunner; public BenchmarkActionAsync(object instance, MethodInfo method, int unrollFactor) { @@ -147,7 +146,7 @@ public BenchmarkActionAsync(object instance, MethodInfo method, int unrollFactor InvokeSingle = InvokeSingleHardcodedOverhead; InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcodedOverhead; } - asyncBenchmarkRunner = new (new AsyncBenchmarkFunc(callback)); + asyncBenchmarkRunner = new (new AsyncBenchmarkFunc(callback), new AsyncBenchmarkFunc(callback)); } private TAwaitable Overhead() => default; From 4242bee121594db67bd2911cc56baac96ead6b9c Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 29 Jun 2023 06:16:15 -0400 Subject: [PATCH 11/13] Split IAsyncConsumer to IAsyncVoidConsumer and IAsyncResultConsumer. Split AsyncBenchmarkRunner workload and overhead. --- src/BenchmarkDotNet/Code/CodeGenerator.cs | 4 +- .../Code/DeclarationsProvider.cs | 45 ++- .../Configs/ConfigExtensions.cs | 5 +- src/BenchmarkDotNet/Configs/ManualConfig.cs | 10 +- .../Engines/AsyncConsumerStateMachine.cs | 356 ++++++++++++------ src/BenchmarkDotNet/Engines/AsyncConsumers.cs | 61 +-- .../Templates/BenchmarkType.txt | 26 +- .../BenchmarkActionFactory_Implementations.cs | 86 ++++- 8 files changed, 413 insertions(+), 180 deletions(-) diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs index d97412d34c..bf0d86806c 100644 --- a/src/BenchmarkDotNet/Code/CodeGenerator.cs +++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs @@ -52,8 +52,8 @@ internal static string Generate(BuildPartition buildPartition) .Replace("$WorkloadMethodReturnType$", provider.WorkloadMethodReturnTypeName) .Replace("$WorkloadMethodReturnTypeModifiers$", provider.WorkloadMethodReturnTypeModifiers) .Replace("$OverheadMethodReturnTypeName$", provider.OverheadMethodReturnTypeName) - .Replace("$AsyncBenchmarkRunnerType$", provider.GetAsyncBenchmarkRunnerTypeName(buildInfo.Id)) - .Replace("$InitializeAsyncBenchmarkRunnerField$", provider.GetInitializeAsyncBenchmarkRunnerField(buildInfo.Id)) + .Replace("$AsyncBenchmarkRunnerFieldDeclarations$", provider.GetAsyncBenchmarkRunnerFieldDeclarations(buildInfo.Id)) + .Replace("$InitializeAsyncBenchmarkRunnerFields$", provider.GetInitializeAsyncBenchmarkRunnerFields(buildInfo.Id)) .Replace("$GlobalSetupMethodName$", provider.GlobalSetupMethodName) .Replace("$GlobalCleanupMethodName$", provider.GlobalCleanupMethodName) .Replace("$IterationSetupMethodName$", provider.IterationSetupMethodName) diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs index 3054ff7652..da81645f69 100644 --- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs +++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs @@ -45,9 +45,9 @@ internal abstract class DeclarationsProvider public string OverheadMethodReturnTypeName => OverheadMethodReturnType.GetCorrectCSharpTypeName(); - public virtual string GetAsyncBenchmarkRunnerTypeName(BenchmarkId id) => null; + public virtual string GetAsyncBenchmarkRunnerFieldDeclarations(BenchmarkId id) => null; - public virtual string GetInitializeAsyncBenchmarkRunnerField(BenchmarkId id) => null; + public virtual string GetInitializeAsyncBenchmarkRunnerFields(BenchmarkId id) => null; public virtual void OverrideUnrollFactor(BenchmarkCase benchmarkCase) { } @@ -158,24 +158,49 @@ internal class AsyncDeclarationsProvider : DeclarationsProvider public override string OverheadImplementation => $"return default({OverheadMethodReturnType.GetCorrectCSharpTypeName()});"; - protected override Type OverheadMethodReturnType => WorkloadMethodReturnType; + protected override Type OverheadMethodReturnType => typeof(EmptyAwaiter); // we return this simple type because creating bigger ValueType could take longer than benchmarked method itself private string GetRunnableName(BenchmarkId id) => $"BenchmarkDotNet.Autogenerated.Runnable_{id}"; - public override string GetAsyncBenchmarkRunnerTypeName(BenchmarkId id) + private (string workloadRunnerTypeName, string overheadRunnerTypeName) GetAsyncBenchmarkRunnerTypeNames(BenchmarkId id) { string consumerTypeName = asyncConsumerType.GetCorrectCSharpTypeName(); - string awaiterTypeName = asyncConsumerType.GetInterfaces() - .First(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncConsumer<,>)) - .GetGenericArguments()[1].GetCorrectCSharpTypeName(); + + var asyncConsumerInterfaceType = asyncConsumerType.GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncVoidConsumer<,>)); + bool isVoidConsumer = asyncConsumerInterfaceType?.GetGenericArguments()[0] == WorkloadMethodReturnType; + if (!isVoidConsumer) + { + asyncConsumerInterfaceType = asyncConsumerType.GetInterfaces().First(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncResultConsumer<,,>)); + } + + Type[] genericArguments = asyncConsumerInterfaceType.GetGenericArguments(); + string awaiterTypeName = genericArguments[1].GetCorrectCSharpTypeName(); + + string appendResultType = isVoidConsumer ? string.Empty : $", {genericArguments[2].GetCorrectCSharpTypeName()}"; + string runnableName = GetRunnableName(id); - return $"BenchmarkDotNet.Engines.AsyncBenchmarkRunner<{runnableName}.WorkloadFunc, {runnableName}.OverheadFunc, {consumerTypeName}, {WorkloadMethodReturnTypeName}, {awaiterTypeName}>"; + + return ( + $"BenchmarkDotNet.Engines.AsyncWorkloadRunner<{runnableName}.WorkloadFunc, {consumerTypeName}, {WorkloadMethodReturnTypeName}, {awaiterTypeName}{appendResultType}>", + $"BenchmarkDotNet.Engines.AsyncOverheadRunner<{runnableName}.OverheadFunc, {consumerTypeName}, {WorkloadMethodReturnTypeName}, {awaiterTypeName}{appendResultType}>" + ); + } + + public override string GetAsyncBenchmarkRunnerFieldDeclarations(BenchmarkId id) + { + var (workloadRunnerTypeName, overheadRunnerTypeName) = GetAsyncBenchmarkRunnerTypeNames(id); + + return $"private readonly {workloadRunnerTypeName} __asyncWorkloadRunner;" + Environment.NewLine + + $" private readonly {overheadRunnerTypeName} __asyncOverheadRunner;"; } - public override string GetInitializeAsyncBenchmarkRunnerField(BenchmarkId id) + public override string GetInitializeAsyncBenchmarkRunnerFields(BenchmarkId id) { + var (workloadRunnerTypeName, overheadRunnerTypeName) = GetAsyncBenchmarkRunnerTypeNames(id); string runnableName = GetRunnableName(id); - return $"__asyncBenchmarkRunner = new {GetAsyncBenchmarkRunnerTypeName(id)}(new {runnableName}.WorkloadFunc(this), new {runnableName}.OverheadFunc(this));"; + + return $"__asyncWorkloadRunner = new {workloadRunnerTypeName}(new {runnableName}.WorkloadFunc(this));" + + $" __asyncOverheadRunner = new {overheadRunnerTypeName}(new {runnableName}.OverheadFunc(this));"; } public override void OverrideUnrollFactor(BenchmarkCase benchmarkCase) => benchmarkCase.ForceUnrollFactorForAsync(); diff --git a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs index e64d326daf..e0736eaaf1 100644 --- a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs +++ b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs @@ -116,10 +116,7 @@ public static class ConfigExtensions [PublicAPI] public static ManualConfig HideColumns(this IConfig config, params string[] columnNames) => config.With(c => c.HideColumns(columnNames)); [PublicAPI] public static ManualConfig HideColumns(this IConfig config, params IColumn[] columns) => config.With(c => c.HideColumns(columns)); [PublicAPI] public static ManualConfig HideColumns(this IConfig config, params IColumnHidingRule[] rules) => config.With(c => c.HideColumns(rules)); - [PublicAPI] public static ManualConfig AddAsyncConsumer(this IConfig config) - where TAwaiter : ICriticalNotifyCompletion - where TAsyncConsumer : struct, IAsyncConsumer - => config.With(c => c.AddAsyncConsumer()); + [PublicAPI] public static ManualConfig AddAsyncConsumer(this IConfig config, Type awaitableType, Type asyncConsumerType) => config.With(c => c.AddAsyncConsumer(awaitableType, asyncConsumerType)); public static ImmutableConfig CreateImmutableConfig(this IConfig config) => ImmutableConfigBuilder.Create(config); diff --git a/src/BenchmarkDotNet/Configs/ManualConfig.cs b/src/BenchmarkDotNet/Configs/ManualConfig.cs index 93d0676da4..b10a8634fd 100644 --- a/src/BenchmarkDotNet/Configs/ManualConfig.cs +++ b/src/BenchmarkDotNet/Configs/ManualConfig.cs @@ -118,13 +118,16 @@ public ManualConfig WithBuildTimeout(TimeSpan buildTimeout) return this; } + // TODO: only pass in asyncConsumerType, get awaitableType from IAsyncConsumer public ManualConfig AddAsyncConsumer(Type awaitableType, Type asyncConsumerType) { // Validate types - if (asyncConsumerType.IsNotPublic || (!asyncConsumerType.IsValueType && asyncConsumerType.GetConstructor(Array.Empty()) == null)) + bool isPublic = asyncConsumerType.IsPublic || asyncConsumerType.IsNestedPublic; + if (!isPublic || (!asyncConsumerType.IsValueType && asyncConsumerType.GetConstructor(Array.Empty()) == null)) { throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] is not a public struct, or a public class with a public, parameterless constructor."); } + // TODO: handle multiple generics, verify that the open generics are contained in TAwaitable. https://stackoverflow.com/questions/65725729/why-is-checking-equality-of-open-generic-types-inconsistent bool consumerIsOpenGeneric = asyncConsumerType.IsGenericTypeDefinition; bool awaitableisOpenGeneric = awaitableType.IsGenericTypeDefinition; if (consumerIsOpenGeneric != awaitableisOpenGeneric) @@ -150,8 +153,9 @@ public ManualConfig AddAsyncConsumer(Type awaitableType, Type asyncConsumerType) // TODO: handle partially closed types. var closedAwaitableType = awaitableisOpenGeneric ? awaitableType.MakeGenericType(typeof(int)) : awaitableType; var closedConsumerType = consumerIsOpenGeneric ? asyncConsumerType.MakeGenericType(typeof(int)) : asyncConsumerType; - var iAsyncConsumerType = closedConsumerType.GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncConsumer<,>)) - ?? throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] does not implement IAsyncConsumer."); + var iAsyncConsumerType = closedConsumerType.GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncVoidConsumer<,>)) + ?? closedConsumerType.GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncResultConsumer<,,>)) + ?? throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] does not implement IAsyncVoidConsumer or IAsyncResultConsumer."); if (iAsyncConsumerType.GetGenericArguments()[0] != closedAwaitableType) { throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] does not implement IAsyncConsumer with the expected TAwaitable type [{awaitableType}]."); diff --git a/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs b/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs index a0e71c25c3..a33d2a636f 100644 --- a/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs +++ b/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs @@ -31,104 +31,83 @@ void INotifyCompletion.OnCompleted(Action continuation) => this.continuation = continuation; } - public sealed class AsyncBenchmarkRunner : IDisposable - where TWorkloadFunc : struct, IFunc - where TOverheadFunc : struct, IFunc - where TAsyncConsumer : IAsyncConsumer, new() + public struct EmptyAwaiter : ICriticalNotifyCompletion + { + void ICriticalNotifyCompletion.UnsafeOnCompleted(Action continuation) + => throw new NotImplementedException(); + + void INotifyCompletion.OnCompleted(Action continuation) + => throw new NotImplementedException(); + } + + public abstract class AsyncBenchmarkRunner : IDisposable + { + public abstract ValueTask Invoke(long repeatCount, IClock clock); + public abstract ValueTask InvokeSingle(); + public abstract void Dispose(); + } + + public abstract class AsyncBenchmarkRunner : AsyncBenchmarkRunner + where TFunc : struct, IFunc + where TAsyncConsumer : IAsyncVoidConsumer, new() where TAwaiter : ICriticalNotifyCompletion { private readonly AutoResetValueTaskSource valueTaskSource = new (); - private readonly TWorkloadFunc workloadFunc; - private readonly TOverheadFunc overheadFunc; + private readonly TFunc func; private long repeatsRemaining; private IClock clock; - private AsyncStateMachineAdvancer workloadAsyncStateMachineAdvancer; - private AsyncStateMachineAdvancer overheadAsyncStateMachineAdvancer; + private AsyncStateMachineAdvancer asyncStateMachineAdvancer; private bool isDisposed; - public AsyncBenchmarkRunner(TWorkloadFunc workloadFunc, TOverheadFunc overheadFunc) + public AsyncBenchmarkRunner(TFunc func) { - this.workloadFunc = workloadFunc; - this.overheadFunc = overheadFunc; + this.func = func; } - private void MaybeInitializeWorkload() + private void MaybeInitializeStateMachine() { - if (workloadAsyncStateMachineAdvancer != null) + if (asyncStateMachineAdvancer != null) { return; } // Initialize the state machine and consumer before the workload starts. - workloadAsyncStateMachineAdvancer = new (); - StateMachine stateMachine = default; + asyncStateMachineAdvancer = new (); + StateMachine stateMachine = default; stateMachine.consumer = new (); stateMachine.consumer.CreateAsyncMethodBuilder(); stateMachine.owner = this; - stateMachine.stateMachineAdvancer = workloadAsyncStateMachineAdvancer; - stateMachine.func = workloadFunc; - stateMachine.state = -1; - stateMachine.consumer.Start(ref stateMachine); - } - - public ValueTask InvokeWorkload(long repeatCount, IClock clock) - { - MaybeInitializeWorkload(); - repeatsRemaining = repeatCount; - // The clock is started inside the state machine. - this.clock = clock; - workloadAsyncStateMachineAdvancer.Advance(); - this.clock = default; - return new ValueTask(valueTaskSource, valueTaskSource.Version); - } - - private void MaybeInitializeOverhead() - { - if (overheadAsyncStateMachineAdvancer != null) - { - return; - } - - // Initialize the state machine and consumer before the overhead starts. - overheadAsyncStateMachineAdvancer = new (); - StateMachine stateMachine = default; - stateMachine.consumer = new () { asyncConsumer = new () }; - stateMachine.consumer.CreateAsyncMethodBuilder(); - stateMachine.owner = this; - stateMachine.stateMachineAdvancer = overheadAsyncStateMachineAdvancer; - stateMachine.func = overheadFunc; + stateMachine.stateMachineAdvancer = asyncStateMachineAdvancer; + stateMachine.func = func; stateMachine.state = -1; stateMachine.consumer.Start(ref stateMachine); } - public ValueTask InvokeOverhead(long repeatCount, IClock clock) + public override ValueTask Invoke(long repeatCount, IClock clock) { - MaybeInitializeOverhead(); + MaybeInitializeStateMachine(); repeatsRemaining = repeatCount; // The clock is started inside the state machine. this.clock = clock; - overheadAsyncStateMachineAdvancer.Advance(); + asyncStateMachineAdvancer.Advance(); this.clock = default; return new ValueTask(valueTaskSource, valueTaskSource.Version); } // TODO: make sure Dispose is called. - public void Dispose() + public override void Dispose() { - // Set the isDisposed flag and advance the state machines to complete the consumers. + // Set the isDisposed flag and advance the state machine to complete the consumer. isDisposed = true; - workloadAsyncStateMachineAdvancer?.Advance(); - overheadAsyncStateMachineAdvancer?.Advance(); + asyncStateMachineAdvancer?.Advance(); } // C# compiler creates struct state machines in Release mode, so we do the same. - private struct StateMachine : IAsyncStateMachine - where TFunc : struct, IFunc - where TConsumer : IAsyncConsumer, new() + private struct StateMachine : IAsyncStateMachine { - internal AsyncBenchmarkRunner owner; + internal AsyncBenchmarkRunner owner; internal AsyncStateMachineAdvancer stateMachineAdvancer; - internal TConsumer consumer; + internal TAsyncConsumer consumer; internal TFunc func; internal int state; private StartedClock startedClock; @@ -168,21 +147,23 @@ public void MoveNext() if (state == 1) { state = 0; - GetResult(); + consumer.GetResult(ref currentAwaiter); + currentAwaiter = default; } StartLoop: while (--owner.repeatsRemaining >= 0) { var awaitable = func.Invoke(); - GetAwaiter(ref awaitable); - if (!GetIsCompleted()) + var awaiter = consumer.GetAwaiter(ref awaitable); + if (!consumer.GetIsCompleted(ref awaiter)) { state = 1; + currentAwaiter = awaiter; consumer.AwaitOnCompleted(ref currentAwaiter, ref this); return; } - GetResult(); + consumer.GetResult(ref awaiter); } } catch (Exception e) @@ -203,57 +184,13 @@ public void MoveNext() owner.valueTaskSource.SetResult(clockspan); } - // Make sure the methods are called without inlining. - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - private void GetAwaiter(ref TAwaitable awaitable) => currentAwaiter = consumer.GetAwaiter(ref awaitable); - - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - private bool GetIsCompleted() => consumer.GetIsCompleted(ref currentAwaiter); - - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - private void GetResult() => consumer.GetResult(ref currentAwaiter); - void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) => consumer.SetStateMachine(stateMachine); } - private struct OverheadConsumer : IAsyncConsumer - { - internal TAsyncConsumer asyncConsumer; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void CreateAsyncMethodBuilder() - => asyncConsumer.CreateAsyncMethodBuilder(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine - => asyncConsumer.Start(ref stateMachine); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AwaitOnCompleted(ref TAnyAwaiter awaiter, ref TStateMachine stateMachine) - where TAnyAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine - => asyncConsumer.AwaitOnCompleted(ref awaiter, ref stateMachine); - - public void SetResult() - => asyncConsumer.SetResult(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetStateMachine(IAsyncStateMachine stateMachine) - => asyncConsumer.SetStateMachine(stateMachine); - - public TAwaiter GetAwaiter(ref TAwaitable awaitable) - => default; - - public bool GetIsCompleted(ref TAwaiter awaiter) - => true; - - public void GetResult(ref TAwaiter awaiter) { } - } - - public ValueTask InvokeSingle() + public override ValueTask InvokeSingle() { var asyncConsumer = new TAsyncConsumer(); - var awaitable = workloadFunc.Invoke(); + var awaitable = func.Invoke(); if (null == default(TAwaitable) && awaitable is Task task) { @@ -286,6 +223,7 @@ public ValueTask InvokeSingle() stateMachine.builder.Start(ref stateMachine); return stateMachine.builder.Task; + // TODO: remove the commented code after the statemachine is verified working. //var taskCompletionSource = new TaskCompletionSource(); //awaiter.UnsafeOnCompleted(() => //{ @@ -333,4 +271,204 @@ public void MoveNext() void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) => builder.SetStateMachine(stateMachine); } } + + public sealed class AsyncWorkloadRunner : AsyncBenchmarkRunner.AsyncConsumer, TAwaitable, TAwaiter> + where TFunc : struct, IFunc + where TAsyncConsumer : IAsyncVoidConsumer, new() + where TAwaiter : ICriticalNotifyCompletion + { + public AsyncWorkloadRunner(TFunc func) : base(func) { } + + public struct AsyncConsumer : IAsyncVoidConsumer + { + internal TAsyncConsumer asyncConsumer; + + public void CreateAsyncMethodBuilder() + { + asyncConsumer = new (); + asyncConsumer.CreateAsyncMethodBuilder(); + } + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => asyncConsumer.Start(ref stateMachine); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AwaitOnCompleted(ref TAnyAwaiter awaiter, ref TStateMachine stateMachine) + where TAnyAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + => asyncConsumer.AwaitOnCompleted(ref awaiter, ref stateMachine); + + public void SetResult() + => asyncConsumer.SetResult(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetStateMachine(IAsyncStateMachine stateMachine) + => asyncConsumer.SetStateMachine(stateMachine); + + // Make sure the methods are called without inlining. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public TAwaiter GetAwaiter(ref TAwaitable awaitable) + => asyncConsumer.GetAwaiter(ref awaitable); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public bool GetIsCompleted(ref TAwaiter awaiter) + => asyncConsumer.GetIsCompleted(ref awaiter); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public void GetResult(ref TAwaiter awaiter) + => asyncConsumer.GetResult(ref awaiter); + } + } + + public sealed class AsyncOverheadRunner : AsyncBenchmarkRunner.AsyncConsumer, EmptyAwaiter, EmptyAwaiter> + where TFunc : struct, IFunc + where TAsyncConsumer : IAsyncVoidConsumer, new() + where TAwaiter : ICriticalNotifyCompletion + { + public AsyncOverheadRunner(TFunc func) : base(func) { } + + public struct AsyncConsumer : IAsyncVoidConsumer + { + internal TAsyncConsumer asyncConsumer; + + public void CreateAsyncMethodBuilder() + { + asyncConsumer = new (); + asyncConsumer.CreateAsyncMethodBuilder(); + } + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => asyncConsumer.Start(ref stateMachine); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AwaitOnCompleted(ref TAnyAwaiter awaiter, ref TStateMachine stateMachine) + where TAnyAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + => asyncConsumer.AwaitOnCompleted(ref awaiter, ref stateMachine); + + public void SetResult() + => asyncConsumer.SetResult(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetStateMachine(IAsyncStateMachine stateMachine) + => asyncConsumer.SetStateMachine(stateMachine); + + // Make sure the methods are called without inlining. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public EmptyAwaiter GetAwaiter(ref EmptyAwaiter awaitable) + => default; + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public bool GetIsCompleted(ref EmptyAwaiter awaiter) + => true; + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public void GetResult(ref EmptyAwaiter awaiter) { } + } + } + + public sealed class AsyncWorkloadRunner : AsyncBenchmarkRunner.AsyncConsumer, TAwaitable, TAwaiter> + where TFunc : struct, IFunc + where TAsyncConsumer : IAsyncResultConsumer, new() + where TAwaiter : ICriticalNotifyCompletion + { + public AsyncWorkloadRunner(TFunc func) : base(func) { } + + public struct AsyncConsumer : IAsyncVoidConsumer + { + internal TAsyncConsumer asyncConsumer; + + public void CreateAsyncMethodBuilder() + { + asyncConsumer = new (); + asyncConsumer.CreateAsyncMethodBuilder(); + } + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => asyncConsumer.Start(ref stateMachine); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AwaitOnCompleted(ref TAnyAwaiter awaiter, ref TStateMachine stateMachine) + where TAnyAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + => asyncConsumer.AwaitOnCompleted(ref awaiter, ref stateMachine); + + public void SetResult() + => asyncConsumer.SetResult(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetStateMachine(IAsyncStateMachine stateMachine) + => asyncConsumer.SetStateMachine(stateMachine); + + // Make sure the methods are called without inlining. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public TAwaiter GetAwaiter(ref TAwaitable awaitable) + => asyncConsumer.GetAwaiter(ref awaitable); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public bool GetIsCompleted(ref TAwaiter awaiter) + => asyncConsumer.GetIsCompleted(ref awaiter); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GetResult(ref TAwaiter awaiter) + => GetResultNoInlining(ref awaiter); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private TResult GetResultNoInlining(ref TAwaiter awaiter) + => asyncConsumer.GetResult(ref awaiter); + } + } + + public sealed class AsyncOverheadRunner : AsyncBenchmarkRunner.AsyncConsumer, EmptyAwaiter, EmptyAwaiter> + where TFunc : struct, IFunc + where TAsyncConsumer : IAsyncResultConsumer, new() + where TAwaiter : ICriticalNotifyCompletion + { + public AsyncOverheadRunner(TFunc func) : base(func) { } + + public struct AsyncConsumer : IAsyncVoidConsumer + { + internal TAsyncConsumer asyncConsumer; + + public void CreateAsyncMethodBuilder() + { + asyncConsumer = new (); + asyncConsumer.CreateAsyncMethodBuilder(); + } + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => asyncConsumer.Start(ref stateMachine); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AwaitOnCompleted(ref TAnyAwaiter awaiter, ref TStateMachine stateMachine) + where TAnyAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + => asyncConsumer.AwaitOnCompleted(ref awaiter, ref stateMachine); + + public void SetResult() + => asyncConsumer.SetResult(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetStateMachine(IAsyncStateMachine stateMachine) + => asyncConsumer.SetStateMachine(stateMachine); + + // Make sure the methods are called without inlining. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public EmptyAwaiter GetAwaiter(ref EmptyAwaiter awaitable) + => default; + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public bool GetIsCompleted(ref EmptyAwaiter awaiter) + => true; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GetResult(ref EmptyAwaiter awaiter) + { + GetResultNoInlining(ref awaiter); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private void GetResultNoInlining(ref EmptyAwaiter awaiter) { } + } + } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Engines/AsyncConsumers.cs b/src/BenchmarkDotNet/Engines/AsyncConsumers.cs index 3cc4c8c5ee..092c23d6d6 100644 --- a/src/BenchmarkDotNet/Engines/AsyncConsumers.cs +++ b/src/BenchmarkDotNet/Engines/AsyncConsumers.cs @@ -3,16 +3,25 @@ namespace BenchmarkDotNet.Engines { - // TODO: handle return types from GetResult. - public interface IAwaitableConverter where TAwaiter : ICriticalNotifyCompletion { public TAwaiter GetAwaiter(ref TAwaitable awaitable); public bool GetIsCompleted(ref TAwaiter awaiter); + } + + public interface IAwaitableVoidConverter : IAwaitableConverter + where TAwaiter : ICriticalNotifyCompletion + { public void GetResult(ref TAwaiter awaiter); } + public interface IAwaitableResultConverter : IAwaitableConverter + where TAwaiter : ICriticalNotifyCompletion + { + public TResult GetResult(ref TAwaiter awaiter); + } + public interface IAsyncMethodBuilder { public void CreateAsyncMethodBuilder(); @@ -25,19 +34,27 @@ public void AwaitOnCompleted(ref TAwaiter awaiter, ref public void SetResult(); } - public interface IAsyncConsumer : IAwaitableConverter, IAsyncMethodBuilder + public interface IAsyncVoidConsumer : IAwaitableVoidConverter, IAsyncMethodBuilder where TAwaiter : ICriticalNotifyCompletion { } + public interface IAsyncResultConsumer : IAwaitableResultConverter, IAsyncMethodBuilder + where TAwaiter : ICriticalNotifyCompletion + { + } + + // We use a type that users cannot access to prevent the async method builder from being jitted with the user's type, in case the benchmark is ran with ColdStart. + internal struct UnusedStruct { } + // We use ConfigureAwait(false) to prevent dead-locks with InProcess toolchains (it could be ran on a thread with a SynchronizationContext). - // Using struct rather than class forces the JIT to generate specialized code that can be inlined. - public struct TaskConsumer : IAsyncConsumer + // Using struct rather than class forces the JIT to generate specialized code that can be inlined, and avoids an extra allocation. + public struct TaskConsumer : IAsyncVoidConsumer { - private AsyncTaskMethodBuilder builder; + private AsyncTaskMethodBuilder builder; public void CreateAsyncMethodBuilder() - => builder = AsyncTaskMethodBuilder.Create(); + => builder = AsyncTaskMethodBuilder.Create(); public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => builder.Start(ref stateMachine); @@ -48,7 +65,7 @@ public void AwaitOnCompleted(ref TAwaiter awaiter, ref => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); public void SetResult() - => builder.SetResult(); + => builder.SetResult(default); public void SetStateMachine(IAsyncStateMachine stateMachine) => builder.SetStateMachine(stateMachine); @@ -63,12 +80,12 @@ public void GetResult(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => awaiter.GetResult(); } - public struct TaskConsumer : IAsyncConsumer, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter> + public struct TaskConsumer : IAsyncResultConsumer, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter, T> { - private AsyncTaskMethodBuilder builder; + private AsyncTaskMethodBuilder builder; public void CreateAsyncMethodBuilder() - => builder = AsyncTaskMethodBuilder.Create(); + => builder = AsyncTaskMethodBuilder.Create(); public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => builder.Start(ref stateMachine); @@ -79,7 +96,7 @@ public void AwaitOnCompleted(ref TAwaiter awaiter, ref => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); public void SetResult() - => builder.SetResult(); + => builder.SetResult(default); public void SetStateMachine(IAsyncStateMachine stateMachine) => builder.SetStateMachine(stateMachine); @@ -90,16 +107,16 @@ public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter(ref Task a public bool GetIsCompleted(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => awaiter.IsCompleted; - public void GetResult(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) + public T GetResult(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => awaiter.GetResult(); } - public struct ValueTaskConsumer : IAsyncConsumer + public struct ValueTaskConsumer : IAsyncVoidConsumer { - private AsyncValueTaskMethodBuilder builder; + private AsyncValueTaskMethodBuilder builder; public void CreateAsyncMethodBuilder() - => builder = AsyncValueTaskMethodBuilder.Create(); + => builder = AsyncValueTaskMethodBuilder.Create(); public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => builder.Start(ref stateMachine); @@ -110,7 +127,7 @@ public void AwaitOnCompleted(ref TAwaiter awaiter, ref => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); public void SetResult() - => builder.SetResult(); + => builder.SetResult(default); public void SetStateMachine(IAsyncStateMachine stateMachine) => builder.SetStateMachine(stateMachine); @@ -125,12 +142,12 @@ public void GetResult(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaite => awaiter.GetResult(); } - public struct ValueTaskConsumer : IAsyncConsumer, ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter> + public struct ValueTaskConsumer : IAsyncResultConsumer, ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter, T> { - private AsyncValueTaskMethodBuilder builder; + private AsyncValueTaskMethodBuilder builder; public void CreateAsyncMethodBuilder() - => builder = AsyncValueTaskMethodBuilder.Create(); + => builder = AsyncValueTaskMethodBuilder.Create(); public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => builder.Start(ref stateMachine); @@ -141,7 +158,7 @@ public void AwaitOnCompleted(ref TAwaiter awaiter, ref => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); public void SetResult() - => builder.SetResult(); + => builder.SetResult(default); public void SetStateMachine(IAsyncStateMachine stateMachine) => builder.SetStateMachine(stateMachine); @@ -152,7 +169,7 @@ public ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter GetAwaiter(ref public bool GetIsCompleted(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) => awaiter.IsCompleted; - public void GetResult(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) + public T GetResult(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) => awaiter.GetResult(); } } diff --git a/src/BenchmarkDotNet/Templates/BenchmarkType.txt b/src/BenchmarkDotNet/Templates/BenchmarkType.txt index d3f4aa7209..c17a7a5dc8 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkType.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkType.txt @@ -63,7 +63,7 @@ iterationCleanupAction = $IterationCleanupMethodName$; overheadDelegate = __Overhead; workloadDelegate = $WorkloadMethodDelegate$; - $InitializeAsyncBenchmarkRunnerField$ + $InitializeAsyncBenchmarkRunnerFields$ $InitializeArgumentFields$ } @@ -121,13 +121,14 @@ this.instance = instance; } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public $WorkloadMethodReturnType$ Invoke() { return instance.__InvokeWorkload(); } } - private struct OverheadFunc : BenchmarkDotNet.Engines.IFunc<$WorkloadMethodReturnType$> + private struct OverheadFunc : BenchmarkDotNet.Engines.IFunc<$OverheadMethodReturnTypeName$> { private readonly BenchmarkDotNet.Autogenerated.Runnable_$ID$ instance; @@ -136,22 +137,27 @@ this.instance = instance; } - public $WorkloadMethodReturnType$ Invoke() + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public $OverheadMethodReturnTypeName$ Invoke() { return instance.__InvokeOverhead(); } } - private readonly $AsyncBenchmarkRunnerType$ __asyncBenchmarkRunner; + $AsyncBenchmarkRunnerFieldDeclarations$ + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] private $WorkloadMethodReturnType$ __InvokeWorkload() { +// TODO: pass arguments directly. $LoadArguments$ return workloadDelegate($PassArguments$); } - - private $WorkloadMethodReturnType$ __InvokeOverhead() + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private $OverheadMethodReturnTypeName$ __InvokeOverhead() { +// TODO: pass arguments directly. $LoadArguments$ return overheadDelegate($PassArguments$); } @@ -159,22 +165,22 @@ // Awaits are not unrolled. private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { - return __asyncBenchmarkRunner.InvokeOverhead(invokeCount, clock); + return __asyncOverheadRunner.Invoke(invokeCount, clock); } private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { - return __asyncBenchmarkRunner.InvokeOverhead(invokeCount, clock); + return __asyncOverheadRunner.Invoke(invokeCount, clock); } private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { - return __asyncBenchmarkRunner.InvokeWorkload(invokeCount, clock); + return __asyncWorkloadRunner.Invoke(invokeCount, clock); } private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { - return __asyncBenchmarkRunner.InvokeWorkload(invokeCount, clock); + return __asyncWorkloadRunner.Invoke(invokeCount, clock); } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs index 1b47950183..18dba406f4 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs @@ -116,55 +116,101 @@ internal static BenchmarkActionBase Create(Type consumerType, Type awaitableType } internal class BenchmarkActionAsync : BenchmarkActionBase - where TAsyncConsumer : struct, IAsyncConsumer + where TAsyncConsumer : struct, IAsyncVoidConsumer where TAwaiter : ICriticalNotifyCompletion { - // IBenchmarkFunc implemented via struct instead of on the class so that it can be inlined. - private readonly struct AsyncBenchmarkFunc : IFunc + private readonly struct WorkloadFunc : IFunc { private readonly Func callback; - internal AsyncBenchmarkFunc(Func callback) => this.callback = callback; + internal WorkloadFunc(Func callback) => this.callback = callback; public TAwaitable Invoke() => callback(); } - private readonly Func callback; - private readonly AsyncBenchmarkRunner asyncBenchmarkRunner; + private readonly struct OverheadFunc : IFunc + { + private readonly Func callback; + + internal OverheadFunc(Func callback) => this.callback = callback; + public EmptyAwaiter Invoke() => callback(); + } + + private readonly AsyncBenchmarkRunner asyncBenchmarkRunner; public BenchmarkActionAsync(object instance, MethodInfo method, int unrollFactor) { bool isIdle = method == null; if (!isIdle) { - callback = CreateWorkload>(instance, method); + var callback = CreateWorkload>(instance, method); + asyncBenchmarkRunner = new AsyncWorkloadRunner(new WorkloadFunc(callback)); InvokeSingle = InvokeSingleHardcoded; InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; } else { - callback = Overhead; - InvokeSingle = InvokeSingleHardcodedOverhead; - InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcodedOverhead; + asyncBenchmarkRunner = new AsyncOverheadRunner(new OverheadFunc(Overhead)); + InvokeSingle = InvokeSingleHardcoded; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; } - asyncBenchmarkRunner = new (new AsyncBenchmarkFunc(callback), new AsyncBenchmarkFunc(callback)); } - private TAwaitable Overhead() => default; + private EmptyAwaiter Overhead() => default; + + protected virtual ValueTask InvokeSingleHardcoded() + => asyncBenchmarkRunner.InvokeSingle(); + + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + => asyncBenchmarkRunner.Invoke(repeatCount, clock); + } - private ValueTask InvokeSingleHardcodedOverhead() + internal class BenchmarkActionAsync : BenchmarkActionBase + where TAsyncConsumer : struct, IAsyncResultConsumer + where TAwaiter : ICriticalNotifyCompletion + { + private readonly struct WorkloadFunc : IFunc { - callback(); - return new ValueTask(); + private readonly Func callback; + + internal WorkloadFunc(Func callback) => this.callback = callback; + public TAwaitable Invoke() => callback(); + } + + private readonly struct OverheadFunc : IFunc + { + private readonly Func callback; + + internal OverheadFunc(Func callback) => this.callback = callback; + public EmptyAwaiter Invoke() => callback(); + } + + private readonly AsyncBenchmarkRunner asyncBenchmarkRunner; + + public BenchmarkActionAsync(object instance, MethodInfo method, int unrollFactor) + { + bool isIdle = method == null; + if (!isIdle) + { + var callback = CreateWorkload>(instance, method); + asyncBenchmarkRunner = new AsyncWorkloadRunner(new WorkloadFunc(callback)); + InvokeSingle = InvokeSingleHardcoded; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; + } + else + { + asyncBenchmarkRunner = new AsyncOverheadRunner(new OverheadFunc(Overhead)); + InvokeSingle = InvokeSingleHardcoded; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; + } } - private ValueTask InvokeNoUnrollHardcodedOverhead(long repeatCount, IClock clock) - => asyncBenchmarkRunner.InvokeOverhead(repeatCount, clock); + private EmptyAwaiter Overhead() => default; protected virtual ValueTask InvokeSingleHardcoded() => asyncBenchmarkRunner.InvokeSingle(); private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) - => asyncBenchmarkRunner.InvokeWorkload(repeatCount, clock); + => asyncBenchmarkRunner.Invoke(repeatCount, clock); } internal class BenchmarkActionTask : BenchmarkActionAsync @@ -174,7 +220,7 @@ public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) } } - internal class BenchmarkActionTask : BenchmarkActionAsync, Task, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter> + internal class BenchmarkActionTask : BenchmarkActionAsync, Task, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter, T> { public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) : base(instance, method, unrollFactor) { @@ -188,7 +234,7 @@ public BenchmarkActionValueTask(object instance, MethodInfo method, int unrollFa } } - internal class BenchmarkActionValueTask : BenchmarkActionAsync, ValueTask, ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter> + internal class BenchmarkActionValueTask : BenchmarkActionAsync, ValueTask, ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter, T> { public BenchmarkActionValueTask(object instance, MethodInfo method, int unrollFactor) : base(instance, method, unrollFactor) { From fbbb467c5652348c7b5f0436a31100fa009a0b91 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 29 Jun 2023 19:24:19 -0400 Subject: [PATCH 12/13] Pass arguments directly if awaitable. Use abstract base class without generics. Explicitly specify overhead awaitable/awaiter types. Use awaitable types in InProcessNoEmitToolchain. --- src/BenchmarkDotNet/Code/CodeGenerator.cs | 21 ++-- .../Code/DeclarationsProvider.cs | 41 +++---- .../Engines/AsyncConsumerStateMachine.cs | 37 +++--- .../Templates/BenchmarkType.txt | 11 +- .../NoEmit/BenchmarkActionFactory.cs | 98 +++++++++------- .../BenchmarkActionFactory_Implementations.cs | 107 ++++-------------- .../InProcess/NoEmit/InProcessNoEmitRunner.cs | 12 +- .../InProcessTest.cs | 40 +++---- 8 files changed, 153 insertions(+), 214 deletions(-) diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs index bf0d86806c..4fea455640 100644 --- a/src/BenchmarkDotNet/Code/CodeGenerator.cs +++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs @@ -52,7 +52,6 @@ internal static string Generate(BuildPartition buildPartition) .Replace("$WorkloadMethodReturnType$", provider.WorkloadMethodReturnTypeName) .Replace("$WorkloadMethodReturnTypeModifiers$", provider.WorkloadMethodReturnTypeModifiers) .Replace("$OverheadMethodReturnTypeName$", provider.OverheadMethodReturnTypeName) - .Replace("$AsyncBenchmarkRunnerFieldDeclarations$", provider.GetAsyncBenchmarkRunnerFieldDeclarations(buildInfo.Id)) .Replace("$InitializeAsyncBenchmarkRunnerFields$", provider.GetInitializeAsyncBenchmarkRunnerFields(buildInfo.Id)) .Replace("$GlobalSetupMethodName$", provider.GlobalSetupMethodName) .Replace("$GlobalCleanupMethodName$", provider.GlobalCleanupMethodName) @@ -64,8 +63,10 @@ internal static string Generate(BuildPartition buildPartition) .Replace("$ParamsContent$", GetParamsContent(benchmark)) .Replace("$ArgumentsDefinition$", GetArgumentsDefinition(benchmark)) .Replace("$DeclareArgumentFields$", GetDeclareArgumentFields(benchmark)) - .Replace("$InitializeArgumentFields$", GetInitializeArgumentFields(benchmark)).Replace("$LoadArguments$", GetLoadArguments(benchmark)) + .Replace("$InitializeArgumentFields$", GetInitializeArgumentFields(benchmark)) + .Replace("$LoadArguments$", GetLoadArguments(benchmark)) .Replace("$PassArguments$", passArguments) + .Replace("$PassArgumentsDirect$", GetPassArgumentsDirect(benchmark)) .Replace("$EngineFactoryType$", GetEngineFactoryTypeName(benchmark)) .Replace("$MeasureExtraStats$", buildInfo.Config.HasExtraStatsDiagnoser() ? "true" : "false") .Replace("$DisassemblerEntryMethodName$", DisassemblerConstants.DisassemblerEntryMethodName) @@ -157,11 +158,6 @@ private static DeclarationsProvider GetDeclarationsProvider(Descriptor descripto { var method = descriptor.WorkloadMethod; - if (config.GetIsAwaitable(method.ReturnType, out var asyncConsumerType)) - { - return new AsyncDeclarationsProvider(descriptor, asyncConsumerType); - } - if (method.ReturnType == typeof(void)) { bool isUsingAsyncKeyword = method.HasAttribute(); @@ -182,6 +178,11 @@ private static DeclarationsProvider GetDeclarationsProvider(Descriptor descripto return new ByRefDeclarationsProvider(descriptor); } + if (config.GetIsAwaitable(method.ReturnType, out var asyncConsumerType)) + { + return new AwaitableDeclarationsProvider(descriptor, asyncConsumerType); + } + return new NonVoidDeclarationsProvider(descriptor); } @@ -223,6 +224,12 @@ private static string GetPassArguments(BenchmarkCase benchmarkCase) benchmarkCase.Descriptor.WorkloadMethod.GetParameters() .Select((parameter, index) => $"{GetParameterModifier(parameter)} arg{index}")); + private static string GetPassArgumentsDirect(BenchmarkCase benchmarkCase) + => string.Join( + ", ", + benchmarkCase.Descriptor.WorkloadMethod.GetParameters() + .Select((parameter, index) => $"{GetParameterModifier(parameter)} __argField{index}")); + private static string GetExtraAttributes(Descriptor descriptor) => descriptor.WorkloadMethod.GetCustomAttributes(false).OfType().Any() ? "[System.STAThreadAttribute]" : string.Empty; diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs index da81645f69..8a6fb44811 100644 --- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs +++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs @@ -45,8 +45,6 @@ internal abstract class DeclarationsProvider public string OverheadMethodReturnTypeName => OverheadMethodReturnType.GetCorrectCSharpTypeName(); - public virtual string GetAsyncBenchmarkRunnerFieldDeclarations(BenchmarkId id) => null; - public virtual string GetInitializeAsyncBenchmarkRunnerFields(BenchmarkId id) => null; public virtual void OverrideUnrollFactor(BenchmarkCase benchmarkCase) { } @@ -149,20 +147,22 @@ public ByReadOnlyRefDeclarationsProvider(Descriptor descriptor) : base(descripto public override string WorkloadMethodReturnTypeModifiers => "ref readonly"; } - internal class AsyncDeclarationsProvider : DeclarationsProvider + internal class AwaitableDeclarationsProvider : DeclarationsProvider { private readonly Type asyncConsumerType; - public AsyncDeclarationsProvider(Descriptor descriptor, Type asyncConsumerType) : base(descriptor) => this.asyncConsumerType = asyncConsumerType; + public AwaitableDeclarationsProvider(Descriptor descriptor, Type asyncConsumerType) : base(descriptor) => this.asyncConsumerType = asyncConsumerType; public override string ReturnsDefinition => "RETURNS_AWAITABLE"; public override string OverheadImplementation => $"return default({OverheadMethodReturnType.GetCorrectCSharpTypeName()});"; - protected override Type OverheadMethodReturnType => typeof(EmptyAwaiter); // we return this simple type because creating bigger ValueType could take longer than benchmarked method itself + protected override Type OverheadMethodReturnType => WorkloadMethodReturnType.IsValueType && !WorkloadMethodReturnType.IsPrimitive + ? typeof(EmptyAwaiter) // we return this simple type so we don't include the cost of a large struct in the overhead + : WorkloadMethodReturnType; private string GetRunnableName(BenchmarkId id) => $"BenchmarkDotNet.Autogenerated.Runnable_{id}"; - private (string workloadRunnerTypeName, string overheadRunnerTypeName) GetAsyncBenchmarkRunnerTypeNames(BenchmarkId id) + public override string GetInitializeAsyncBenchmarkRunnerFields(BenchmarkId id) { string consumerTypeName = asyncConsumerType.GetCorrectCSharpTypeName(); @@ -174,32 +174,19 @@ internal class AsyncDeclarationsProvider : DeclarationsProvider } Type[] genericArguments = asyncConsumerInterfaceType.GetGenericArguments(); - string awaiterTypeName = genericArguments[1].GetCorrectCSharpTypeName(); - + Type awaiterType = genericArguments[1]; + string awaiterTypeName = awaiterType.GetCorrectCSharpTypeName(); + string overheadAwaiterTypeName = awaiterType.IsValueType + ? typeof(EmptyAwaiter).GetCorrectCSharpTypeName() // we use this simple type so we don't include the cost of a large struct in the overhead + : awaiterTypeName; string appendResultType = isVoidConsumer ? string.Empty : $", {genericArguments[2].GetCorrectCSharpTypeName()}"; string runnableName = GetRunnableName(id); - return ( - $"BenchmarkDotNet.Engines.AsyncWorkloadRunner<{runnableName}.WorkloadFunc, {consumerTypeName}, {WorkloadMethodReturnTypeName}, {awaiterTypeName}{appendResultType}>", - $"BenchmarkDotNet.Engines.AsyncOverheadRunner<{runnableName}.OverheadFunc, {consumerTypeName}, {WorkloadMethodReturnTypeName}, {awaiterTypeName}{appendResultType}>" - ); - } - - public override string GetAsyncBenchmarkRunnerFieldDeclarations(BenchmarkId id) - { - var (workloadRunnerTypeName, overheadRunnerTypeName) = GetAsyncBenchmarkRunnerTypeNames(id); - - return $"private readonly {workloadRunnerTypeName} __asyncWorkloadRunner;" + Environment.NewLine - + $" private readonly {overheadRunnerTypeName} __asyncOverheadRunner;"; - } - - public override string GetInitializeAsyncBenchmarkRunnerFields(BenchmarkId id) - { - var (workloadRunnerTypeName, overheadRunnerTypeName) = GetAsyncBenchmarkRunnerTypeNames(id); - string runnableName = GetRunnableName(id); + var workloadRunnerTypeName = $"BenchmarkDotNet.Engines.AsyncWorkloadRunner<{runnableName}.WorkloadFunc, {consumerTypeName}, {WorkloadMethodReturnTypeName}, {awaiterTypeName}{appendResultType}>"; + var overheadRunnerTypeName = $"BenchmarkDotNet.Engines.AsyncOverheadRunner<{runnableName}.OverheadFunc, {consumerTypeName}, {WorkloadMethodReturnTypeName}, {awaiterTypeName}, {OverheadMethodReturnTypeName}, {overheadAwaiterTypeName}{appendResultType}>"; - return $"__asyncWorkloadRunner = new {workloadRunnerTypeName}(new {runnableName}.WorkloadFunc(this));" + return $"__asyncWorkloadRunner = new {workloadRunnerTypeName}(new {runnableName}.WorkloadFunc(this));" + Environment.NewLine + $" __asyncOverheadRunner = new {overheadRunnerTypeName}(new {runnableName}.OverheadFunc(this));"; } diff --git a/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs b/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs index a33d2a636f..30d144b33c 100644 --- a/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs +++ b/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs @@ -272,7 +272,8 @@ public void MoveNext() } } - public sealed class AsyncWorkloadRunner : AsyncBenchmarkRunner.AsyncConsumer, TAwaitable, TAwaiter> + public sealed class AsyncWorkloadRunner + : AsyncBenchmarkRunner.AsyncConsumer, TAwaitable, TAwaiter> where TFunc : struct, IFunc where TAsyncConsumer : IAsyncVoidConsumer, new() where TAwaiter : ICriticalNotifyCompletion @@ -320,14 +321,17 @@ public void GetResult(ref TAwaiter awaiter) } } - public sealed class AsyncOverheadRunner : AsyncBenchmarkRunner.AsyncConsumer, EmptyAwaiter, EmptyAwaiter> - where TFunc : struct, IFunc + // TODO: pass overhead types explicitly (TAwaitableOverhead, TAwaiterOverhead), only use EmptyAwaiter for non-primitive struct types. + public sealed class AsyncOverheadRunner + : AsyncBenchmarkRunner.AsyncConsumer, TAwaitableOverhead, TAwaiterOverhead> + where TFunc : struct, IFunc where TAsyncConsumer : IAsyncVoidConsumer, new() where TAwaiter : ICriticalNotifyCompletion + where TAwaiterOverhead : ICriticalNotifyCompletion { public AsyncOverheadRunner(TFunc func) : base(func) { } - public struct AsyncConsumer : IAsyncVoidConsumer + public struct AsyncConsumer : IAsyncVoidConsumer { internal TAsyncConsumer asyncConsumer; @@ -355,19 +359,20 @@ public void SetStateMachine(IAsyncStateMachine stateMachine) // Make sure the methods are called without inlining. [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public EmptyAwaiter GetAwaiter(ref EmptyAwaiter awaitable) + public TAwaiterOverhead GetAwaiter(ref TAwaitableOverhead awaitable) => default; [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public bool GetIsCompleted(ref EmptyAwaiter awaiter) + public bool GetIsCompleted(ref TAwaiterOverhead awaiter) => true; [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public void GetResult(ref EmptyAwaiter awaiter) { } + public void GetResult(ref TAwaiterOverhead awaiter) { } } } - public sealed class AsyncWorkloadRunner : AsyncBenchmarkRunner.AsyncConsumer, TAwaitable, TAwaiter> + public sealed class AsyncWorkloadRunner + : AsyncBenchmarkRunner.AsyncConsumer, TAwaitable, TAwaiter> where TFunc : struct, IFunc where TAsyncConsumer : IAsyncResultConsumer, new() where TAwaiter : ICriticalNotifyCompletion @@ -419,14 +424,16 @@ private TResult GetResultNoInlining(ref TAwaiter awaiter) } } - public sealed class AsyncOverheadRunner : AsyncBenchmarkRunner.AsyncConsumer, EmptyAwaiter, EmptyAwaiter> - where TFunc : struct, IFunc + public sealed class AsyncOverheadRunner + : AsyncBenchmarkRunner.AsyncConsumer, TAwaitableOverhead, TAwaiterOverhead> + where TFunc : struct, IFunc where TAsyncConsumer : IAsyncResultConsumer, new() where TAwaiter : ICriticalNotifyCompletion + where TAwaiterOverhead : ICriticalNotifyCompletion { public AsyncOverheadRunner(TFunc func) : base(func) { } - public struct AsyncConsumer : IAsyncVoidConsumer + public struct AsyncConsumer : IAsyncVoidConsumer { internal TAsyncConsumer asyncConsumer; @@ -454,21 +461,21 @@ public void SetStateMachine(IAsyncStateMachine stateMachine) // Make sure the methods are called without inlining. [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public EmptyAwaiter GetAwaiter(ref EmptyAwaiter awaitable) + public TAwaiterOverhead GetAwaiter(ref TAwaitableOverhead awaitable) => default; [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public bool GetIsCompleted(ref EmptyAwaiter awaiter) + public bool GetIsCompleted(ref TAwaiterOverhead awaiter) => true; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void GetResult(ref EmptyAwaiter awaiter) + public void GetResult(ref TAwaiterOverhead awaiter) { GetResultNoInlining(ref awaiter); } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - private void GetResultNoInlining(ref EmptyAwaiter awaiter) { } + private void GetResultNoInlining(ref TAwaiterOverhead awaiter) { } } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Templates/BenchmarkType.txt b/src/BenchmarkDotNet/Templates/BenchmarkType.txt index c17a7a5dc8..530a5381bf 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkType.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkType.txt @@ -144,22 +144,19 @@ } } - $AsyncBenchmarkRunnerFieldDeclarations$ + private readonly BenchmarkDotNet.Engines.AsyncBenchmarkRunner __asyncWorkloadRunner; + private readonly BenchmarkDotNet.Engines.AsyncBenchmarkRunner __asyncOverheadRunner; [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] private $WorkloadMethodReturnType$ __InvokeWorkload() { -// TODO: pass arguments directly. - $LoadArguments$ - return workloadDelegate($PassArguments$); + return workloadDelegate($PassArgumentsDirect$); } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] private $OverheadMethodReturnTypeName$ __InvokeOverhead() { -// TODO: pass arguments directly. - $LoadArguments$ - return overheadDelegate($PassArguments$); + return overheadDelegate($PassArgumentsDirect$); } // Awaits are not unrolled. diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs index 6fe35c75e6..a3c28b4773 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs @@ -1,8 +1,10 @@ using System; +using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; - +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Running; @@ -22,36 +24,17 @@ private static BenchmarkAction CreateCore( object instance, MethodInfo? targetMethod, MethodInfo? fallbackIdleSignature, - int unrollFactor) + int unrollFactor, + IConfig config) { PrepareInstanceAndResultType(instance, targetMethod, fallbackIdleSignature, out var resultInstance, out var resultType); if (resultType == typeof(void)) return new BenchmarkActionVoid(resultInstance, targetMethod, unrollFactor); - if (resultType == typeof(Task)) - return new BenchmarkActionTask(resultInstance, targetMethod, unrollFactor); - - if (resultType == typeof(ValueTask)) - return new BenchmarkActionValueTask(resultInstance, targetMethod, unrollFactor); - - if (resultType.GetTypeInfo().IsGenericType) + if (config.GetIsAwaitable(resultType, out var asyncConsumerType)) { - var genericType = resultType.GetGenericTypeDefinition(); - var argType = resultType.GenericTypeArguments[0]; - if (typeof(Task<>) == genericType) - return Create( - typeof(BenchmarkActionTask<>).MakeGenericType(argType), - resultInstance, - targetMethod, - unrollFactor); - - if (typeof(ValueTask<>).IsAssignableFrom(genericType)) - return Create( - typeof(BenchmarkActionValueTask<>).MakeGenericType(argType), - resultInstance, - targetMethod, - unrollFactor); + return CreateBenchmarkActionAwaitable(asyncConsumerType, resultType, resultInstance, targetMethod, unrollFactor); } if (targetMethod == null && resultType.GetTypeInfo().IsValueType) @@ -65,6 +48,33 @@ private static BenchmarkAction CreateCore( unrollFactor); } + private static BenchmarkActionBase CreateBenchmarkActionAwaitable(Type asyncConsumerType, Type awaitableType, object instance, MethodInfo method, int unrollFactor) + { + var asyncConsumerInterfaceType = asyncConsumerType.GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncVoidConsumer<,>)); + bool isVoidConsumer = asyncConsumerInterfaceType?.GetGenericArguments()[0] == awaitableType; + if (!isVoidConsumer) + { + asyncConsumerInterfaceType = asyncConsumerType.GetInterfaces().First(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncResultConsumer<,,>)); + } + + Type[] genericArguments = asyncConsumerInterfaceType.GetGenericArguments(); + Type awaiterType = genericArguments[1]; + + if (isVoidConsumer) + { + return (BenchmarkActionBase) Activator.CreateInstance( + typeof(BenchmarkActionAwaitable<,,>).MakeGenericType(asyncConsumerType, awaitableType, awaiterType), + instance, + method, + unrollFactor); + } + return (BenchmarkActionBase) Activator.CreateInstance( + typeof(BenchmarkActionAwaitable<,,,>).MakeGenericType(asyncConsumerType, awaitableType, awaiterType, genericArguments[2]), + instance, + method, + unrollFactor); + } + private static void PrepareInstanceAndResultType( object instance, MethodInfo targetMethod, MethodInfo fallbackIdleSignature, out object resultInstance, out Type resultType) @@ -103,13 +113,7 @@ private static void FallbackMethod() { } internal static int GetUnrollFactor(BenchmarkCase benchmarkCase) { - // Only support (Value)Task for async benchmarks. - var methodReturnType = benchmarkCase.Descriptor.WorkloadMethod.ReturnType; - bool isAwaitable = methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask) - || (methodReturnType.GetTypeInfo().IsGenericType - && (methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) - || methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>))); - if (isAwaitable) + if (benchmarkCase.Config.GetIsAwaitable(benchmarkCase.Descriptor.WorkloadMethod.ReturnType, out _)) { benchmarkCase.ForceUnrollFactorForAsync(); } @@ -120,49 +124,55 @@ internal static int GetUnrollFactor(BenchmarkCase benchmarkCase) /// Descriptor info. /// Instance of target. /// Unroll factor. + /// Config. /// Run benchmark action. - public static BenchmarkAction CreateWorkload(Descriptor descriptor, object instance, int unrollFactor) => - CreateCore(instance, descriptor.WorkloadMethod, null, unrollFactor); + public static BenchmarkAction CreateWorkload(Descriptor descriptor, object instance, int unrollFactor, IConfig config) => + CreateCore(instance, descriptor.WorkloadMethod, null, unrollFactor, config); /// Creates idle benchmark action. /// Descriptor info. /// Instance of target. /// Unroll factor. + /// Config. /// Idle benchmark action. - public static BenchmarkAction CreateOverhead(Descriptor descriptor, object instance, int unrollFactor) => - CreateCore(instance, null, descriptor.WorkloadMethod, unrollFactor); + public static BenchmarkAction CreateOverhead(Descriptor descriptor, object instance, int unrollFactor, IConfig config) => + CreateCore(instance, null, descriptor.WorkloadMethod, unrollFactor, config); /// Creates global setup benchmark action. /// Descriptor info. /// Instance of target. + /// Config. /// Setup benchmark action. - public static BenchmarkAction CreateGlobalSetup(Descriptor descriptor, object instance) => - CreateCore(instance, descriptor.GlobalSetupMethod, FallbackSignature, 1); + public static BenchmarkAction CreateGlobalSetup(Descriptor descriptor, object instance, IConfig config) => + CreateCore(instance, descriptor.GlobalSetupMethod, FallbackSignature, 1, config); /// Creates global cleanup benchmark action. /// Descriptor info. /// Instance of target. + /// Config. /// Cleanup benchmark action. - public static BenchmarkAction CreateGlobalCleanup(Descriptor descriptor, object instance) => - CreateCore(instance, descriptor.GlobalCleanupMethod, FallbackSignature, 1); + public static BenchmarkAction CreateGlobalCleanup(Descriptor descriptor, object instance, IConfig config) => + CreateCore(instance, descriptor.GlobalCleanupMethod, FallbackSignature, 1, config); /// Creates global setup benchmark action. /// Descriptor info. /// Instance of target. + /// Config. /// Setup benchmark action. - public static BenchmarkAction CreateIterationSetup(Descriptor descriptor, object instance) => - CreateCore(instance, descriptor.IterationSetupMethod, FallbackSignature, 1); + public static BenchmarkAction CreateIterationSetup(Descriptor descriptor, object instance, IConfig config) => + CreateCore(instance, descriptor.IterationSetupMethod, FallbackSignature, 1, config); /// Creates global cleanup benchmark action. /// Descriptor info. /// Instance of target. + /// Config. /// Cleanup benchmark action. - public static BenchmarkAction CreateIterationCleanup(Descriptor descriptor, object instance) => - CreateCore(instance, descriptor.IterationCleanupMethod, FallbackSignature, 1); + public static BenchmarkAction CreateIterationCleanup(Descriptor descriptor, object instance, IConfig config) => + CreateCore(instance, descriptor.IterationCleanupMethod, FallbackSignature, 1, config); /// Creates a dummy benchmark action. /// Dummy benchmark action. public static BenchmarkAction CreateDummy() => - CreateCore(new DummyInstance(), DummyMethod, null, 1); + CreateCore(new DummyInstance(), DummyMethod, null, 1, DefaultConfig.Instance); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs index 18dba406f4..03b1fc3c0e 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs @@ -1,5 +1,4 @@ using BenchmarkDotNet.Engines; -using BenchmarkDotNet.Helpers; using Perfolizer.Horology; using System; using System.Reflection; @@ -103,59 +102,37 @@ private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock cl public override object LastRunResult => result; } - internal static class BenchmarkActionAsyncFactory + private readonly struct AwaitableFunc : IFunc { - internal static BenchmarkActionBase Create(Type consumerType, Type awaitableType, Type awaiterType, object instance, MethodInfo method, int unrollFactor) - { - return (BenchmarkActionBase) Activator.CreateInstance( - typeof(BenchmarkActionAsync<,,>).MakeGenericType(consumerType, awaitableType, awaiterType), - instance, - method, - unrollFactor); - } + private readonly Func callback; + + internal AwaitableFunc(Func callback) => this.callback = callback; + public TAwaitable Invoke() => callback(); } - internal class BenchmarkActionAsync : BenchmarkActionBase + internal class BenchmarkActionAwaitable : BenchmarkActionBase where TAsyncConsumer : struct, IAsyncVoidConsumer where TAwaiter : ICriticalNotifyCompletion { - private readonly struct WorkloadFunc : IFunc - { - private readonly Func callback; - - internal WorkloadFunc(Func callback) => this.callback = callback; - public TAwaitable Invoke() => callback(); - } - - private readonly struct OverheadFunc : IFunc - { - private readonly Func callback; - - internal OverheadFunc(Func callback) => this.callback = callback; - public EmptyAwaiter Invoke() => callback(); - } - private readonly AsyncBenchmarkRunner asyncBenchmarkRunner; - public BenchmarkActionAsync(object instance, MethodInfo method, int unrollFactor) + public BenchmarkActionAwaitable(object instance, MethodInfo method, int unrollFactor) { bool isIdle = method == null; if (!isIdle) { var callback = CreateWorkload>(instance, method); - asyncBenchmarkRunner = new AsyncWorkloadRunner(new WorkloadFunc(callback)); - InvokeSingle = InvokeSingleHardcoded; - InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; + asyncBenchmarkRunner = new AsyncWorkloadRunner, TAsyncConsumer, TAwaitable, TAwaiter>(new AwaitableFunc(callback)); } else { - asyncBenchmarkRunner = new AsyncOverheadRunner(new OverheadFunc(Overhead)); - InvokeSingle = InvokeSingleHardcoded; - InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; + asyncBenchmarkRunner = new AsyncOverheadRunner, TAsyncConsumer, TAwaitable, TAwaiter, TAwaitable, TAwaiter>(new AwaitableFunc(Overhead)); } + InvokeSingle = InvokeSingleHardcoded; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; } - private EmptyAwaiter Overhead() => default; + private TAwaitable Overhead() => default; protected virtual ValueTask InvokeSingleHardcoded() => asyncBenchmarkRunner.InvokeSingle(); @@ -164,47 +141,29 @@ private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock cl => asyncBenchmarkRunner.Invoke(repeatCount, clock); } - internal class BenchmarkActionAsync : BenchmarkActionBase + internal class BenchmarkActionAwaitable : BenchmarkActionBase where TAsyncConsumer : struct, IAsyncResultConsumer where TAwaiter : ICriticalNotifyCompletion { - private readonly struct WorkloadFunc : IFunc - { - private readonly Func callback; - - internal WorkloadFunc(Func callback) => this.callback = callback; - public TAwaitable Invoke() => callback(); - } - - private readonly struct OverheadFunc : IFunc - { - private readonly Func callback; - - internal OverheadFunc(Func callback) => this.callback = callback; - public EmptyAwaiter Invoke() => callback(); - } - private readonly AsyncBenchmarkRunner asyncBenchmarkRunner; - public BenchmarkActionAsync(object instance, MethodInfo method, int unrollFactor) + public BenchmarkActionAwaitable(object instance, MethodInfo method, int unrollFactor) { bool isIdle = method == null; if (!isIdle) { var callback = CreateWorkload>(instance, method); - asyncBenchmarkRunner = new AsyncWorkloadRunner(new WorkloadFunc(callback)); - InvokeSingle = InvokeSingleHardcoded; - InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; + asyncBenchmarkRunner = new AsyncWorkloadRunner, TAsyncConsumer, TAwaitable, TAwaiter, TResult>(new AwaitableFunc(callback)); } else { - asyncBenchmarkRunner = new AsyncOverheadRunner(new OverheadFunc(Overhead)); - InvokeSingle = InvokeSingleHardcoded; - InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; + asyncBenchmarkRunner = new AsyncOverheadRunner, TAsyncConsumer, TAwaitable, TAwaiter, TAwaitable, TAwaiter, TResult>(new AwaitableFunc(Overhead)); } + InvokeSingle = InvokeSingleHardcoded; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; } - private EmptyAwaiter Overhead() => default; + private TAwaitable Overhead() => default; protected virtual ValueTask InvokeSingleHardcoded() => asyncBenchmarkRunner.InvokeSingle(); @@ -212,33 +171,5 @@ protected virtual ValueTask InvokeSingleHardcoded() private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) => asyncBenchmarkRunner.Invoke(repeatCount, clock); } - - internal class BenchmarkActionTask : BenchmarkActionAsync - { - public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) : base(instance, method, unrollFactor) - { - } - } - - internal class BenchmarkActionTask : BenchmarkActionAsync, Task, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter, T> - { - public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) : base(instance, method, unrollFactor) - { - } - } - - internal class BenchmarkActionValueTask : BenchmarkActionAsync - { - public BenchmarkActionValueTask(object instance, MethodInfo method, int unrollFactor) : base(instance, method, unrollFactor) - { - } - } - - internal class BenchmarkActionValueTask : BenchmarkActionAsync, ValueTask, ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter, T> - { - public BenchmarkActionValueTask(object instance, MethodInfo method, int unrollFactor) : base(instance, method, unrollFactor) - { - } - } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs index a6f5e37619..972e187c48 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs @@ -110,12 +110,12 @@ public static void RunCore(IHost host, BenchmarkCase benchmarkCase) // DONTTOUCH: these should be allocated together var instance = Activator.CreateInstance(benchmarkCase.Descriptor.Type); - var workloadAction = BenchmarkActionFactory.CreateWorkload(target, instance, unrollFactor); - var overheadAction = BenchmarkActionFactory.CreateOverhead(target, instance, unrollFactor); - var globalSetupAction = BenchmarkActionFactory.CreateGlobalSetup(target, instance); - var globalCleanupAction = BenchmarkActionFactory.CreateGlobalCleanup(target, instance); - var iterationSetupAction = BenchmarkActionFactory.CreateIterationSetup(target, instance); - var iterationCleanupAction = BenchmarkActionFactory.CreateIterationCleanup(target, instance); + var workloadAction = BenchmarkActionFactory.CreateWorkload(target, instance, unrollFactor, benchmarkCase.Config); + var overheadAction = BenchmarkActionFactory.CreateOverhead(target, instance, unrollFactor, benchmarkCase.Config); + var globalSetupAction = BenchmarkActionFactory.CreateGlobalSetup(target, instance, benchmarkCase.Config); + var globalCleanupAction = BenchmarkActionFactory.CreateGlobalCleanup(target, instance, benchmarkCase.Config); + var iterationSetupAction = BenchmarkActionFactory.CreateIterationSetup(target, instance, benchmarkCase.Config); + var iterationCleanupAction = BenchmarkActionFactory.CreateIterationCleanup(target, instance, benchmarkCase.Config); var dummy1 = BenchmarkActionFactory.CreateDummy(); var dummy2 = BenchmarkActionFactory.CreateDummy(); var dummy3 = BenchmarkActionFactory.CreateDummy(); diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs index 7ea899ee12..148b3501af 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs @@ -112,24 +112,24 @@ private void TestInvoke(Expression> methodCall, int un var descriptor = new Descriptor(typeof(BenchmarkAllCases), targetMethod, targetMethod, targetMethod); // Run mode - var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkAllCases(), unrollFactor); + var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkAllCases(), unrollFactor, DefaultConfig.Instance); TestInvoke(action, unrollFactor, false, ref BenchmarkAllCases.Counter); // Idle mode - action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkAllCases(), unrollFactor); + action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkAllCases(), unrollFactor, DefaultConfig.Instance); TestInvoke(action, unrollFactor, true, ref BenchmarkAllCases.Counter); // GlobalSetup/GlobalCleanup - action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkAllCases()); + action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkAllCases(), DefaultConfig.Instance); TestInvoke(action, 1, false, ref BenchmarkAllCases.Counter); - action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkAllCases()); + action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkAllCases(), DefaultConfig.Instance); TestInvoke(action, 1, false, ref BenchmarkAllCases.Counter); // GlobalSetup/GlobalCleanup (empty) descriptor = new Descriptor(typeof(BenchmarkAllCases), targetMethod); - action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkAllCases()); + action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkAllCases(), DefaultConfig.Instance); TestInvoke(action, unrollFactor, true, ref BenchmarkAllCases.Counter); - action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkAllCases()); + action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkAllCases(), DefaultConfig.Instance); TestInvoke(action, unrollFactor, true, ref BenchmarkAllCases.Counter); // Dummy (just in case something may broke) @@ -154,11 +154,11 @@ private void TestInvoke(Expression> methodCall, in } // Run mode - var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkAllCases(), unrollFactor); + var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkAllCases(), unrollFactor, DefaultConfig.Instance); TestInvoke(action, unrollFactor, false, ref BenchmarkAllCases.Counter); // Idle mode - action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkAllCases(), unrollFactor); + action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkAllCases(), unrollFactor, DefaultConfig.Instance); TestInvoke(action, unrollFactor, true, ref BenchmarkAllCases.Counter); } @@ -170,24 +170,24 @@ private void TestInvokeSetupCleanupTask(Expression Date: Sun, 2 Jul 2023 03:59:20 -0400 Subject: [PATCH 13/13] Separated awaitable adapter and async method builder adapter. Support multiple generic arguments in adapters. No need to pass awaitable type to config, only pass awaitable adapter type. --- src/BenchmarkDotNet/Code/CodeGenerator.cs | 4 +- .../Code/DeclarationsProvider.cs | 28 +- .../Configs/AsyncAdapterDefinition.cs | 230 +++++++++ .../Configs/ConfigExtensions.cs | 40 +- src/BenchmarkDotNet/Configs/DebugConfig.cs | 9 +- src/BenchmarkDotNet/Configs/DefaultConfig.cs | 22 +- .../Configs/IAsyncMethodBuilderAdapter.cs | 16 + .../Configs/IAwaitableAdapter.cs | 20 + src/BenchmarkDotNet/Configs/IConfig.cs | 3 +- .../Configs/ImmutableConfig.cs | 8 +- .../Configs/ImmutableConfigBuilder.cs | 2 +- src/BenchmarkDotNet/Configs/ManualConfig.cs | 74 +-- .../Engines/AsyncBenchmarkRunner.cs | 379 ++++++++++++++ .../Engines/AsyncConsumerStateMachine.cs | 481 ------------------ src/BenchmarkDotNet/Engines/AsyncConsumers.cs | 175 ------- .../Engines/AsyncMethodBuilderAdapters.cs | 66 +++ .../Engines/AwaitableAdapters.cs | 40 ++ .../NoEmit/BenchmarkActionFactory.cs | 29 +- .../BenchmarkActionFactory_Implementations.cs | 21 +- 19 files changed, 842 insertions(+), 805 deletions(-) create mode 100644 src/BenchmarkDotNet/Configs/AsyncAdapterDefinition.cs create mode 100644 src/BenchmarkDotNet/Configs/IAsyncMethodBuilderAdapter.cs create mode 100644 src/BenchmarkDotNet/Configs/IAwaitableAdapter.cs create mode 100644 src/BenchmarkDotNet/Engines/AsyncBenchmarkRunner.cs delete mode 100644 src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs delete mode 100644 src/BenchmarkDotNet/Engines/AsyncConsumers.cs create mode 100644 src/BenchmarkDotNet/Engines/AsyncMethodBuilderAdapters.cs create mode 100644 src/BenchmarkDotNet/Engines/AwaitableAdapters.cs diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs index 4fea455640..65a0bd1030 100644 --- a/src/BenchmarkDotNet/Code/CodeGenerator.cs +++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs @@ -178,9 +178,9 @@ private static DeclarationsProvider GetDeclarationsProvider(Descriptor descripto return new ByRefDeclarationsProvider(descriptor); } - if (config.GetIsAwaitable(method.ReturnType, out var asyncConsumerType)) + if (config.GetIsAwaitable(method.ReturnType, out var adapter)) { - return new AwaitableDeclarationsProvider(descriptor, asyncConsumerType); + return new AwaitableDeclarationsProvider(descriptor, adapter); } return new NonVoidDeclarationsProvider(descriptor); diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs index 8a6fb44811..cefd2ebef0 100644 --- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs +++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; +using BenchmarkDotNet.Configs; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers; @@ -149,8 +150,9 @@ public ByReadOnlyRefDeclarationsProvider(Descriptor descriptor) : base(descripto internal class AwaitableDeclarationsProvider : DeclarationsProvider { - private readonly Type asyncConsumerType; - public AwaitableDeclarationsProvider(Descriptor descriptor, Type asyncConsumerType) : base(descriptor) => this.asyncConsumerType = asyncConsumerType; + private readonly ConcreteAsyncAdapter adapter; + + public AwaitableDeclarationsProvider(Descriptor descriptor, ConcreteAsyncAdapter adapter) : base(descriptor) => this.adapter = adapter; public override string ReturnsDefinition => "RETURNS_AWAITABLE"; @@ -164,27 +166,19 @@ internal class AwaitableDeclarationsProvider : DeclarationsProvider public override string GetInitializeAsyncBenchmarkRunnerFields(BenchmarkId id) { - string consumerTypeName = asyncConsumerType.GetCorrectCSharpTypeName(); - - var asyncConsumerInterfaceType = asyncConsumerType.GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncVoidConsumer<,>)); - bool isVoidConsumer = asyncConsumerInterfaceType?.GetGenericArguments()[0] == WorkloadMethodReturnType; - if (!isVoidConsumer) - { - asyncConsumerInterfaceType = asyncConsumerType.GetInterfaces().First(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncResultConsumer<,,>)); - } + string awaitableAdapterTypeName = adapter.awaitableAdapterType.GetCorrectCSharpTypeName(); + string asyncMethodBuilderAdapterTypeName = adapter.asyncMethodBuilderAdapterType.GetCorrectCSharpTypeName(); - Type[] genericArguments = asyncConsumerInterfaceType.GetGenericArguments(); - Type awaiterType = genericArguments[1]; - string awaiterTypeName = awaiterType.GetCorrectCSharpTypeName(); - string overheadAwaiterTypeName = awaiterType.IsValueType + string awaiterTypeName = adapter.awaiterType.GetCorrectCSharpTypeName(); + string overheadAwaiterTypeName = adapter.awaiterType.IsValueType ? typeof(EmptyAwaiter).GetCorrectCSharpTypeName() // we use this simple type so we don't include the cost of a large struct in the overhead : awaiterTypeName; - string appendResultType = isVoidConsumer ? string.Empty : $", {genericArguments[2].GetCorrectCSharpTypeName()}"; + string appendResultType = adapter.resultType == null ? string.Empty : $", {adapter.resultType.GetCorrectCSharpTypeName()}"; string runnableName = GetRunnableName(id); - var workloadRunnerTypeName = $"BenchmarkDotNet.Engines.AsyncWorkloadRunner<{runnableName}.WorkloadFunc, {consumerTypeName}, {WorkloadMethodReturnTypeName}, {awaiterTypeName}{appendResultType}>"; - var overheadRunnerTypeName = $"BenchmarkDotNet.Engines.AsyncOverheadRunner<{runnableName}.OverheadFunc, {consumerTypeName}, {WorkloadMethodReturnTypeName}, {awaiterTypeName}, {OverheadMethodReturnTypeName}, {overheadAwaiterTypeName}{appendResultType}>"; + var workloadRunnerTypeName = $"BenchmarkDotNet.Engines.AsyncWorkloadRunner<{runnableName}.WorkloadFunc, {asyncMethodBuilderAdapterTypeName}, {awaitableAdapterTypeName}, {WorkloadMethodReturnTypeName}, {awaiterTypeName}{appendResultType}>"; + var overheadRunnerTypeName = $"BenchmarkDotNet.Engines.AsyncOverheadRunner<{runnableName}.OverheadFunc, {asyncMethodBuilderAdapterTypeName}, {OverheadMethodReturnTypeName}, {overheadAwaiterTypeName}{appendResultType}>"; return $"__asyncWorkloadRunner = new {workloadRunnerTypeName}(new {runnableName}.WorkloadFunc(this));" + Environment.NewLine + $" __asyncOverheadRunner = new {overheadRunnerTypeName}(new {runnableName}.OverheadFunc(this));"; diff --git a/src/BenchmarkDotNet/Configs/AsyncAdapterDefinition.cs b/src/BenchmarkDotNet/Configs/AsyncAdapterDefinition.cs new file mode 100644 index 0000000000..22185669e2 --- /dev/null +++ b/src/BenchmarkDotNet/Configs/AsyncAdapterDefinition.cs @@ -0,0 +1,230 @@ +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace BenchmarkDotNet.Configs +{ + public sealed class AsyncAdapterDefinition : IComparable, IEquatable + { + public Type AwaitableType { get; private set; } + public Type AwaitableAdapterType { get; private set; } + public Type AsyncMethodBuilderAdapterType { get; private set; } + + private readonly Type[] awaitableAdapterInterfaceGenericArguments; + private readonly Type[] originalAwaitableGenericArguments; + private readonly int openGenericArgumentsCount; + + public AsyncAdapterDefinition(Type awaitableAdapterType, Type? asyncMethodBuilderAdapterType = null) + { + // If asyncMethodBuilderAdapterType is null, we use awaitableAdapterType if it implements IAsyncMethodBuilderAdapter, + // otherwise we fallback to AsyncTaskMethodBuilderAdapter. + asyncMethodBuilderAdapterType ??= awaitableAdapterType.GetInterfaces().Contains(typeof(IAsyncMethodBuilderAdapter)) + ? awaitableAdapterType + : typeof(AsyncTaskMethodBuilderAdapter); + AwaitableAdapterType = awaitableAdapterType; + AsyncMethodBuilderAdapterType = asyncMethodBuilderAdapterType; + + // Validate asyncMethodBuilderAdapterType + bool isPublic = asyncMethodBuilderAdapterType.IsPublic || asyncMethodBuilderAdapterType.IsNestedPublic; + if (!isPublic || (!asyncMethodBuilderAdapterType.IsValueType && asyncMethodBuilderAdapterType.GetConstructor(Array.Empty()) == null)) + { + throw new ArgumentException($"asyncMethodBuilderAdapterType [{asyncMethodBuilderAdapterType.GetCorrectCSharpTypeName()}] is not a public struct, or a public class with a public, parameterless constructor."); + } + if (!asyncMethodBuilderAdapterType.GetInterfaces().Contains(typeof(IAsyncMethodBuilderAdapter))) + { + throw new ArgumentException($"asyncMethodBuilderAdapterType [{asyncMethodBuilderAdapterType.GetCorrectCSharpTypeName()}] does not implement [{typeof(IAsyncMethodBuilderAdapter).GetCorrectCSharpTypeName()}]."); + } + + // Validate awaitableAdapterType + isPublic = asyncMethodBuilderAdapterType.IsPublic || asyncMethodBuilderAdapterType.IsNestedPublic; + if (!isPublic || (!asyncMethodBuilderAdapterType.IsValueType && asyncMethodBuilderAdapterType.GetConstructor(Array.Empty()) == null)) + { + throw new ArgumentException($"awaitableAdapterType [{asyncMethodBuilderAdapterType.GetCorrectCSharpTypeName()}] is not a public struct, or a public class with a public, parameterless constructor."); + } + + // Must implement exactly 1 IAwaitableAdapter + var awaitableAdapterVoidInterfaces = awaitableAdapterType.GetInterfaces().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAwaitableAdapter<,>)).ToArray(); + var awaitableAdapterResultInterfaces = awaitableAdapterType.GetInterfaces().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAwaitableAdapter<,,>)).ToArray(); + if (awaitableAdapterVoidInterfaces.Length + awaitableAdapterResultInterfaces.Length != 1) + { + string msg = awaitableAdapterVoidInterfaces.Length + awaitableAdapterResultInterfaces.Length == 0 + ? "does not implement" + : "implements more than one of"; + throw new ArgumentException($"awaitableAdapterType [{awaitableAdapterType.GetCorrectCSharpTypeName()}] {msg} [{typeof(IAwaitableAdapter<,>).GetCorrectCSharpTypeName()}] or [{typeof(IAwaitableAdapter<,,>).GetCorrectCSharpTypeName()}]."); + } + + // Retrieve the awaitable type from the IAwaitableAdapter interface + awaitableAdapterInterfaceGenericArguments = awaitableAdapterVoidInterfaces.Length == 1 + ? awaitableAdapterVoidInterfaces[0].GetGenericArguments() + : awaitableAdapterResultInterfaces[0].GetGenericArguments(); + + Type awaitableType = awaitableAdapterInterfaceGenericArguments[0]; + + // Validate open generics + // awaitableType and awaitableAdapterType must contain the exact same open generic types, + // while asyncMethodBuilderAdapterType can have the same or fewer open generic types. + // It doesn't matter if an open generic type is used multiple times (), as long as each distinct type matches. + originalAwaitableGenericArguments = awaitableType.GetGenericArguments(); + var awaitableOpenGenericTypes = new HashSet( + originalAwaitableGenericArguments.Where(t => t.IsGenericParameter) + ); + Type[] builderGenericTypes = asyncMethodBuilderAdapterType.GetGenericArguments(); + var builderOpenGenericTypes = new HashSet( + builderGenericTypes.Where(t => t.IsGenericParameter) + ); + Type[] awaitableAdapterGenericTypes = awaitableAdapterType.GetGenericArguments(); + foreach (var genericType in awaitableAdapterGenericTypes.Where(t => t.IsGenericParameter).Distinct()) + { + ++openGenericArgumentsCount; + if (!awaitableOpenGenericTypes.Remove(genericType)) + { + throw new ArgumentException($"awaitableAdapterType [{awaitableAdapterType.GetCorrectCSharpTypeName()}] has at least 1 open generic argument that is not contained in the awaitable type [{awaitableType.GetCorrectCSharpTypeName()}]."); + } + builderOpenGenericTypes.Remove(genericType); + } + if (awaitableOpenGenericTypes.Count > 0) + { + throw new ArgumentException($"awaitable type [{awaitableType.GetCorrectCSharpTypeName()}] has at least 1 open generic argument that is not defined in awaitableAdapterType [{awaitableAdapterType.GetCorrectCSharpTypeName()}]."); + } + if (builderOpenGenericTypes.Count > 0) + { + throw new ArgumentException($"asyncMethodBuilderAdapterType [{asyncMethodBuilderAdapterType.GetCorrectCSharpTypeName()}] has at least 1 open generic argument that is not defined in awaitableAdapterType [{awaitableAdapterType.GetCorrectCSharpTypeName()}]."); + } + + if (awaitableType.IsGenericType) + { + // Remap open generic types to their generic type definition (MyType becomes MyType) + // Get generic types from the generic type definition, then fill in the non-generic types. + var awaitableGenericTypeDefinition = awaitableType.GetGenericTypeDefinition(); + Type[] usableGenericTypes = awaitableGenericTypeDefinition.GetGenericArguments(); + for (int i = 0; i < usableGenericTypes.Length; ++i) + { + if (!originalAwaitableGenericArguments[i].IsGenericParameter) + { + usableGenericTypes[i] = originalAwaitableGenericArguments[i]; + } + } + AwaitableType = awaitableGenericTypeDefinition.MakeGenericType(usableGenericTypes); + } + else + { + AwaitableType = awaitableType; + } + } + + internal bool TryMatch(Type type, out ConcreteAsyncAdapter constructedAwaitableAdapter) + { + if (type == AwaitableType) + { + constructedAwaitableAdapter = new ConcreteAsyncAdapter() + { + awaitableType = AwaitableType, + awaiterType = awaitableAdapterInterfaceGenericArguments[1], + awaitableAdapterType = AwaitableAdapterType, + asyncMethodBuilderAdapterType = AsyncMethodBuilderAdapterType, + resultType = awaitableAdapterInterfaceGenericArguments.Length == 2 ? null : awaitableAdapterInterfaceGenericArguments[2] + }; + return true; + } + + if (!type.IsGenericType || !AwaitableType.IsGenericType) + { + constructedAwaitableAdapter = null; + return false; + } + if (type.GetGenericTypeDefinition() != AwaitableType.GetGenericTypeDefinition()) + { + constructedAwaitableAdapter = null; + return false; + } + + // Match against closed generic types, and build a map of open generics to concrete types. + Type[] actualGenericTypes = type.GetGenericArguments(); + var openToConcreteMap = new Dictionary(); + for (int i = 0; i < actualGenericTypes.Length; ++i) + { + Type originalGenericArgument = originalAwaitableGenericArguments[i]; + if (originalGenericArgument.IsGenericParameter) + { + openToConcreteMap[originalGenericArgument] = actualGenericTypes[i]; + continue; + } + if (originalGenericArgument != actualGenericTypes[i]) + { + constructedAwaitableAdapter = null; + return false; + } + } + + // Construct the concrete types using the mapped open-to-concrete types. + Type[] concreteAwaitableAdapterGenericArguments = MapToConcreteTypes(originalAwaitableGenericArguments, openToConcreteMap); + Type concreteAwaitableAdapterType = AwaitableAdapterType.GetGenericTypeDefinition().MakeGenericType(concreteAwaitableAdapterGenericArguments); + + Type concreteAsyncMethodBuilderAdapterType = AsyncMethodBuilderAdapterType; + if (AsyncMethodBuilderAdapterType.IsGenericType) + { + Type[] originalAsyncMethodBuilderAdapterGenericArguments = AsyncMethodBuilderAdapterType.GetGenericArguments(); + // Only construct the concrete asyncMethodBuilderAdapterType if it's not already concrete. + if (originalAsyncMethodBuilderAdapterGenericArguments.Any(t => t.IsGenericParameter)) + { + Type[] concreteAsyncMethodBuilderAdapterGenericArguments = MapToConcreteTypes(originalAsyncMethodBuilderAdapterGenericArguments, openToConcreteMap); + concreteAsyncMethodBuilderAdapterType = AsyncMethodBuilderAdapterType.GetGenericTypeDefinition().MakeGenericType(concreteAwaitableAdapterGenericArguments); + } + } + + Type[] concreteAwaitableAdapterInterfaceGenericArguments = concreteAwaitableAdapterType.GetInterfaces() + .Single(t => t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(IAwaitableAdapter<,>) || t.GetGenericTypeDefinition() == typeof(IAwaitableAdapter<,,>))) + .GetGenericArguments(); + constructedAwaitableAdapter = new ConcreteAsyncAdapter() + { + awaitableType = type, + awaiterType = concreteAwaitableAdapterInterfaceGenericArguments[1], + awaitableAdapterType = concreteAwaitableAdapterType, + asyncMethodBuilderAdapterType = concreteAsyncMethodBuilderAdapterType, + resultType = concreteAwaitableAdapterInterfaceGenericArguments.Length == 2 ? null : concreteAwaitableAdapterInterfaceGenericArguments[2] + }; + return true; + } + + private static Type[] MapToConcreteTypes(Type[] originalTypes, Dictionary openToConcreteMap) + { + Type[] concreteTypes = new Type[originalTypes.Length]; + originalTypes.CopyTo(concreteTypes, 0); + for (int i = 0; i < concreteTypes.Length; ++i) + { + Type genericType = concreteTypes[i]; + if (genericType.IsGenericParameter) + { + concreteTypes[i] = openToConcreteMap[genericType]; + } + } + return concreteTypes; + } + + // We override the default reference type hashing and equality algorithms to forward to AwaitableAdapterType. + // This makes AwaitableAdapterType the "key" when this type is added to a hashset. + public override int GetHashCode() + => AwaitableAdapterType.GetHashCode(); + + public override bool Equals(object obj) + => obj is AsyncAdapterDefinition def && Equals(def); + + public bool Equals(AsyncAdapterDefinition other) + => AwaitableAdapterType.Equals(other.AwaitableAdapterType); + + // We compare against types with fewer open generic types first. This prioritizes exact matches over constructed matches (Task is better than Task<>). + public int CompareTo(AsyncAdapterDefinition other) + => openGenericArgumentsCount.CompareTo(other.openGenericArgumentsCount); + } + + internal sealed class ConcreteAsyncAdapter + { + internal Type awaitableType; + internal Type awaiterType; + internal Type awaitableAdapterType; + internal Type asyncMethodBuilderAdapterType; + internal Type? resultType; + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs index e0736eaaf1..f0bba07ca9 100644 --- a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs +++ b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; using System.Globalization; @@ -10,6 +11,7 @@ using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Filters; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; @@ -116,7 +118,19 @@ public static class ConfigExtensions [PublicAPI] public static ManualConfig HideColumns(this IConfig config, params string[] columnNames) => config.With(c => c.HideColumns(columnNames)); [PublicAPI] public static ManualConfig HideColumns(this IConfig config, params IColumn[] columns) => config.With(c => c.HideColumns(columns)); [PublicAPI] public static ManualConfig HideColumns(this IConfig config, params IColumnHidingRule[] rules) => config.With(c => c.HideColumns(rules)); - [PublicAPI] public static ManualConfig AddAsyncConsumer(this IConfig config, Type awaitableType, Type asyncConsumerType) => config.With(c => c.AddAsyncConsumer(awaitableType, asyncConsumerType)); + + /// + /// Adds an adapter to make a type awaitable. This type must implement or . + /// Optionally provide an async method builder adapter that will be used to consume the awaitable type. This type must implement . + /// If not provided, will be used if it implements , otherwise will be used. + /// + /// + /// If an adapter already exists for the corresponding awaitable type, it will be overridden. + /// + /// + [PublicAPI] + public static ManualConfig AddAsyncAdapter(this IConfig config, Type awaitableAdapterType, Type asyncMethodBuilderAdapterType = null) + => config.With(c => c.AddAsyncAdapter(awaitableAdapterType, asyncMethodBuilderAdapterType)); public static ImmutableConfig CreateImmutableConfig(this IConfig config) => ImmutableConfigBuilder.Create(config); @@ -136,26 +150,20 @@ private static ManualConfig With(this IConfig config, Action addAc return manualConfig; } - internal static bool GetIsAwaitable(this IConfig config, Type type, out Type asyncConsumerType) + internal static bool GetIsAwaitable(this IConfig config, Type type, out ConcreteAsyncAdapter adapter) { - var consumerTypes = config.GetAsyncConsumerTypes(); - if (consumerTypes.TryGetValue(type, out asyncConsumerType)) - { - return true; - } - if (type.IsGenericType) + var asyncAdapterDefinitions = new HashSet(DefaultConfig.DefaultAsyncAdapterDefinitions); + asyncAdapterDefinitions.AddRange(config.GetAsyncAdapterDefinitions()); + var arr = asyncAdapterDefinitions.ToArray(); + Array.Sort(arr); + foreach (var adapterDefinition in arr) { - var genericType = type.GetGenericArguments()[0]; - foreach (var kvp in consumerTypes) + if (adapterDefinition.TryMatch(type, out adapter)) { - if (kvp.Key.IsGenericType && kvp.Key.MakeGenericType(genericType) == type) - { - // TODO: handle partially closed types. - asyncConsumerType = kvp.Value.MakeGenericType(genericType); - return true; - } + return true; } } + adapter = null; return false; } } diff --git a/src/BenchmarkDotNet/Configs/DebugConfig.cs b/src/BenchmarkDotNet/Configs/DebugConfig.cs index 56ec0c4191..2fe048ae8b 100644 --- a/src/BenchmarkDotNet/Configs/DebugConfig.cs +++ b/src/BenchmarkDotNet/Configs/DebugConfig.cs @@ -67,14 +67,7 @@ public abstract class DebugConfig : IConfig public IEnumerable GetHardwareCounters() => Array.Empty(); public IEnumerable GetFilters() => Array.Empty(); public IEnumerable GetColumnHidingRules() => Array.Empty(); - public IReadOnlyDictionary GetAsyncConsumerTypes() => new Dictionary() - { - // Default consumers for Task and ValueTask. - [typeof(Task)] = typeof(TaskConsumer), - [typeof(Task<>)] = typeof(TaskConsumer<>), - [typeof(ValueTask)] = typeof(ValueTaskConsumer), - [typeof(ValueTask<>)] = typeof(ValueTaskConsumer<>), - }; + public IEnumerable GetAsyncAdapterDefinitions() => Array.Empty(); public IOrderer Orderer => DefaultOrderer.Instance; public ICategoryDiscoverer? CategoryDiscoverer => DefaultCategoryDiscoverer.Instance; diff --git a/src/BenchmarkDotNet/Configs/DefaultConfig.cs b/src/BenchmarkDotNet/Configs/DefaultConfig.cs index 3c5d175134..7d8756d3c3 100644 --- a/src/BenchmarkDotNet/Configs/DefaultConfig.cs +++ b/src/BenchmarkDotNet/Configs/DefaultConfig.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.IO; -using System.Threading.Tasks; using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Diagnosers; @@ -25,6 +25,15 @@ public class DefaultConfig : IConfig public static readonly IConfig Instance = new DefaultConfig(); private readonly static Conclusion[] emptyConclusion = Array.Empty(); + // AsyncAdapterDefinition can be expensive to create, so we cache the defaults. + internal static ImmutableArray DefaultAsyncAdapterDefinitions { get; } = new[] + { + new AsyncAdapterDefinition(typeof(TaskAdapter), typeof(AsyncTaskMethodBuilderAdapter)), + new AsyncAdapterDefinition(typeof(TaskAdapter<>), typeof(AsyncTaskMethodBuilderAdapter)), + new AsyncAdapterDefinition(typeof(ValueTaskAdapter), typeof(AsyncValueTaskMethodBuilderAdapter)), + new AsyncAdapterDefinition(typeof(ValueTaskAdapter<>), typeof(AsyncValueTaskMethodBuilderAdapter)) + }.ToImmutableArray(); + private DefaultConfig() { } @@ -112,13 +121,8 @@ public string ArtifactsPath public IEnumerable GetColumnHidingRules() => Array.Empty(); - public IReadOnlyDictionary GetAsyncConsumerTypes() => new Dictionary() - { - // Default consumers for Task and ValueTask. - [typeof(Task)] = typeof(TaskConsumer), - [typeof(Task<>)] = typeof(TaskConsumer<>), - [typeof(ValueTask)] = typeof(ValueTaskConsumer), - [typeof(ValueTask<>)] = typeof(ValueTaskConsumer<>), - }; + // We don't expose the default adapters to users, they get combined with user-supplied adapters when the actual benchmark types are constructed. + // This is necessary so the defaults won't override user-supplied adapters when combining configs. + public IEnumerable GetAsyncAdapterDefinitions() => Array.Empty(); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Configs/IAsyncMethodBuilderAdapter.cs b/src/BenchmarkDotNet/Configs/IAsyncMethodBuilderAdapter.cs new file mode 100644 index 0000000000..06256100bc --- /dev/null +++ b/src/BenchmarkDotNet/Configs/IAsyncMethodBuilderAdapter.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; + +namespace BenchmarkDotNet.Configs +{ + public interface IAsyncMethodBuilderAdapter + { + public void CreateAsyncMethodBuilder(); + public void Start(ref TStateMachine stateMachine) + where TStateMachine : IAsyncStateMachine; + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine; + public void SetStateMachine(IAsyncStateMachine stateMachine); + public void SetResult(); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Configs/IAwaitableAdapter.cs b/src/BenchmarkDotNet/Configs/IAwaitableAdapter.cs new file mode 100644 index 0000000000..bfdd6c2941 --- /dev/null +++ b/src/BenchmarkDotNet/Configs/IAwaitableAdapter.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; + +namespace BenchmarkDotNet.Configs +{ + public interface IAwaitableAdapter + where TAwaiter : ICriticalNotifyCompletion + { + public TAwaiter GetAwaiter(ref TAwaitable awaitable); + public bool GetIsCompleted(ref TAwaiter awaiter); + public void GetResult(ref TAwaiter awaiter); + } + + public interface IAwaitableAdapter + where TAwaiter : ICriticalNotifyCompletion + { + public TAwaiter GetAwaiter(ref TAwaitable awaitable); + public bool GetIsCompleted(ref TAwaiter awaiter); + public TResult GetResult(ref TAwaiter awaiter); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Configs/IConfig.cs b/src/BenchmarkDotNet/Configs/IConfig.cs index c8b7cfbfe1..9f83ed45c4 100644 --- a/src/BenchmarkDotNet/Configs/IConfig.cs +++ b/src/BenchmarkDotNet/Configs/IConfig.cs @@ -12,7 +12,6 @@ using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; using BenchmarkDotNet.Validators; -using JetBrains.Annotations; namespace BenchmarkDotNet.Configs { @@ -29,7 +28,7 @@ public interface IConfig IEnumerable GetFilters(); IEnumerable GetLogicalGroupRules(); IEnumerable GetColumnHidingRules(); - IReadOnlyDictionary GetAsyncConsumerTypes(); + IEnumerable GetAsyncAdapterDefinitions(); IOrderer? Orderer { get; } ICategoryDiscoverer? CategoryDiscoverer { get; } diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs index be2e886ad6..782783e269 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs @@ -32,7 +32,7 @@ public sealed class ImmutableConfig : IConfig private readonly ImmutableHashSet filters; private readonly ImmutableArray rules; private readonly ImmutableArray columnHidingRules; - private readonly ImmutableDictionary asyncConsumerTypes; + private readonly ImmutableHashSet awaitableAdapters; internal ImmutableConfig( ImmutableArray uniqueColumnProviders, @@ -45,7 +45,7 @@ internal ImmutableConfig( ImmutableHashSet uniqueFilters, ImmutableArray uniqueRules, ImmutableArray uniqueColumnHidingRules, - ImmutableDictionary uniqueAsyncConsumerTypes, + ImmutableHashSet uniqueAsyncConsumerTypes, ImmutableHashSet uniqueRunnableJobs, ConfigUnionRule unionRule, string artifactsPath, @@ -67,7 +67,7 @@ internal ImmutableConfig( filters = uniqueFilters; rules = uniqueRules; columnHidingRules = uniqueColumnHidingRules; - asyncConsumerTypes = uniqueAsyncConsumerTypes; + awaitableAdapters = uniqueAsyncConsumerTypes; jobs = uniqueRunnableJobs; UnionRule = unionRule; ArtifactsPath = artifactsPath; @@ -100,7 +100,7 @@ internal ImmutableConfig( public IEnumerable GetFilters() => filters; public IEnumerable GetLogicalGroupRules() => rules; public IEnumerable GetColumnHidingRules() => columnHidingRules; - public IReadOnlyDictionary GetAsyncConsumerTypes() => asyncConsumerTypes; + public IEnumerable GetAsyncAdapterDefinitions() => awaitableAdapters; public ILogger GetCompositeLogger() => new CompositeLogger(loggers); public IExporter GetCompositeExporter() => new CompositeExporter(exporters); diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs index f74a5419c2..163a23bb69 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs @@ -51,7 +51,7 @@ public static ImmutableConfig Create(IConfig source) var uniqueFilters = source.GetFilters().ToImmutableHashSet(); var uniqueRules = source.GetLogicalGroupRules().ToImmutableArray(); var uniqueHidingRules = source.GetColumnHidingRules().ToImmutableArray(); - var uniqueAsyncConsumerTypes = source.GetAsyncConsumerTypes().ToImmutableDictionary(); + var uniqueAsyncConsumerTypes = source.GetAsyncAdapterDefinitions().ToImmutableHashSet(); var uniqueRunnableJobs = GetRunnableJobs(source.GetJobs()).ToImmutableHashSet(); diff --git a/src/BenchmarkDotNet/Configs/ManualConfig.cs b/src/BenchmarkDotNet/Configs/ManualConfig.cs index b10a8634fd..f75ee36f4d 100644 --- a/src/BenchmarkDotNet/Configs/ManualConfig.cs +++ b/src/BenchmarkDotNet/Configs/ManualConfig.cs @@ -3,8 +3,6 @@ using System.ComponentModel; using System.Globalization; using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Diagnosers; @@ -37,14 +35,7 @@ public class ManualConfig : IConfig private readonly List filters = new List(); private readonly List logicalGroupRules = new List(); private readonly List columnHidingRules = new List(); - private readonly Dictionary asyncConsumerTypes = new Dictionary() - { - // Default consumers for Task and ValueTask. - [typeof(Task)] = typeof(TaskConsumer), - [typeof(Task<>)] = typeof(TaskConsumer<>), - [typeof(ValueTask)] = typeof(ValueTaskConsumer), - [typeof(ValueTask<>)] = typeof(ValueTaskConsumer<>), - }; + private readonly HashSet awaitableAdapters = new HashSet(); public IEnumerable GetColumnProviders() => columnProviders; public IEnumerable GetExporters() => exporters; @@ -57,7 +48,7 @@ public class ManualConfig : IConfig public IEnumerable GetFilters() => filters; public IEnumerable GetLogicalGroupRules() => logicalGroupRules; public IEnumerable GetColumnHidingRules() => columnHidingRules; - public IReadOnlyDictionary GetAsyncConsumerTypes() => asyncConsumerTypes; + public IEnumerable GetAsyncAdapterDefinitions() => awaitableAdapters; [PublicAPI] public ConfigOptions Options { get; set; } [PublicAPI] public ConfigUnionRule UnionRule { get; set; } = ConfigUnionRule.Union; @@ -118,50 +109,20 @@ public ManualConfig WithBuildTimeout(TimeSpan buildTimeout) return this; } - // TODO: only pass in asyncConsumerType, get awaitableType from IAsyncConsumer - public ManualConfig AddAsyncConsumer(Type awaitableType, Type asyncConsumerType) + /// + /// Adds an adapter to make a type awaitable. This type must implement or . + /// Optionally provide an async method builder adapter that will be used to consume the awaitable type. This type must implement . + /// If not provided, will be used if it implements , otherwise will be used. + /// + /// + /// If an adapter already exists for the corresponding awaitable type, it will be overridden. + /// + /// The awaitable type adapter. + /// The async method builder adapter. + /// + public ManualConfig AddAsyncAdapter(Type awaitableAdapterType, Type asyncMethodBuilderAdapterType = null) { - // Validate types - bool isPublic = asyncConsumerType.IsPublic || asyncConsumerType.IsNestedPublic; - if (!isPublic || (!asyncConsumerType.IsValueType && asyncConsumerType.GetConstructor(Array.Empty()) == null)) - { - throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] is not a public struct, or a public class with a public, parameterless constructor."); - } - // TODO: handle multiple generics, verify that the open generics are contained in TAwaitable. https://stackoverflow.com/questions/65725729/why-is-checking-equality-of-open-generic-types-inconsistent - bool consumerIsOpenGeneric = asyncConsumerType.IsGenericTypeDefinition; - bool awaitableisOpenGeneric = awaitableType.IsGenericTypeDefinition; - if (consumerIsOpenGeneric != awaitableisOpenGeneric) - { - throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] or awaitableType [{awaitableType}] is an open generic type, while the other is not. Both types must be open or both must be closed."); - } - int consumerOpenGenericCount = asyncConsumerType.GetGenericArguments().Count(t => t.IsGenericParameter); - if (consumerOpenGenericCount > 1) - { - throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] has more than 1 open generic argument. Only 0 or 1 open generic arguments are supported."); - } - int awaitableOpenGenericCount = awaitableType.GetGenericArguments().Count(t => t.IsGenericParameter); - if (awaitableOpenGenericCount > 1) - { - throw new ArgumentException($"awaitableType [{awaitableType}] has more than 1 open generic argument. Only 0 or 1 open generic arguments are supported."); - } - if (consumerOpenGenericCount != awaitableOpenGenericCount) - { - throw new ArgumentException($"awaitableType [{awaitableType}] does not have the same open generic argument count as awaitableType [{awaitableType}]."); - } - - // If the types are open, make closed types for comparison. - // TODO: handle partially closed types. - var closedAwaitableType = awaitableisOpenGeneric ? awaitableType.MakeGenericType(typeof(int)) : awaitableType; - var closedConsumerType = consumerIsOpenGeneric ? asyncConsumerType.MakeGenericType(typeof(int)) : asyncConsumerType; - var iAsyncConsumerType = closedConsumerType.GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncVoidConsumer<,>)) - ?? closedConsumerType.GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncResultConsumer<,,>)) - ?? throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] does not implement IAsyncVoidConsumer or IAsyncResultConsumer."); - if (iAsyncConsumerType.GetGenericArguments()[0] != closedAwaitableType) - { - throw new ArgumentException($"asyncConsumerType [{asyncConsumerType}] does not implement IAsyncConsumer with the expected TAwaitable type [{awaitableType}]."); - } - - asyncConsumerTypes[awaitableType] = asyncConsumerType; + awaitableAdapters.Add(new AsyncAdapterDefinition(awaitableAdapterType, asyncMethodBuilderAdapterType)); return this; } @@ -320,10 +281,7 @@ public void Add(IConfig config) SummaryStyle = config.SummaryStyle ?? SummaryStyle; logicalGroupRules.AddRange(config.GetLogicalGroupRules()); columnHidingRules.AddRange(config.GetColumnHidingRules()); - foreach (var kvp in config.GetAsyncConsumerTypes()) - { - asyncConsumerTypes[kvp.Key] = kvp.Value; - } + awaitableAdapters.AddRange(config.GetAsyncAdapterDefinitions()); Options |= config.Options; BuildTimeout = GetBuildTimeout(BuildTimeout, config.BuildTimeout); } diff --git a/src/BenchmarkDotNet/Engines/AsyncBenchmarkRunner.cs b/src/BenchmarkDotNet/Engines/AsyncBenchmarkRunner.cs new file mode 100644 index 0000000000..397a55cedf --- /dev/null +++ b/src/BenchmarkDotNet/Engines/AsyncBenchmarkRunner.cs @@ -0,0 +1,379 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Helpers; +using Perfolizer.Horology; +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Engines +{ + // Using an interface instead of delegates allows the JIT to inline the call when it's used as a generic struct. + public interface IFunc + { + TResult Invoke(); + } + + public struct EmptyAwaiter : ICriticalNotifyCompletion + { + void ICriticalNotifyCompletion.UnsafeOnCompleted(Action continuation) + => throw new NotImplementedException(); + + void INotifyCompletion.OnCompleted(Action continuation) + => throw new NotImplementedException(); + } + + public abstract class AsyncBenchmarkRunner : IDisposable, ICriticalNotifyCompletion + { + // The continuation callback moves the state machine forward through the builder in the TAsyncMethodBuilderAdapter. + private Action continuation; + + private protected void AdvanceStateMachine() + { + Action action = continuation; + continuation = null; + action(); + } + + void ICriticalNotifyCompletion.UnsafeOnCompleted(Action continuation) + => this.continuation = continuation; + + void INotifyCompletion.OnCompleted(Action continuation) + => this.continuation = continuation; + + public abstract ValueTask Invoke(long repeatCount, IClock clock); + public abstract ValueTask InvokeSingle(); + // TODO: make sure Dispose is called. + public abstract void Dispose(); + } + + public abstract class AsyncBenchmarkRunner : AsyncBenchmarkRunner + where TFunc : struct, IFunc + where TAsyncMethodBuilderAdapter : IAsyncMethodBuilderAdapter, new() + where TAwaitableAdapter : IAwaitableAdapter + where TAwaiter : ICriticalNotifyCompletion + { + private AutoResetValueTaskSource valueTaskSource; + private readonly TAwaitableAdapter awaitableAdapter; + private readonly TFunc func; + private long repeatsRemaining; + private IClock clock; + private bool isDisposed; + + public AsyncBenchmarkRunner(TFunc func, TAwaitableAdapter awaitableAdapter) + { + this.func = func; + this.awaitableAdapter = awaitableAdapter; + } + + public override ValueTask Invoke(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + // The clock is started inside the state machine. + this.clock = clock; + + if (valueTaskSource == null) + { + valueTaskSource = new AutoResetValueTaskSource(); + // Initialize and start the state machine. + StateMachine stateMachine = default; + stateMachine.asyncMethodBuilderAdapter = new (); + stateMachine.asyncMethodBuilderAdapter.CreateAsyncMethodBuilder(); + stateMachine.awaitableAdapter = awaitableAdapter; + stateMachine.owner = this; + stateMachine.func = func; + stateMachine.state = -1; + stateMachine.asyncMethodBuilderAdapter.Start(ref stateMachine); + } + else + { + AdvanceStateMachine(); + } + + return new ValueTask(valueTaskSource, valueTaskSource.Version); + } + + public override void Dispose() + { + // Set the isDisposed flag and advance the state machine to complete the consumer. + isDisposed = true; + AdvanceStateMachine(); + } + + // C# compiler creates struct state machines in Release mode, so we do the same. + private struct StateMachine : IAsyncStateMachine + { + internal AsyncBenchmarkRunner owner; + internal TAsyncMethodBuilderAdapter asyncMethodBuilderAdapter; + internal TAwaitableAdapter awaitableAdapter; + internal TFunc func; + internal int state; + private StartedClock startedClock; + private TAwaiter currentAwaiter; + +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveOptimization)] +#endif + public void MoveNext() + { + try + { + if (state == -1) + { + if (owner.isDisposed) + { + // The owner has been disposed, we complete the consumer. + asyncMethodBuilderAdapter.SetResult(); + return; + } + + // The benchmark has been started, start the clock. + state = 0; + var clock = owner.clock; + owner.clock = default; + startedClock = clock.Start(); + goto StartLoop; + } + + if (state == 1) + { + state = 0; + awaitableAdapter.GetResult(ref currentAwaiter); + currentAwaiter = default; + } + + StartLoop: + while (--owner.repeatsRemaining >= 0) + { + var awaitable = func.Invoke(); + var awaiter = awaitableAdapter.GetAwaiter(ref awaitable); + if (!awaitableAdapter.GetIsCompleted(ref awaiter)) + { + state = 1; + currentAwaiter = awaiter; + asyncMethodBuilderAdapter.AwaitOnCompleted(ref currentAwaiter, ref this); + return; + } + awaitableAdapter.GetResult(ref awaiter); + } + } + catch (Exception e) + { + currentAwaiter = default; + startedClock = default; + owner.valueTaskSource.SetException(e); + return; + } + var clockspan = startedClock.GetElapsed(); + currentAwaiter = default; + startedClock = default; + state = -1; + { + // We hook up the continuation to the owner so the state machine can be advanced when the next benchmark iteration starts. + asyncMethodBuilderAdapter.AwaitOnCompleted(ref owner, ref this); + } + owner.valueTaskSource.SetResult(clockspan); + } + + void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) => asyncMethodBuilderAdapter.SetStateMachine(stateMachine); + } + + public override ValueTask InvokeSingle() + { + var awaitable = func.Invoke(); + + if (null == default(TAwaitable) && awaitable is Task task) + { + return new ValueTask(task); + } + + if (typeof(TAwaitable) == typeof(ValueTask)) + { + return (ValueTask) (object) awaitable; + } + + var awaiter = awaitableAdapter.GetAwaiter(ref awaitable); + if (awaitableAdapter.GetIsCompleted(ref awaiter)) + { + try + { + awaitableAdapter.GetResult(ref awaiter); + } + catch (Exception e) + { + return new ValueTask(Task.FromException(e)); + } + return new ValueTask(); + } + + ToValueTaskVoidStateMachine stateMachine = default; + stateMachine.builder = AsyncValueTaskMethodBuilder.Create(); + stateMachine.awaitableAdapter = awaitableAdapter; + stateMachine.awaiter = awaiter; + stateMachine.builder.Start(ref stateMachine); + return stateMachine.builder.Task; + + // TODO: remove the commented code after the statemachine is verified working. + //var taskCompletionSource = new TaskCompletionSource(); + //awaiter.UnsafeOnCompleted(() => + //{ + // try + // { + // asyncConsumer.GetResult(ref awaiter); + // } + // catch (Exception e) + // { + // taskCompletionSource.SetException(e); + // return; + // } + // taskCompletionSource.SetResult(true); + //}); + //return new ValueTask(taskCompletionSource.Task); + } + + private struct ToValueTaskVoidStateMachine : IAsyncStateMachine + { + internal AsyncValueTaskMethodBuilder builder; + internal TAwaitableAdapter awaitableAdapter; + internal TAwaiter awaiter; + private bool isStarted; + + public void MoveNext() + { + if (!isStarted) + { + isStarted = true; + builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); + return; + } + + try + { + awaitableAdapter.GetResult(ref awaiter); + builder.SetResult(); + } + catch (Exception e) + { + builder.SetException(e); + } + } + + void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) => builder.SetStateMachine(stateMachine); + } + } + + public sealed class AsyncWorkloadRunner + : AsyncBenchmarkRunner.AwaitableAdapter, TAwaitable, TAwaiter> + where TFunc : struct, IFunc + where TAsyncMethodBuilderAdapter : IAsyncMethodBuilderAdapter, new() + where TAwaitableAdapter : IAwaitableAdapter, new() + where TAwaiter : ICriticalNotifyCompletion + { + public AsyncWorkloadRunner(TFunc func) : base(func, new AwaitableAdapter() { userAdapter = new TAwaitableAdapter() }) { } + + public struct AwaitableAdapter : IAwaitableAdapter + { + internal TAwaitableAdapter userAdapter; + + // Make sure the methods are called without inlining. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public TAwaiter GetAwaiter(ref TAwaitable awaitable) + => userAdapter.GetAwaiter(ref awaitable); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public bool GetIsCompleted(ref TAwaiter awaiter) + => userAdapter.GetIsCompleted(ref awaiter); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public void GetResult(ref TAwaiter awaiter) + => userAdapter.GetResult(ref awaiter); + } + } + + public sealed class AsyncOverheadRunner + : AsyncBenchmarkRunner.AwaitableAdapter, TAwaitable, TAwaiter> + where TFunc : struct, IFunc + where TAsyncMethodBuilderAdapter : IAsyncMethodBuilderAdapter, new() + where TAwaiter : ICriticalNotifyCompletion + { + public AsyncOverheadRunner(TFunc func) : base(func, new ()) { } + + public struct AwaitableAdapter : IAwaitableAdapter + { + // Make sure the methods are called without inlining. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public TAwaiter GetAwaiter(ref TAwaitable awaitable) + => default; + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public bool GetIsCompleted(ref TAwaiter awaiter) + => true; + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public void GetResult(ref TAwaiter awaiter) { } + } + } + + public sealed class AsyncWorkloadRunner + : AsyncBenchmarkRunner.AwaitableAdapter, TAwaitable, TAwaiter> + where TFunc : struct, IFunc + where TAsyncMethodBuilderAdapter : IAsyncMethodBuilderAdapter, new() + where TAwaitableAdapter : IAwaitableAdapter, new() + where TAwaiter : ICriticalNotifyCompletion + { + public AsyncWorkloadRunner(TFunc func) : base(func, new AwaitableAdapter() { userAdapter = new TAwaitableAdapter() }) { } + + public struct AwaitableAdapter : IAwaitableAdapter + { + internal TAwaitableAdapter userAdapter; + + // Make sure the methods are called without inlining. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public TAwaiter GetAwaiter(ref TAwaitable awaitable) + => userAdapter.GetAwaiter(ref awaitable); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public bool GetIsCompleted(ref TAwaiter awaiter) + => userAdapter.GetIsCompleted(ref awaiter); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GetResult(ref TAwaiter awaiter) + => GetResultNoInlining(ref awaiter); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private TResult GetResultNoInlining(ref TAwaiter awaiter) + => userAdapter.GetResult(ref awaiter); + } + } + + public sealed class AsyncOverheadRunner + : AsyncBenchmarkRunner.AwaitableAdapter, TAwaitable, TAwaiter> + where TFunc : struct, IFunc + where TAsyncMethodBuilderAdapter : IAsyncMethodBuilderAdapter, new() + where TAwaiter : ICriticalNotifyCompletion + { + public AsyncOverheadRunner(TFunc func) : base(func, new ()) { } + + public struct AwaitableAdapter : IAwaitableAdapter + { + // Make sure the methods are called without inlining. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public TAwaiter GetAwaiter(ref TAwaitable awaitable) + => default; + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public bool GetIsCompleted(ref TAwaiter awaiter) + => true; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GetResult(ref TAwaiter awaiter) + { + GetResultNoInlining(ref awaiter); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] +#pragma warning disable IDE0060 // Remove unused parameter + private void GetResultNoInlining(ref TAwaiter awaiter) { } +#pragma warning restore IDE0060 // Remove unused parameter + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs b/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs deleted file mode 100644 index 30d144b33c..0000000000 --- a/src/BenchmarkDotNet/Engines/AsyncConsumerStateMachine.cs +++ /dev/null @@ -1,481 +0,0 @@ -using BenchmarkDotNet.Helpers; -using Perfolizer.Horology; -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -namespace BenchmarkDotNet.Engines -{ - // Using an interface instead of delegates allows the JIT to inline the call when it's used as a generic struct. - public interface IFunc - { - TResult Invoke(); - } - - internal sealed class AsyncStateMachineAdvancer : ICriticalNotifyCompletion - { - // The continuation callback moves the state machine forward through the builder in the TAsyncConsumer. - private Action continuation; - - internal void Advance() - { - Action action = continuation; - continuation = null; - action(); - } - - void ICriticalNotifyCompletion.UnsafeOnCompleted(Action continuation) - => this.continuation = continuation; - - void INotifyCompletion.OnCompleted(Action continuation) - => this.continuation = continuation; - } - - public struct EmptyAwaiter : ICriticalNotifyCompletion - { - void ICriticalNotifyCompletion.UnsafeOnCompleted(Action continuation) - => throw new NotImplementedException(); - - void INotifyCompletion.OnCompleted(Action continuation) - => throw new NotImplementedException(); - } - - public abstract class AsyncBenchmarkRunner : IDisposable - { - public abstract ValueTask Invoke(long repeatCount, IClock clock); - public abstract ValueTask InvokeSingle(); - public abstract void Dispose(); - } - - public abstract class AsyncBenchmarkRunner : AsyncBenchmarkRunner - where TFunc : struct, IFunc - where TAsyncConsumer : IAsyncVoidConsumer, new() - where TAwaiter : ICriticalNotifyCompletion - { - private readonly AutoResetValueTaskSource valueTaskSource = new (); - private readonly TFunc func; - private long repeatsRemaining; - private IClock clock; - private AsyncStateMachineAdvancer asyncStateMachineAdvancer; - private bool isDisposed; - - public AsyncBenchmarkRunner(TFunc func) - { - this.func = func; - } - - private void MaybeInitializeStateMachine() - { - if (asyncStateMachineAdvancer != null) - { - return; - } - - // Initialize the state machine and consumer before the workload starts. - asyncStateMachineAdvancer = new (); - StateMachine stateMachine = default; - stateMachine.consumer = new (); - stateMachine.consumer.CreateAsyncMethodBuilder(); - stateMachine.owner = this; - stateMachine.stateMachineAdvancer = asyncStateMachineAdvancer; - stateMachine.func = func; - stateMachine.state = -1; - stateMachine.consumer.Start(ref stateMachine); - } - - public override ValueTask Invoke(long repeatCount, IClock clock) - { - MaybeInitializeStateMachine(); - repeatsRemaining = repeatCount; - // The clock is started inside the state machine. - this.clock = clock; - asyncStateMachineAdvancer.Advance(); - this.clock = default; - return new ValueTask(valueTaskSource, valueTaskSource.Version); - } - - // TODO: make sure Dispose is called. - public override void Dispose() - { - // Set the isDisposed flag and advance the state machine to complete the consumer. - isDisposed = true; - asyncStateMachineAdvancer?.Advance(); - } - - // C# compiler creates struct state machines in Release mode, so we do the same. - private struct StateMachine : IAsyncStateMachine - { - internal AsyncBenchmarkRunner owner; - internal AsyncStateMachineAdvancer stateMachineAdvancer; - internal TAsyncConsumer consumer; - internal TFunc func; - internal int state; - private StartedClock startedClock; - private TAwaiter currentAwaiter; - -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveOptimization)] -#endif - public void MoveNext() - { - try - { - if (state < 0) - { - if (state == -1) - { - // This is called when we call asyncConsumer.Start, so we just hook up the continuation - // to the advancer so the state machine can be moved forward when the benchmark starts. - state = -2; - consumer.AwaitOnCompleted(ref stateMachineAdvancer, ref this); - return; - } - - if (owner.isDisposed) - { - // The owner has been disposed, we complete the consumer. - consumer.SetResult(); - return; - } - - // The benchmark has been started, start the clock. - state = 0; - startedClock = owner.clock.Start(); - goto StartLoop; - } - - if (state == 1) - { - state = 0; - consumer.GetResult(ref currentAwaiter); - currentAwaiter = default; - } - - StartLoop: - while (--owner.repeatsRemaining >= 0) - { - var awaitable = func.Invoke(); - var awaiter = consumer.GetAwaiter(ref awaitable); - if (!consumer.GetIsCompleted(ref awaiter)) - { - state = 1; - currentAwaiter = awaiter; - consumer.AwaitOnCompleted(ref currentAwaiter, ref this); - return; - } - consumer.GetResult(ref awaiter); - } - } - catch (Exception e) - { - currentAwaiter = default; - startedClock = default; - owner.valueTaskSource.SetException(e); - return; - } - var clockspan = startedClock.GetElapsed(); - currentAwaiter = default; - startedClock = default; - state = -2; - { - // We hook up the continuation to the advancer so the state machine can be moved forward when the next benchmark iteration starts. - consumer.AwaitOnCompleted(ref stateMachineAdvancer, ref this); - } - owner.valueTaskSource.SetResult(clockspan); - } - - void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) => consumer.SetStateMachine(stateMachine); - } - - public override ValueTask InvokeSingle() - { - var asyncConsumer = new TAsyncConsumer(); - var awaitable = func.Invoke(); - - if (null == default(TAwaitable) && awaitable is Task task) - { - return new ValueTask(task); - } - - if (typeof(TAwaitable) == typeof(ValueTask)) - { - return (ValueTask) (object) awaitable; - } - - var awaiter = asyncConsumer.GetAwaiter(ref awaitable); - if (asyncConsumer.GetIsCompleted(ref awaiter)) - { - try - { - asyncConsumer.GetResult(ref awaiter); - } - catch (Exception e) - { - return new ValueTask(Task.FromException(e)); - } - return new ValueTask(); - } - - ToValueTaskVoidStateMachine stateMachine = default; - stateMachine.builder = AsyncValueTaskMethodBuilder.Create(); - stateMachine.consumer = asyncConsumer; - stateMachine.awaiter = awaiter; - stateMachine.builder.Start(ref stateMachine); - return stateMachine.builder.Task; - - // TODO: remove the commented code after the statemachine is verified working. - //var taskCompletionSource = new TaskCompletionSource(); - //awaiter.UnsafeOnCompleted(() => - //{ - // try - // { - // asyncConsumer.GetResult(ref awaiter); - // } - // catch (Exception e) - // { - // taskCompletionSource.SetException(e); - // return; - // } - // taskCompletionSource.SetResult(true); - //}); - //return new ValueTask(taskCompletionSource.Task); - } - - private struct ToValueTaskVoidStateMachine : IAsyncStateMachine - { - internal AsyncValueTaskMethodBuilder builder; - internal TAsyncConsumer consumer; - internal TAwaiter awaiter; - private bool isStarted; - - public void MoveNext() - { - if (!isStarted) - { - isStarted = true; - builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); - return; - } - - try - { - consumer.GetResult(ref awaiter); - builder.SetResult(); - } - catch (Exception e) - { - builder.SetException(e); - } - } - - void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) => builder.SetStateMachine(stateMachine); - } - } - - public sealed class AsyncWorkloadRunner - : AsyncBenchmarkRunner.AsyncConsumer, TAwaitable, TAwaiter> - where TFunc : struct, IFunc - where TAsyncConsumer : IAsyncVoidConsumer, new() - where TAwaiter : ICriticalNotifyCompletion - { - public AsyncWorkloadRunner(TFunc func) : base(func) { } - - public struct AsyncConsumer : IAsyncVoidConsumer - { - internal TAsyncConsumer asyncConsumer; - - public void CreateAsyncMethodBuilder() - { - asyncConsumer = new (); - asyncConsumer.CreateAsyncMethodBuilder(); - } - - public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine - => asyncConsumer.Start(ref stateMachine); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AwaitOnCompleted(ref TAnyAwaiter awaiter, ref TStateMachine stateMachine) - where TAnyAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine - => asyncConsumer.AwaitOnCompleted(ref awaiter, ref stateMachine); - - public void SetResult() - => asyncConsumer.SetResult(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetStateMachine(IAsyncStateMachine stateMachine) - => asyncConsumer.SetStateMachine(stateMachine); - - // Make sure the methods are called without inlining. - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public TAwaiter GetAwaiter(ref TAwaitable awaitable) - => asyncConsumer.GetAwaiter(ref awaitable); - - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public bool GetIsCompleted(ref TAwaiter awaiter) - => asyncConsumer.GetIsCompleted(ref awaiter); - - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public void GetResult(ref TAwaiter awaiter) - => asyncConsumer.GetResult(ref awaiter); - } - } - - // TODO: pass overhead types explicitly (TAwaitableOverhead, TAwaiterOverhead), only use EmptyAwaiter for non-primitive struct types. - public sealed class AsyncOverheadRunner - : AsyncBenchmarkRunner.AsyncConsumer, TAwaitableOverhead, TAwaiterOverhead> - where TFunc : struct, IFunc - where TAsyncConsumer : IAsyncVoidConsumer, new() - where TAwaiter : ICriticalNotifyCompletion - where TAwaiterOverhead : ICriticalNotifyCompletion - { - public AsyncOverheadRunner(TFunc func) : base(func) { } - - public struct AsyncConsumer : IAsyncVoidConsumer - { - internal TAsyncConsumer asyncConsumer; - - public void CreateAsyncMethodBuilder() - { - asyncConsumer = new (); - asyncConsumer.CreateAsyncMethodBuilder(); - } - - public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine - => asyncConsumer.Start(ref stateMachine); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AwaitOnCompleted(ref TAnyAwaiter awaiter, ref TStateMachine stateMachine) - where TAnyAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine - => asyncConsumer.AwaitOnCompleted(ref awaiter, ref stateMachine); - - public void SetResult() - => asyncConsumer.SetResult(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetStateMachine(IAsyncStateMachine stateMachine) - => asyncConsumer.SetStateMachine(stateMachine); - - // Make sure the methods are called without inlining. - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public TAwaiterOverhead GetAwaiter(ref TAwaitableOverhead awaitable) - => default; - - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public bool GetIsCompleted(ref TAwaiterOverhead awaiter) - => true; - - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public void GetResult(ref TAwaiterOverhead awaiter) { } - } - } - - public sealed class AsyncWorkloadRunner - : AsyncBenchmarkRunner.AsyncConsumer, TAwaitable, TAwaiter> - where TFunc : struct, IFunc - where TAsyncConsumer : IAsyncResultConsumer, new() - where TAwaiter : ICriticalNotifyCompletion - { - public AsyncWorkloadRunner(TFunc func) : base(func) { } - - public struct AsyncConsumer : IAsyncVoidConsumer - { - internal TAsyncConsumer asyncConsumer; - - public void CreateAsyncMethodBuilder() - { - asyncConsumer = new (); - asyncConsumer.CreateAsyncMethodBuilder(); - } - - public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine - => asyncConsumer.Start(ref stateMachine); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AwaitOnCompleted(ref TAnyAwaiter awaiter, ref TStateMachine stateMachine) - where TAnyAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine - => asyncConsumer.AwaitOnCompleted(ref awaiter, ref stateMachine); - - public void SetResult() - => asyncConsumer.SetResult(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetStateMachine(IAsyncStateMachine stateMachine) - => asyncConsumer.SetStateMachine(stateMachine); - - // Make sure the methods are called without inlining. - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public TAwaiter GetAwaiter(ref TAwaitable awaitable) - => asyncConsumer.GetAwaiter(ref awaitable); - - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public bool GetIsCompleted(ref TAwaiter awaiter) - => asyncConsumer.GetIsCompleted(ref awaiter); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void GetResult(ref TAwaiter awaiter) - => GetResultNoInlining(ref awaiter); - - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - private TResult GetResultNoInlining(ref TAwaiter awaiter) - => asyncConsumer.GetResult(ref awaiter); - } - } - - public sealed class AsyncOverheadRunner - : AsyncBenchmarkRunner.AsyncConsumer, TAwaitableOverhead, TAwaiterOverhead> - where TFunc : struct, IFunc - where TAsyncConsumer : IAsyncResultConsumer, new() - where TAwaiter : ICriticalNotifyCompletion - where TAwaiterOverhead : ICriticalNotifyCompletion - { - public AsyncOverheadRunner(TFunc func) : base(func) { } - - public struct AsyncConsumer : IAsyncVoidConsumer - { - internal TAsyncConsumer asyncConsumer; - - public void CreateAsyncMethodBuilder() - { - asyncConsumer = new (); - asyncConsumer.CreateAsyncMethodBuilder(); - } - - public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine - => asyncConsumer.Start(ref stateMachine); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AwaitOnCompleted(ref TAnyAwaiter awaiter, ref TStateMachine stateMachine) - where TAnyAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine - => asyncConsumer.AwaitOnCompleted(ref awaiter, ref stateMachine); - - public void SetResult() - => asyncConsumer.SetResult(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetStateMachine(IAsyncStateMachine stateMachine) - => asyncConsumer.SetStateMachine(stateMachine); - - // Make sure the methods are called without inlining. - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public TAwaiterOverhead GetAwaiter(ref TAwaitableOverhead awaitable) - => default; - - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public bool GetIsCompleted(ref TAwaiterOverhead awaiter) - => true; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void GetResult(ref TAwaiterOverhead awaiter) - { - GetResultNoInlining(ref awaiter); - } - - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - private void GetResultNoInlining(ref TAwaiterOverhead awaiter) { } - } - } -} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Engines/AsyncConsumers.cs b/src/BenchmarkDotNet/Engines/AsyncConsumers.cs deleted file mode 100644 index 092c23d6d6..0000000000 --- a/src/BenchmarkDotNet/Engines/AsyncConsumers.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -namespace BenchmarkDotNet.Engines -{ - public interface IAwaitableConverter - where TAwaiter : ICriticalNotifyCompletion - { - public TAwaiter GetAwaiter(ref TAwaitable awaitable); - public bool GetIsCompleted(ref TAwaiter awaiter); - } - - public interface IAwaitableVoidConverter : IAwaitableConverter - where TAwaiter : ICriticalNotifyCompletion - { - public void GetResult(ref TAwaiter awaiter); - } - - public interface IAwaitableResultConverter : IAwaitableConverter - where TAwaiter : ICriticalNotifyCompletion - { - public TResult GetResult(ref TAwaiter awaiter); - } - - public interface IAsyncMethodBuilder - { - public void CreateAsyncMethodBuilder(); - public void Start(ref TStateMachine stateMachine) - where TStateMachine : IAsyncStateMachine; - public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) - where TAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine; - public void SetStateMachine(IAsyncStateMachine stateMachine); - public void SetResult(); - } - - public interface IAsyncVoidConsumer : IAwaitableVoidConverter, IAsyncMethodBuilder - where TAwaiter : ICriticalNotifyCompletion - { - } - - public interface IAsyncResultConsumer : IAwaitableResultConverter, IAsyncMethodBuilder - where TAwaiter : ICriticalNotifyCompletion - { - } - - // We use a type that users cannot access to prevent the async method builder from being jitted with the user's type, in case the benchmark is ran with ColdStart. - internal struct UnusedStruct { } - - // We use ConfigureAwait(false) to prevent dead-locks with InProcess toolchains (it could be ran on a thread with a SynchronizationContext). - // Using struct rather than class forces the JIT to generate specialized code that can be inlined, and avoids an extra allocation. - public struct TaskConsumer : IAsyncVoidConsumer - { - private AsyncTaskMethodBuilder builder; - - public void CreateAsyncMethodBuilder() - => builder = AsyncTaskMethodBuilder.Create(); - - public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine - => builder.Start(ref stateMachine); - - public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) - where TAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine - => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); - - public void SetResult() - => builder.SetResult(default); - - public void SetStateMachine(IAsyncStateMachine stateMachine) - => builder.SetStateMachine(stateMachine); - - public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter(ref Task awaitable) - => awaitable.ConfigureAwait(false).GetAwaiter(); - - public bool GetIsCompleted(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) - => awaiter.IsCompleted; - - public void GetResult(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) - => awaiter.GetResult(); - } - - public struct TaskConsumer : IAsyncResultConsumer, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter, T> - { - private AsyncTaskMethodBuilder builder; - - public void CreateAsyncMethodBuilder() - => builder = AsyncTaskMethodBuilder.Create(); - - public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine - => builder.Start(ref stateMachine); - - public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) - where TAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine - => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); - - public void SetResult() - => builder.SetResult(default); - - public void SetStateMachine(IAsyncStateMachine stateMachine) - => builder.SetStateMachine(stateMachine); - - public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter(ref Task awaitable) - => awaitable.ConfigureAwait(false).GetAwaiter(); - - public bool GetIsCompleted(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) - => awaiter.IsCompleted; - - public T GetResult(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) - => awaiter.GetResult(); - } - - public struct ValueTaskConsumer : IAsyncVoidConsumer - { - private AsyncValueTaskMethodBuilder builder; - - public void CreateAsyncMethodBuilder() - => builder = AsyncValueTaskMethodBuilder.Create(); - - public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine - => builder.Start(ref stateMachine); - - public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) - where TAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine - => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); - - public void SetResult() - => builder.SetResult(default); - - public void SetStateMachine(IAsyncStateMachine stateMachine) - => builder.SetStateMachine(stateMachine); - - public ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter GetAwaiter(ref ValueTask awaitable) - => awaitable.ConfigureAwait(false).GetAwaiter(); - - public bool GetIsCompleted(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) - => awaiter.IsCompleted; - - public void GetResult(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) - => awaiter.GetResult(); - } - - public struct ValueTaskConsumer : IAsyncResultConsumer, ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter, T> - { - private AsyncValueTaskMethodBuilder builder; - - public void CreateAsyncMethodBuilder() - => builder = AsyncValueTaskMethodBuilder.Create(); - - public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine - => builder.Start(ref stateMachine); - - public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) - where TAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine - => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); - - public void SetResult() - => builder.SetResult(default); - - public void SetStateMachine(IAsyncStateMachine stateMachine) - => builder.SetStateMachine(stateMachine); - - public ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter GetAwaiter(ref ValueTask awaitable) - => awaitable.ConfigureAwait(false).GetAwaiter(); - - public bool GetIsCompleted(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) - => awaiter.IsCompleted; - - public T GetResult(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) - => awaiter.GetResult(); - } -} diff --git a/src/BenchmarkDotNet/Engines/AsyncMethodBuilderAdapters.cs b/src/BenchmarkDotNet/Engines/AsyncMethodBuilderAdapters.cs new file mode 100644 index 0000000000..c73afaad72 --- /dev/null +++ b/src/BenchmarkDotNet/Engines/AsyncMethodBuilderAdapters.cs @@ -0,0 +1,66 @@ +using BenchmarkDotNet.Configs; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Engines +{ + // Using struct rather than class forces the JIT to generate specialized code for direct calls instead of virtual, and avoids an extra allocation. + public struct AsyncTaskMethodBuilderAdapter : IAsyncMethodBuilderAdapter + { + // We use a type that users cannot access to prevent the async method builder from being pre-jitted with the user's type, in case the benchmark is ran with ColdStart. + private struct EmptyStruct { } + + private AsyncTaskMethodBuilder builder; + + public void CreateAsyncMethodBuilder() + => builder = AsyncTaskMethodBuilder.Create(); + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => builder.Start(ref stateMachine); + + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + + public void SetResult() + => builder.SetResult(default); + + public void SetStateMachine(IAsyncStateMachine stateMachine) + => builder.SetStateMachine(stateMachine); + } + + public struct AsyncValueTaskMethodBuilderAdapter : IAsyncMethodBuilderAdapter + { + // We use a type that users cannot access to prevent the async method builder from being pre-jitted with the user's type, in case the benchmark is ran with ColdStart. + private struct EmptyStruct { } + + private AsyncValueTaskMethodBuilder builder; + + public void CreateAsyncMethodBuilder() + => builder = AsyncValueTaskMethodBuilder.Create(); + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => builder.Start(ref stateMachine); + + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + + public void SetResult() + => builder.SetResult(default); + + public void SetStateMachine(IAsyncStateMachine stateMachine) + => builder.SetStateMachine(stateMachine); + + public ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter GetAwaiter(ref ValueTask awaitable) + => awaitable.ConfigureAwait(false).GetAwaiter(); + + public bool GetIsCompleted(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) + => awaiter.IsCompleted; + + public void GetResult(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) + => awaiter.GetResult(); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Engines/AwaitableAdapters.cs b/src/BenchmarkDotNet/Engines/AwaitableAdapters.cs new file mode 100644 index 0000000000..f8cfda9355 --- /dev/null +++ b/src/BenchmarkDotNet/Engines/AwaitableAdapters.cs @@ -0,0 +1,40 @@ +using BenchmarkDotNet.Configs; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Engines +{ + // Using struct rather than class forces the JIT to generate specialized code for direct calls instead of virtual, and avoids an extra allocation. + public struct TaskAdapter : IAwaitableAdapter + { + // We use ConfigureAwait(false) to prevent dead-locks with InProcess toolchains (it could be ran on a thread with a SynchronizationContext). + public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter(ref Task awaitable) => awaitable.ConfigureAwait(false).GetAwaiter(); + public bool GetIsCompleted(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => awaiter.IsCompleted; + public void GetResult(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => awaiter.GetResult(); + } + + public struct TaskAdapter : IAwaitableAdapter, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter, TResult> + { + // We use ConfigureAwait(false) to prevent dead-locks with InProcess toolchains (it could be ran on a thread with a SynchronizationContext). + public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter(ref Task awaitable) => awaitable.ConfigureAwait(false).GetAwaiter(); + public bool GetIsCompleted(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => awaiter.IsCompleted; + public TResult GetResult(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => awaiter.GetResult(); + } + + // Using struct rather than class forces the JIT to generate specialized code that can be inlined, and avoids an extra allocation. + public struct ValueTaskAdapter : IAwaitableAdapter + { + // We use ConfigureAwait(false) to prevent dead-locks with InProcess toolchains (it could be ran on a thread with a SynchronizationContext). + public ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter GetAwaiter(ref ValueTask awaitable) => awaitable.ConfigureAwait(false).GetAwaiter(); + public bool GetIsCompleted(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) => awaiter.IsCompleted; + public void GetResult(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) => awaiter.GetResult(); + } + + public struct ValueTaskAdapter : IAwaitableAdapter, ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter, TResult> + { + // We use ConfigureAwait(false) to prevent dead-locks with InProcess toolchains (it could be ran on a thread with a SynchronizationContext). + public ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter GetAwaiter(ref ValueTask awaitable) => awaitable.ConfigureAwait(false).GetAwaiter(); + public bool GetIsCompleted(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) => awaiter.IsCompleted; + public TResult GetResult(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) => awaiter.GetResult(); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs index a3c28b4773..9b28a275d4 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs @@ -1,15 +1,10 @@ using System; -using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; -using System.Threading.Tasks; using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Engines; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Running; -using JetBrains.Annotations; - namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit { /// Helper class that creates instances. @@ -32,10 +27,8 @@ private static BenchmarkAction CreateCore( if (resultType == typeof(void)) return new BenchmarkActionVoid(resultInstance, targetMethod, unrollFactor); - if (config.GetIsAwaitable(resultType, out var asyncConsumerType)) - { - return CreateBenchmarkActionAwaitable(asyncConsumerType, resultType, resultInstance, targetMethod, unrollFactor); - } + if (config.GetIsAwaitable(resultType, out var adapter)) + return CreateBenchmarkActionAwaitable(adapter, resultInstance, targetMethod, unrollFactor); if (targetMethod == null && resultType.GetTypeInfo().IsValueType) // for Idle: we return int because creating bigger ValueType could take longer than benchmarked method itself. @@ -48,28 +41,18 @@ private static BenchmarkAction CreateCore( unrollFactor); } - private static BenchmarkActionBase CreateBenchmarkActionAwaitable(Type asyncConsumerType, Type awaitableType, object instance, MethodInfo method, int unrollFactor) + private static BenchmarkActionBase CreateBenchmarkActionAwaitable(ConcreteAsyncAdapter adapter, object instance, MethodInfo method, int unrollFactor) { - var asyncConsumerInterfaceType = asyncConsumerType.GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncVoidConsumer<,>)); - bool isVoidConsumer = asyncConsumerInterfaceType?.GetGenericArguments()[0] == awaitableType; - if (!isVoidConsumer) - { - asyncConsumerInterfaceType = asyncConsumerType.GetInterfaces().First(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAsyncResultConsumer<,,>)); - } - - Type[] genericArguments = asyncConsumerInterfaceType.GetGenericArguments(); - Type awaiterType = genericArguments[1]; - - if (isVoidConsumer) + if (adapter.resultType == null) { return (BenchmarkActionBase) Activator.CreateInstance( - typeof(BenchmarkActionAwaitable<,,>).MakeGenericType(asyncConsumerType, awaitableType, awaiterType), + typeof(BenchmarkActionAwaitable<,,,>).MakeGenericType(adapter.asyncMethodBuilderAdapterType, adapter.awaitableAdapterType, adapter.awaitableType, adapter.awaiterType), instance, method, unrollFactor); } return (BenchmarkActionBase) Activator.CreateInstance( - typeof(BenchmarkActionAwaitable<,,,>).MakeGenericType(asyncConsumerType, awaitableType, awaiterType, genericArguments[2]), + typeof(BenchmarkActionAwaitable<,,,,>).MakeGenericType(adapter.asyncMethodBuilderAdapterType, adapter.awaitableAdapterType, adapter.awaitableType, adapter.awaiterType, adapter.resultType), instance, method, unrollFactor); diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs index 03b1fc3c0e..d86d40b19f 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs @@ -1,4 +1,5 @@ -using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Engines; using Perfolizer.Horology; using System; using System.Reflection; @@ -110,8 +111,9 @@ private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock cl public TAwaitable Invoke() => callback(); } - internal class BenchmarkActionAwaitable : BenchmarkActionBase - where TAsyncConsumer : struct, IAsyncVoidConsumer + internal class BenchmarkActionAwaitable : BenchmarkActionBase + where TAsyncMethodBuilderAdapter : IAsyncMethodBuilderAdapter, new() + where TAwaitableAdapter : IAwaitableAdapter, new() where TAwaiter : ICriticalNotifyCompletion { private readonly AsyncBenchmarkRunner asyncBenchmarkRunner; @@ -122,11 +124,11 @@ public BenchmarkActionAwaitable(object instance, MethodInfo method, int unrollFa if (!isIdle) { var callback = CreateWorkload>(instance, method); - asyncBenchmarkRunner = new AsyncWorkloadRunner, TAsyncConsumer, TAwaitable, TAwaiter>(new AwaitableFunc(callback)); + asyncBenchmarkRunner = new AsyncWorkloadRunner, TAsyncMethodBuilderAdapter, TAwaitableAdapter, TAwaitable, TAwaiter>(new (callback)); } else { - asyncBenchmarkRunner = new AsyncOverheadRunner, TAsyncConsumer, TAwaitable, TAwaiter, TAwaitable, TAwaiter>(new AwaitableFunc(Overhead)); + asyncBenchmarkRunner = new AsyncOverheadRunner, TAsyncMethodBuilderAdapter, TAwaitable, TAwaiter>(new (Overhead)); } InvokeSingle = InvokeSingleHardcoded; InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; @@ -141,8 +143,9 @@ private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock cl => asyncBenchmarkRunner.Invoke(repeatCount, clock); } - internal class BenchmarkActionAwaitable : BenchmarkActionBase - where TAsyncConsumer : struct, IAsyncResultConsumer + internal class BenchmarkActionAwaitable : BenchmarkActionBase + where TAsyncMethodBuilderAdapter : IAsyncMethodBuilderAdapter, new() + where TAwaitableAdapter : IAwaitableAdapter, new() where TAwaiter : ICriticalNotifyCompletion { private readonly AsyncBenchmarkRunner asyncBenchmarkRunner; @@ -153,11 +156,11 @@ public BenchmarkActionAwaitable(object instance, MethodInfo method, int unrollFa if (!isIdle) { var callback = CreateWorkload>(instance, method); - asyncBenchmarkRunner = new AsyncWorkloadRunner, TAsyncConsumer, TAwaitable, TAwaiter, TResult>(new AwaitableFunc(callback)); + asyncBenchmarkRunner = new AsyncWorkloadRunner, TAsyncMethodBuilderAdapter, TAwaitableAdapter, TAwaitable, TAwaiter, TResult>(new (callback)); } else { - asyncBenchmarkRunner = new AsyncOverheadRunner, TAsyncConsumer, TAwaitable, TAwaiter, TAwaitable, TAwaiter, TResult>(new AwaitableFunc(Overhead)); + asyncBenchmarkRunner = new AsyncOverheadRunner, TAsyncMethodBuilderAdapter, TAwaitable, TAwaiter, TResult>(new (Overhead)); } InvokeSingle = InvokeSingleHardcoded; InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded;