diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs index e60dfe2699..917110e807 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) @@ -155,15 +158,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 ddf78eb572..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 $"() => {method.Name}().GetAwaiter().GetResult()"; + return $"() => BenchmarkDotNet.Helpers.AwaitHelper.ToValueTaskVoid({method.Name}())"; } - return method.Name; + return $"() => {{ {method.Name}(); return new System.Threading.Tasks.ValueTask(); }}"; } } @@ -145,34 +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) { } - // 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(); }}"; - - public override string GetWorkloadMethodCall(string passArguments) => $"{Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult()"; + 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()});"; - // 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(); }}"; + protected override Type OverheadMethodReturnType => WorkloadMethodReturnType; - public override string GetWorkloadMethodCall(string passArguments) => $"{Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult()"; + 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..c843ae841a 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; } @@ -46,13 +47,14 @@ public class Engine : IEngine private readonly EngineActualStage actualStage; private readonly bool includeExtraStats; private readonly Random random; + private readonly Helpers.AwaitHelper awaitHelper; 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; @@ -84,13 +86,14 @@ internal Engine( actualStage = new EngineActualStage(this); random = new Random(12345); // we are using constant seed to try to get repeatable results + awaitHelper = new Helpers.AwaitHelper(); } public void Dispose() { try { - GlobalCleanupAction?.Invoke(); + awaitHelper.GetResult(GlobalCleanupAction.Invoke()); } catch (Exception e) { @@ -155,7 +158,7 @@ public Measurement RunIteration(IterationData data) var action = isOverhead ? OverheadAction : WorkloadAction; if (!isOverhead) - IterationSetupAction(); + awaitHelper.GetResult(IterationSetupAction()); GcCollect(); @@ -165,15 +168,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 = awaitHelper.GetResult(op); if (EngineEventSource.Log.IsEnabled()) EngineEventSource.Log.IterationStop(data.IterationMode, data.IterationStage, totalOperations); if (!isOverhead) - IterationCleanupAction(); + awaitHelper.GetResult(IterationCleanupAction()); if (randomizeMemory) RandomizeManagedHeapMemory(); @@ -196,17 +198,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 + 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); + awaitHelper.GetResult(op); var finalGcStats = GcStats.ReadFinal(); var finalThreadingStats = ThreadingStats.ReadFinal(); - IterationCleanupAction(); // we run iteration cleanup after collecting GC stats + 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 +223,14 @@ private void Consume(in Span _) { } private void RandomizeManagedHeapMemory() { // invoke global cleanup before global setup - GlobalCleanupAction?.Invoke(); + 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(); + 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 new file mode 100644 index 0000000000..f72e1ffacf --- /dev/null +++ b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs @@ -0,0 +1,102 @@ +using System; +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(); + } + + 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); + } + } +} 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 060c977014..62cc39bc3b 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; @@ -15,37 +17,23 @@ public ConsumableTypeInfo(Type methodReturnType) if (methodReturnType == null) throw new ArgumentNullException(nameof(methodReturnType)); - OriginMethodReturnType = methodReturnType; + WorkloadMethodReturnType = methodReturnType; - // Please note this code does not support await over extension methods. - var getAwaiterMethod = methodReturnType.GetMethod(nameof(Task.GetAwaiter), BindingFlagsPublicInstance); - if (getAwaiterMethod == null) - { - WorkloadMethodReturnType = methodReturnType; - } - else - { - var getResultMethod = getAwaiterMethod - .ReturnType - .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlagsPublicInstance); - - if (getResultMethod == null) - { - WorkloadMethodReturnType = methodReturnType; - } - else - { - WorkloadMethodReturnType = getResultMethod.ReturnType; - GetAwaiterMethod = getAwaiterMethod; - GetResultMethod = getResultMethod; - } - } + // 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 (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; @@ -71,24 +59,17 @@ 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 GetAwaiterMethod { get; } - [CanBeNull] - public MethodInfo GetResultMethod { get; } - public bool IsVoid { get; } public bool IsByRef { get; } public bool IsConsumable { get; } [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/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 91e97cc71e..883f9e0a80 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,18 +242,22 @@ 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; + private ConsumableTypeInfo globalCleanupReturnInfo; + private ConsumableTypeInfo iterationSetupReturnInfo; + private ConsumableTypeInfo iterationCleanupReturnInfo; private FieldBuilder globalSetupActionField; 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; @@ -261,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; @@ -277,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) @@ -286,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] @@ -320,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); @@ -350,13 +359,23 @@ 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); + iterationSetupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.IterationSetupMethod?.ReturnType); + iterationCleanupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.IterationCleanupMethod?.ReturnType); // Init types runnableBuilder = DefineRunnableTypeBuilder(benchmark, moduleBuilder); @@ -364,6 +383,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 @@ -413,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 = @@ -564,162 +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 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); - - /* - // 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) - */ - 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); - - /* - 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 /* @@ -747,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; @@ -831,19 +710,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(); @@ -868,31 +734,20 @@ .locals init ( EmitLoadArgFieldsToLocals(ilBuilder, argLocals, skipFirstArg); /* - // 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); - } - /* IL_0018: ret */ @@ -916,53 +771,133 @@ .locals init ( 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); + // 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); + 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 methodBuilder = runnableBuilder.DefineNonVirtualInstanceMethod( + methodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask))); var ilBuilder = methodBuilder.GetILGenerator(); - if (optionalTargetMethod != null) - EmitNoArgsMethodCallPopReturn(methodBuilder, optionalTargetMethod, ilBuilder, forceDirectCall: true); + if (returnTypeInfo?.IsAwaitable == true) + { + 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; } + private void EmitAwaitableSetupTeardown( + MethodBuilder methodBuilder, + MethodInfo targetMethod, + ILGenerator ilBuilder, + ConsumableTypeInfo returnTypeInfo) + { + if (targetMethod == null) + throw new ArgumentNullException(nameof(targetMethod)); + + // BenchmarkDotNet.Helpers.AwaitHelper.ToValueTaskVoid(workloadDelegate()); + /* + // call for instance + // 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_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) + { + 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); + } + ilBuilder.Emit(OpCodes.Call, GetToValueTaskMethod(returnTypeInfo.WorkloadMethodReturnType)); + } + + private static MethodInfo GetToValueTaskMethod(Type taskType) + { + var taskYieldType = taskType + .GetMethod(nameof(Task.GetAwaiter), BindingFlagsPublicInstance) + .ReturnType + .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlagsPublicInstance) + .ReturnType; + if (taskYieldType != typeof(void)) + { + Type compareType = taskType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) + ? typeof(Task<>) + : typeof(ValueTask<>); + return typeof(Helpers.AwaitHelper).GetMethods(BindingFlagsPublicInstance) + .First(m => + { + if (m.Name != nameof(Helpers.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 Type[1] { taskYieldType }); + } + else + { + return typeof(Helpers.AwaitHelper).GetMethod(nameof(Helpers.AwaitHelper.ToValueTaskVoid), BindingFlagsPublicStatic, null, new Type[1] { taskType }, null); + } + } + private void EmitCtorBody() { var ilBuilder = ctorMethod.GetILGenerator(); 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 ef351975a1..603466ed05 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(); @@ -98,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 eef3ce8997..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,123 +105,469 @@ 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() => startTaskCallback.Invoke().GetAwaiter().GetResult(); + 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 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 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() => startTaskCallback().GetAwaiter().GetResult(); + 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; } + 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); + 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() + { + 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, 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 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() => startTaskCallback().GetAwaiter().GetResult(); + 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 0774339ad2..250e040786 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(); @@ -97,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 5ca5592e1e..9c9cbf758f 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,142 +136,461 @@ 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; + startedClock = clock.Start(); + try { - unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + while (--repeatsRemaining >= 0) + { + callback(); + } } - else + catch (Exception) { - InvokeMultiple = EmitInvokeMultiple(this, nameof(callback), null, unrollFactor); + 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() => startTaskCallback.Invoke().GetAwaiter().GetResult(); + 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; + startedClock = clock.Start(); + try { - unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + while (--repeatsRemaining >= 0) + { + callback(); + } } - else + catch (Exception) { - InvokeMultiple = EmitInvokeMultiple(this, nameof(callback), nameof(result), unrollFactor); + 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() => startTaskCallback().GetAwaiter().GetResult(); + 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; } + 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, BenchmarkActionCodegen codegenMode, 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 ValueTask Overhead() => default; + + private ValueTask InvokeSingleHardcodedOverhead() + { + callback(); + return new ValueTask(); + } + + private ValueTask InvokeNoUnrollHardcodedOverhead(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + startedClock = clock.Start(); + try + { + while (--repeatsRemaining >= 0) + { + callback(); + } + } + catch (Exception) + { + 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) + { + 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; + startedClock = clock.Start(); + try { - unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + while (--repeatsRemaining >= 0) + { + callback(); + } } - else + catch (Exception) { - InvokeMultiple = EmitInvokeMultiple(this, nameof(callback), nameof(result), unrollFactor); + 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() => startTaskCallback().GetAwaiter().GetResult(); + 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/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs b/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs index 92d7422ae2..371bdb5ece 100644 --- a/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs +++ b/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs @@ -143,7 +143,7 @@ private void TryToGetTaskResult(object result) } else if (result is ValueTask valueTask) { - valueTask.GetAwaiter().GetResult(); + valueTask.AsTask().GetAwaiter().GetResult(); } } diff --git a/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs b/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs index b1e268673c..db967c6d76 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs @@ -10,6 +10,8 @@ using BenchmarkDotNet.Reports; using BenchmarkDotNet.Characteristics; using Perfolizer.Mathematics.OutlierDetection; +using System.Threading.Tasks; +using Perfolizer.Horology; namespace BenchmarkDotNet.IntegrationTests { @@ -90,10 +92,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 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.IntegrationTests/InProcessTest.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs index 89120e84c4..d36f63872e 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,8 @@ 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()); + private readonly Helpers.AwaitHelper awaitHelper = new Helpers.AwaitHelper(); [Fact] public void BenchmarkActionGlobalSetupSupported() => TestInvoke(x => BenchmarkAllCases.GlobalSetup(), UnrollFactor); @@ -44,6 +48,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 +63,30 @@ public InProcessTest(ITestOutputHelper output) : base(output) [Fact] public void BenchmarkActionValueTaskOfTSupported() => TestInvoke(x => x.InvokeOnceValueTaskOfT(), UnrollFactor, DecimalResult); + [Fact] + public void BenchmarkActionGlobalSetupTaskSupported() => TestInvokeSetupCleanupTask(x => BenchmarkSetupCleanupTask.GlobalSetup()); + + [Fact] + public void BenchmarkActionGlobalCleanupTaskSupported() => TestInvokeSetupCleanupTask(x => x.GlobalCleanup()); + + [Fact] + public void BenchmarkActionIterationSetupTaskSupported() => TestInvokeSetupCleanupTask(x => BenchmarkSetupCleanupTask.GlobalSetup()); + + [Fact] + 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) { @@ -64,34 +95,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] @@ -100,30 +131,118 @@ 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); + 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 - 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 (typeof(T).GetTypeInfo().IsValueType) - idleExpected = 0; - else if (expectedResult == null || typeof(T) == typeof(Task)) + idleExpected = GetDefault(methodReturnType.GetGenericArguments()[0]); + else if (expectedResult == null || methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask)) idleExpected = null; + else if (methodReturnType.GetTypeInfo().IsValueType) + idleExpected = 0; 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) + { + 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); + 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) + { + 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); + 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 +255,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); - benchmarkAction.InvokeMultiple(0); - Assert.Equal(0, BenchmarkAllCases.Counter); - benchmarkAction.InvokeMultiple(11); - Assert.Equal(0, BenchmarkAllCases.Counter); + awaitHelper.GetResult(benchmarkAction.InvokeSingle()); + Assert.Equal(0, counter); + awaitHelper.GetResult(benchmarkAction.InvokeUnroll(0, clock)); + Assert.Equal(0, counter); + awaitHelper.GetResult(benchmarkAction.InvokeUnroll(11, clock)); + Assert.Equal(0, counter); } else { - benchmarkAction.InvokeSingle(); - Assert.Equal(1, BenchmarkAllCases.Counter); - benchmarkAction.InvokeMultiple(0); - Assert.Equal(1, BenchmarkAllCases.Counter); - benchmarkAction.InvokeMultiple(11); - Assert.Equal(BenchmarkAllCases.Counter, 1 + unrollFactor * 11); + awaitHelper.GetResult(benchmarkAction.InvokeSingle()); + Assert.Equal(1, counter); + awaitHelper.GetResult(benchmarkAction.InvokeUnroll(0, clock)); + Assert.Equal(1, counter); + awaitHelper.GetResult(benchmarkAction.InvokeUnroll(11, clock)); + Assert.Equal(1 + unrollFactor * 11, counter); } Assert.Equal(benchmarkAction.LastRunResult, expectedResult); } finally { - BenchmarkAllCases.Counter = 0; + counter = 0; } } @@ -256,6 +375,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 +411,85 @@ 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); + } + + [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() + { + 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); + } + + [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() + { + Interlocked.Increment(ref Counter); + } + } } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/Engine/EngineFactoryTests.cs b/tests/BenchmarkDotNet.Tests/Engine/EngineFactoryTests.cs index 89c6fa05bd..27eb1431f9 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;