From 95ad39fbc380eb29b3b189246e2aec3d4ab4bd7f Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 19 Jun 2023 01:42:27 -0400 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFCall=20benchmark=20method=20directly?= =?UTF-8?q?=20instead=20of=20via=20delegate.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BenchmarkDotNet/Code/CodeGenerator.cs | 1 - .../Code/DeclarationsProvider.cs | 8 - .../ModuleBuilderExtensions.cs | 60 ----- .../Templates/BenchmarkType.txt | 115 ++++++--- .../Emitters/RunnableEmitter.cs | 228 +++++++++--------- .../Runnable/RunnableConstants.cs | 4 +- .../ExpectedBenchmarkResultsTests.cs | 165 ++++++++----- 7 files changed, 311 insertions(+), 270 deletions(-) delete mode 100644 src/BenchmarkDotNet/Helpers/Reflection.Emit/ModuleBuilderExtensions.cs diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs index 005564b77c..183ce5b71a 100644 --- a/src/BenchmarkDotNet/Code/CodeGenerator.cs +++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs @@ -45,7 +45,6 @@ internal static string Generate(BuildPartition buildPartition) .Replace("$ID$", buildInfo.Id.ToString()) .Replace("$OperationsPerInvoke$", provider.OperationsPerInvoke) .Replace("$WorkloadTypeName$", provider.WorkloadTypeName) - .Replace("$WorkloadMethodDelegate$", provider.WorkloadMethodDelegate(passArguments)) .Replace("$WorkloadMethodReturnType$", provider.WorkloadMethodReturnTypeName) .Replace("$WorkloadMethodReturnTypeModifiers$", provider.WorkloadMethodReturnTypeModifiers) .Replace("$OverheadMethodReturnTypeName$", provider.OverheadMethodReturnTypeName) diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs index ddf78eb572..d885812c03 100644 --- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs +++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs @@ -36,8 +36,6 @@ internal abstract class DeclarationsProvider public virtual string WorkloadMethodReturnTypeName => WorkloadMethodReturnType.GetCorrectCSharpTypeName(); - public virtual string WorkloadMethodDelegate(string passArguments) => Descriptor.WorkloadMethod.Name; - public virtual string WorkloadMethodReturnTypeModifiers => null; public virtual string GetWorkloadMethodCall(string passArguments) => $"{Descriptor.WorkloadMethod.Name}({passArguments})"; @@ -151,9 +149,6 @@ internal class TaskDeclarationsProvider : VoidDeclarationsProvider // 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()"; protected override Type WorkloadMethodReturnType => typeof(void); @@ -170,9 +165,6 @@ internal class GenericTaskDeclarationsProvider : NonVoidDeclarationsProvider // 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(); }}"; - public override string GetWorkloadMethodCall(string passArguments) => $"{Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult()"; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Helpers/Reflection.Emit/ModuleBuilderExtensions.cs b/src/BenchmarkDotNet/Helpers/Reflection.Emit/ModuleBuilderExtensions.cs deleted file mode 100644 index c879bc1f46..0000000000 --- a/src/BenchmarkDotNet/Helpers/Reflection.Emit/ModuleBuilderExtensions.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; - -namespace BenchmarkDotNet.Helpers.Reflection.Emit -{ - internal static class ModuleBuilderExtensions - { - public static Type EmitCustomDelegate( - this ModuleBuilder moduleBuilder, - string delegateTypeName, - ParameterInfo returnType, - ParameterInfo[] parameters) - { - // TODO: begin/end invoke ? - var delegatePatternType = typeof(Action); - - var typeBuilder = moduleBuilder.DefineType( - delegateTypeName, - delegatePatternType.Attributes, - delegatePatternType.BaseType); - - var ctorPattern = delegatePatternType.GetConstructors().Single(); - var ctorBuilder = typeBuilder.DefineConstructor( - ctorPattern.Attributes, - ctorPattern.CallingConvention, - ctorPattern.GetParameterTypes()); - - foreach (var parameterInfo in ctorPattern.GetParameters()) - { - ctorBuilder.DefineParameter(parameterInfo.Position + 1, parameterInfo.Attributes, parameterInfo.Name); - } - - ctorBuilder.SetImplementationFlags(ctorPattern.GetMethodImplementationFlags()); - - var invokePatternMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(delegatePatternType); - - var invokeBuilder = typeBuilder.DefineMethod( - invokePatternMethod.Name, - invokePatternMethod.Attributes, - invokePatternMethod.CallingConvention, - returnType.ParameterType, - parameters.Select(p => p.ParameterType).ToArray()); - foreach (var parameterInfo in parameters) - { - invokeBuilder.DefineParameter(parameterInfo.Position + 1, parameterInfo.Attributes, parameterInfo.Name); - } - invokeBuilder.DefineParameter(0, returnType.Attributes, ""); - - invokeBuilder.SetImplementationFlags(invokePatternMethod.GetMethodImplementationFlags()); - -#if NETFRAMEWORK - return typeBuilder.CreateType(); -#else - return typeBuilder.CreateTypeInfo(); -#endif - } - } -} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Templates/BenchmarkType.txt b/src/BenchmarkDotNet/Templates/BenchmarkType.txt index f17737d646..545004a4bf 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 sealed class Runnable_$ID$ : global::$WorkloadTypeName$ { public static void Run(BenchmarkDotNet.Engines.IHost host, System.String benchmarkName) { @@ -51,18 +51,12 @@ } } - public delegate $OverheadMethodReturnTypeName$ OverheadDelegate($ArgumentsDefinition$); - - public delegate $WorkloadMethodReturnTypeModifiers$ $WorkloadMethodReturnType$ WorkloadDelegate($ArgumentsDefinition$); - public Runnable_$ID$() { globalSetupAction = $GlobalSetupMethodName$; globalCleanupAction = $GlobalCleanupMethodName$; iterationSetupAction = $IterationSetupMethodName$; iterationCleanupAction = $IterationCleanupMethodName$; - overheadDelegate = __Overhead; - workloadDelegate = $WorkloadMethodDelegate$; $InitializeArgumentFields$ } @@ -70,8 +64,6 @@ private System.Action globalCleanupAction; private System.Action iterationSetupAction; private System.Action iterationCleanupAction; - private BenchmarkDotNet.Autogenerated.Runnable_$ID$.OverheadDelegate overheadDelegate; - private BenchmarkDotNet.Autogenerated.Runnable_$ID$.WorkloadDelegate workloadDelegate; $DeclareArgumentFields$ // this method is used only for the disassembly diagnoser purposes @@ -111,6 +103,19 @@ #if RETURNS_CONSUMABLE_$ID$ + // Prevent inlining the method invoke. + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)] + private $OverheadMethodReturnTypeName$ __OverheadWrapper($ArgumentsDefinition$) + { + return __Overhead($PassArguments$); + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)] + private $WorkloadMethodReturnType$ __WorkloadWrapper($ArgumentsDefinition$) + { + return $WorkloadMethodCall$; + } + private BenchmarkDotNet.Engines.Consumer consumer = new BenchmarkDotNet.Engines.Consumer(); [System.Runtime.CompilerServices.MethodImpl(BenchmarkDotNet.Portability.CodeGenHelper.AggressiveOptimizationOption)] @@ -119,7 +124,7 @@ $LoadArguments$ for (System.Int64 i = 0; i < invokeCount; i++) { - consumer.Consume(overheadDelegate($PassArguments$));@Unroll@ + consumer.Consume(__OverheadWrapper($PassArguments$));@Unroll@ } } @@ -129,7 +134,7 @@ $LoadArguments$ for (System.Int64 i = 0; i < invokeCount; i++) { - consumer.Consume(overheadDelegate($PassArguments$)); + consumer.Consume(__OverheadWrapper($PassArguments$)); } } @@ -139,7 +144,7 @@ $LoadArguments$ for (System.Int64 i = 0; i < invokeCount; i++) { - consumer.Consume(workloadDelegate($PassArguments$)$ConsumeField$);@Unroll@ + consumer.Consume(__WorkloadWrapper($PassArguments$)$ConsumeField$);@Unroll@ } } @@ -149,7 +154,7 @@ $LoadArguments$ for (System.Int64 i = 0; i < invokeCount; i++) { - consumer.Consume(workloadDelegate($PassArguments$)$ConsumeField$); + consumer.Consume(__WorkloadWrapper($PassArguments$)$ConsumeField$); } } @@ -167,6 +172,19 @@ #elif RETURNS_NON_CONSUMABLE_STRUCT_$ID$ + // Prevent inlining the method invoke. + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)] + private $OverheadMethodReturnTypeName$ __OverheadWrapper($ArgumentsDefinition$) + { + return __Overhead($PassArguments$); + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)] + private $WorkloadMethodReturnType$ __WorkloadWrapper($ArgumentsDefinition$) + { + return $WorkloadMethodCall$; + } + [System.Runtime.CompilerServices.MethodImpl(BenchmarkDotNet.Portability.CodeGenHelper.AggressiveOptimizationOption)] private void OverheadActionUnroll(System.Int64 invokeCount) { @@ -174,7 +192,7 @@ $OverheadMethodReturnTypeName$ result = default($OverheadMethodReturnTypeName$); for (System.Int64 i = 0; i < invokeCount; i++) { - result = overheadDelegate($PassArguments$);@Unroll@ + result = __OverheadWrapper($PassArguments$);@Unroll@ } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(result); } @@ -186,7 +204,7 @@ $OverheadMethodReturnTypeName$ result = default($OverheadMethodReturnTypeName$); for (System.Int64 i = 0; i < invokeCount; i++) { - result = overheadDelegate($PassArguments$); + result = __OverheadWrapper($PassArguments$); } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(result); } @@ -198,7 +216,7 @@ $WorkloadMethodReturnType$ result = default($WorkloadMethodReturnType$); for (System.Int64 i = 0; i < invokeCount; i++) { - result = workloadDelegate($PassArguments$);@Unroll@ + result = __WorkloadWrapper($PassArguments$);@Unroll@ } NonGenericKeepAliveWithoutBoxing(result); } @@ -210,7 +228,7 @@ $WorkloadMethodReturnType$ result = default($WorkloadMethodReturnType$); for (System.Int64 i = 0; i < invokeCount; i++) { - result = workloadDelegate($PassArguments$); + result = __WorkloadWrapper($PassArguments$); } NonGenericKeepAliveWithoutBoxing(result); } @@ -234,6 +252,19 @@ #elif RETURNS_BYREF_$ID$ + // Prevent inlining the method invoke. + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)] + private $OverheadMethodReturnTypeName$ __OverheadWrapper($ArgumentsDefinition$) + { + return __Overhead($PassArguments$); + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)] + private ref $WorkloadMethodReturnType$ __WorkloadWrapper($ArgumentsDefinition$) + { + return ref $WorkloadMethodCall$; + } + [System.Runtime.CompilerServices.MethodImpl(BenchmarkDotNet.Portability.CodeGenHelper.AggressiveOptimizationOption)] private void OverheadActionUnroll(System.Int64 invokeCount) { @@ -241,7 +272,7 @@ $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); for (System.Int64 i = 0; i < invokeCount; i++) { - value = overheadDelegate($PassArguments$);@Unroll@ + value = __OverheadWrapper($PassArguments$);@Unroll@ } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); } @@ -253,7 +284,7 @@ $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); for (System.Int64 i = 0; i < invokeCount; i++) { - value = overheadDelegate($PassArguments$); + value = __OverheadWrapper($PassArguments$); } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); } @@ -267,7 +298,7 @@ ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; for (System.Int64 i = 0; i < invokeCount; i++) { - alias = workloadDelegate($PassArguments$);@Unroll@ + alias = __WorkloadWrapper($PassArguments$);@Unroll@ } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(ref alias); } @@ -279,7 +310,7 @@ ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; for (System.Int64 i = 0; i < invokeCount; i++) { - alias = workloadDelegate($PassArguments$); + alias = __WorkloadWrapper($PassArguments$); } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(ref alias); } @@ -297,6 +328,19 @@ } #elif RETURNS_BYREF_READONLY_$ID$ + // Prevent inlining the method invoke. + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)] + private $OverheadMethodReturnTypeName$ __OverheadWrapper($ArgumentsDefinition$) + { + return __Overhead($PassArguments$); + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)] + private ref readonly $WorkloadMethodReturnType$ __WorkloadWrapper($ArgumentsDefinition$) + { + return ref $WorkloadMethodCall$; + } + [System.Runtime.CompilerServices.MethodImpl(BenchmarkDotNet.Portability.CodeGenHelper.AggressiveOptimizationOption)] private void OverheadActionUnroll(System.Int64 invokeCount) { @@ -304,7 +348,7 @@ $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); for (System.Int64 i = 0; i < invokeCount; i++) { - value = overheadDelegate($PassArguments$);@Unroll@ + value = __OverheadWrapper($PassArguments$);@Unroll@ } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); } @@ -316,7 +360,7 @@ $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); for (System.Int64 i = 0; i < invokeCount; i++) { - value = overheadDelegate($PassArguments$); + value = __OverheadWrapper($PassArguments$); } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); } @@ -330,7 +374,7 @@ ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; for (System.Int64 i = 0; i < invokeCount; i++) { - alias = workloadDelegate($PassArguments$);@Unroll@ + alias = __WorkloadWrapper($PassArguments$);@Unroll@ } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxingReadonly(alias); } @@ -342,7 +386,7 @@ ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; for (System.Int64 i = 0; i < invokeCount; i++) { - alias = workloadDelegate($PassArguments$); + alias = __WorkloadWrapper($PassArguments$); } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxingReadonly(alias); } @@ -360,13 +404,26 @@ } #elif RETURNS_VOID_$ID$ + // Prevent inlining the method invoke. + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)] + private void __OverheadWrapper($ArgumentsDefinition$) + { + __Overhead($PassArguments$); + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)] + private void __WorkloadWrapper($ArgumentsDefinition$) + { + $WorkloadMethodCall$; + } + [System.Runtime.CompilerServices.MethodImpl(BenchmarkDotNet.Portability.CodeGenHelper.AggressiveOptimizationOption)] private void OverheadActionUnroll(System.Int64 invokeCount) { $LoadArguments$ for (System.Int64 i = 0; i < invokeCount; i++) { - overheadDelegate($PassArguments$);@Unroll@ + __OverheadWrapper($PassArguments$);@Unroll@ } } @@ -376,7 +433,7 @@ $LoadArguments$ for (System.Int64 i = 0; i < invokeCount; i++) { - overheadDelegate($PassArguments$); + __OverheadWrapper($PassArguments$); } } @@ -386,7 +443,7 @@ $LoadArguments$ for (System.Int64 i = 0; i < invokeCount; i++) { - workloadDelegate($PassArguments$);@Unroll@ + __WorkloadWrapper($PassArguments$);@Unroll@ } } @@ -396,7 +453,7 @@ $LoadArguments$ for (System.Int64 i = 0; i < invokeCount; i++) { - workloadDelegate($PassArguments$); + __WorkloadWrapper($PassArguments$); } } diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs index 9048474329..ec879e0ae8 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs @@ -170,7 +170,7 @@ private static ModuleBuilder DefineModuleBuilder(AssemblyBuilder assemblyBuilder BenchmarkBuildInfo benchmark, ModuleBuilder moduleBuilder) { - // .class public auto ansi beforefieldinit BenchmarkDotNet.Autogenerated.Runnable_0 + // .class public auto ansi sealed beforefieldinit BenchmarkDotNet.Autogenerated.Runnable_0 // extends [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark var benchmarkDescriptor = benchmark.BenchmarkCase.Descriptor; @@ -184,7 +184,7 @@ private static ModuleBuilder DefineModuleBuilder(AssemblyBuilder assemblyBuilder } var result = moduleBuilder.DefineType( GetRunnableTypeName(benchmark), - workloadTypeAttributes, + workloadTypeAttributes | TypeAttributes.Sealed, workloadType); return result; @@ -242,8 +242,6 @@ private static ModuleBuilder DefineModuleBuilder(AssemblyBuilder assemblyBuilder private int jobUnrollFactor; private int dummyUnrollFactor; - private Type overheadDelegateType; - private Type workloadDelegateType; private TypeBuilder runnableBuilder; private ConsumableTypeInfo consumableInfo; private ConsumeEmitter consumeEmitter; @@ -252,8 +250,6 @@ private static ModuleBuilder DefineModuleBuilder(AssemblyBuilder assemblyBuilder private FieldBuilder globalCleanupActionField; private FieldBuilder iterationSetupActionField; private FieldBuilder iterationCleanupActionField; - private FieldBuilder overheadDelegateField; - private FieldBuilder workloadDelegateField; private FieldBuilder notElevenField; private FieldBuilder dummyVarField; @@ -263,8 +259,9 @@ private static ModuleBuilder DefineModuleBuilder(AssemblyBuilder assemblyBuilder private MethodBuilder dummy1Method; private MethodBuilder dummy2Method; private MethodBuilder dummy3Method; - private MethodInfo workloadImplementationMethod; + private MethodBuilder workloadWrapperMethod; private MethodBuilder overheadImplementationMethod; + private MethodBuilder overheadWrapperMethod; private MethodBuilder overheadActionUnrollMethod; private MethodBuilder overheadActionNoUnrollMethod; private MethodBuilder workloadActionUnrollMethod; @@ -317,11 +314,12 @@ private Type EmitRunnableCore(BenchmarkBuildInfo newBenchmark) // Overhead impl overheadImplementationMethod = EmitOverheadImplementation(OverheadImplementationMethodName); + overheadWrapperMethod = EmitOverheadWrapperImplementation(OverheadWrapperImplementationMethodName); overheadActionUnrollMethod = EmitOverheadAction(OverheadActionUnrollMethodName, jobUnrollFactor); overheadActionNoUnrollMethod = EmitOverheadAction(OverheadActionNoUnrollMethodName, 1); // Workload impl - workloadImplementationMethod = EmitWorkloadImplementation(WorkloadImplementationMethodName); + workloadWrapperMethod = EmitWorkloadWrapperImplementation(WorkloadWrapperImplementationMethodName); workloadActionUnrollMethod = EmitWorkloadAction(WorkloadActionUnrollMethodName, jobUnrollFactor); workloadActionNoUnrollMethod = EmitWorkloadAction(WorkloadActionNoUnrollMethodName, 1); @@ -359,56 +357,8 @@ private void InitForEmitRunnable(BenchmarkBuildInfo newBenchmark) consumableInfo = new ConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.WorkloadMethod.ReturnType); consumeEmitter = ConsumeEmitter.GetConsumeEmitter(consumableInfo); - // Init types + // Init type runnableBuilder = DefineRunnableTypeBuilder(benchmark, moduleBuilder); - overheadDelegateType = EmitOverheadDelegateType(); - workloadDelegateType = EmitWorkloadDelegateType(); - } - - private Type EmitOverheadDelegateType() - { - // .class public auto ansi sealed BenchmarkDotNet.Autogenerated.Runnable_0OverheadDelegate - // extends[mscorlib]System.MulticastDelegate; - var overheadReturnType = EmitParameterInfo.CreateReturnParameter(consumableInfo.OverheadMethodReturnType); - - // replace arg names - var overheadParameters = Descriptor.WorkloadMethod.GetParameters() - .Select(p => - (ParameterInfo)new EmitParameterInfo( - p.Position, - ArgParamPrefix + p.Position, - p.ParameterType, - p.Attributes, - null)) - .ToArray(); - - return moduleBuilder.EmitCustomDelegate( - GetRunnableTypeName(benchmark) + OverheadDelegateTypeSuffix, - overheadReturnType, - overheadParameters); - } - - private Type EmitWorkloadDelegateType() - { - // .class public auto ansi sealed BenchmarkDotNet.Autogenerated.Runnable_0WorkloadDelegate - // extends [mscorlib]System.MulticastDelegate - var workloadReturnType = EmitParameterInfo.CreateReturnParameter(consumableInfo.WorkloadMethodReturnType); - - // Replace arg names - var workloadParameters = Descriptor.WorkloadMethod.GetParameters() - .Select(p => - (ParameterInfo)new EmitParameterInfo( - p.Position, - ArgParamPrefix + p.Position, - p.ParameterType, - p.Attributes, - null)) - .ToArray(); - - return moduleBuilder.EmitCustomDelegate( - GetRunnableTypeName(benchmark) + WorkloadDelegateTypeSuffix, - workloadReturnType, - workloadParameters); } private void DefineFields() @@ -421,10 +371,6 @@ private void DefineFields() runnableBuilder.DefineField(IterationSetupActionFieldName, typeof(Action), FieldAttributes.Private); iterationCleanupActionField = runnableBuilder.DefineField(IterationCleanupActionFieldName, typeof(Action), FieldAttributes.Private); - overheadDelegateField = - runnableBuilder.DefineField(OverheadDelegateFieldName, overheadDelegateType, FieldAttributes.Private); - workloadDelegateField = - runnableBuilder.DefineField(WorkloadDelegateFieldName, workloadDelegateType, FieldAttributes.Private); // Define arg fields foreach (var parameter in Descriptor.WorkloadMethod.GetParameters()) @@ -540,15 +486,25 @@ private MethodBuilder EmitDummyMethod(string methodName, int unrollFactor) private MethodBuilder EmitOverheadImplementation(string methodName) { - var overheadInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(overheadDelegateType); - //.method private hidebysig // instance int32 __Overhead(int64 arg0) cil managed + + // Replace arg names + var parameters = Descriptor.WorkloadMethod.GetParameters() + .Select(p => + (ParameterInfo) new EmitParameterInfo( + p.Position, + ArgParamPrefix + p.Position, + p.ParameterType, + p.Attributes, + null)) + .ToArray(); + var methodBuilder = runnableBuilder.DefineNonVirtualInstanceMethod( methodName, MethodAttributes.Private, - overheadInvokeMethod.ReturnParameter, - overheadInvokeMethod.GetParameters()); + EmitParameterInfo.CreateReturnParameter(consumableInfo.OverheadMethodReturnType), + parameters); var ilBuilder = methodBuilder.GetILGenerator(); var returnType = methodBuilder.ReturnType; @@ -565,59 +521,113 @@ private MethodBuilder EmitOverheadImplementation(string methodName) return methodBuilder; } - private MethodInfo EmitWorkloadImplementation(string methodName) + private MethodBuilder EmitOverheadWrapperImplementation(string methodName) { - // Shortcut: DO NOT emit method if the result type is not awaitable - if (!consumableInfo.IsAwaitable) - return Descriptor.WorkloadMethod; + //.method private hidebysig + // instance int32 __OverheadWrapper(int64 arg0) cil managed noinlining nooptimization - var workloadInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(workloadDelegateType); + // Replace arg names + var parameters = Descriptor.WorkloadMethod.GetParameters() + .Select(p => + (ParameterInfo) new EmitParameterInfo( + p.Position, + ArgParamPrefix + p.Position, + p.ParameterType, + p.Attributes, + null)) + .ToArray(); - //.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"); + EmitParameterInfo.CreateReturnParameter(consumableInfo.OverheadMethodReturnType), + parameters) + .SetNoInliningImplementationFlag() + .SetNoOptimizationImplementationFlag(); var ilBuilder = methodBuilder.GetILGenerator(); - /* - .locals init ( - [0] valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1 - ) + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: call instance int32 [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::__Overhead(int64) + IL_0007: ret */ - var callResultLocal = - ilBuilder.DeclareOptionalLocalForInstanceCall(callResultType, consumableInfo.GetAwaiterMethod); - var awaiterLocal = - ilBuilder.DeclareOptionalLocalForInstanceCall(awaiterType, consumableInfo.GetResultMethod); + if (!overheadImplementationMethod.IsStatic) + ilBuilder.Emit(OpCodes.Ldarg_0); + parameters = methodBuilder.GetEmitParameters(parameters); + ilBuilder.EmitLdargs(parameters); + ilBuilder.Emit(OpCodes.Call, overheadImplementationMethod); + ilBuilder.Emit(OpCodes.Ret); + return methodBuilder; + } + + private MethodBuilder EmitWorkloadWrapperImplementation(string methodName) + { + //.method private hidebysig + // instance string __WorkloadWrapper(int32 arg0) cil managed noinlining nooptimization + + // Replace arg names + var parameters = Descriptor.WorkloadMethod.GetParameters() + .Select(p => + (ParameterInfo) new EmitParameterInfo( + p.Position, + ArgParamPrefix + p.Position, + p.ParameterType, + p.Attributes, + null)) + .ToArray(); + + var methodBuilder = runnableBuilder.DefineNonVirtualInstanceMethod( + methodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(consumableInfo.WorkloadMethodReturnType), + parameters) + .SetNoInliningImplementationFlag() + .SetNoOptimizationImplementationFlag(); + + var ilBuilder = methodBuilder.GetILGenerator(); + + Type awaiterType = null; + LocalBuilder callResultLocal = null; + LocalBuilder awaiterLocal = null; + if (consumableInfo.IsAwaitable) + { + awaiterType = consumableInfo.GetAwaiterMethod?.ReturnType + ?? throw new InvalidOperationException($"Bug: {nameof(consumableInfo.GetAwaiterMethod)} is null"); + /* + .locals init ( + [0] valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1 + ) + */ + callResultLocal = ilBuilder.DeclareOptionalLocalForInstanceCall(consumableInfo.OriginMethodReturnType, consumableInfo.GetAwaiterMethod); + 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) + IL_0002: call instance string [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::ReturnSingleArgCase(int32) */ if (!Descriptor.WorkloadMethod.IsStatic) + { ilBuilder.Emit(OpCodes.Ldarg_0); - ilBuilder.EmitLdargs(args); + } + parameters = methodBuilder.GetEmitParameters(parameters); + ilBuilder.EmitLdargs(parameters); 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); + 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_0014: ret @@ -639,17 +649,14 @@ private MethodBuilder EmitWorkloadAction(string methodName, int unrollFactor) private MethodBuilder EmitActionImpl(string methodName, RunnableActionKind actionKind, int unrollFactor) { - FieldInfo actionDelegateField; - MethodInfo actionInvokeMethod; + MethodInfo invokeMethod; switch (actionKind) { case RunnableActionKind.Overhead: - actionDelegateField = overheadDelegateField; - actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(overheadDelegateType); + invokeMethod = overheadWrapperMethod; break; case RunnableActionKind.Workload: - actionDelegateField = workloadDelegateField; - actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(workloadDelegateType); + invokeMethod = workloadWrapperMethod; break; default: throw new ArgumentOutOfRangeException(nameof(actionKind), actionKind, null); @@ -668,7 +675,7 @@ private MethodBuilder EmitActionImpl(string methodName, RunnableActionKind actio // Emit impl var ilBuilder = actionMethodBuilder.GetILGenerator(); - consumeEmitter.BeginEmitAction(actionMethodBuilder, ilBuilder, actionInvokeMethod, actionKind); + consumeEmitter.BeginEmitAction(actionMethodBuilder, ilBuilder, invokeMethod, actionKind); // init locals var argLocals = EmitDeclareArgLocals(ilBuilder); @@ -704,8 +711,7 @@ private MethodBuilder EmitActionImpl(string methodName, RunnableActionKind actio consumeEmitter.EmitActionBeforeCall(ilBuilder); ilBuilder.Emit(OpCodes.Ldarg_0); - ilBuilder.Emit(OpCodes.Ldfld, actionDelegateField); - ilBuilder.EmitInstanceCallThisValueOnStack(null, actionInvokeMethod, argLocals); + ilBuilder.EmitInstanceCallThisValueOnStack(null, invokeMethod, argLocals, forceDirectCall: true); consumeEmitter.EmitActionAfterCall(ilBuilder); } @@ -959,12 +965,6 @@ private void EmitCtorBody() 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.EmitCtorReturn(ctorMethod); } diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs index c6e8cd8ae1..349b7aa8ca 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs @@ -20,8 +20,6 @@ public class RunnableConstants public const string GlobalCleanupActionFieldName = "globalCleanupAction"; public const string IterationSetupActionFieldName = "iterationSetupAction"; public const string IterationCleanupActionFieldName = "iterationCleanupAction"; - public const string WorkloadDelegateFieldName = "workloadDelegate"; - public const string OverheadDelegateFieldName = "overheadDelegate"; public const string NotElevenFieldName = "NotEleven"; public const string DummyVarFieldName = "dummyVar"; @@ -30,7 +28,9 @@ public class RunnableConstants public const string Dummy2MethodName = "Dummy2"; public const string Dummy3MethodName = "Dummy3"; public const string WorkloadImplementationMethodName = "__Workload"; + public const string WorkloadWrapperImplementationMethodName = "__WorkloadWrapper"; public const string OverheadImplementationMethodName = "__Overhead"; + public const string OverheadWrapperImplementationMethodName = "__OverheadWrapper"; public const string OverheadActionUnrollMethodName = "OverheadActionUnroll"; public const string OverheadActionNoUnrollMethodName = "OverheadActionNoUnroll"; public const string WorkloadActionUnrollMethodName = "WorkloadActionUnroll"; diff --git a/tests/BenchmarkDotNet.IntegrationTests.ManualRunning/ExpectedBenchmarkResultsTests.cs b/tests/BenchmarkDotNet.IntegrationTests.ManualRunning/ExpectedBenchmarkResultsTests.cs index 7f950136fb..33f9adc215 100644 --- a/tests/BenchmarkDotNet.IntegrationTests.ManualRunning/ExpectedBenchmarkResultsTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests.ManualRunning/ExpectedBenchmarkResultsTests.cs @@ -12,25 +12,26 @@ using BenchmarkDotNet.Reports; using BenchmarkDotNet.Tests.XUnit; using BenchmarkDotNet.Toolchains.InProcess.Emit; +using Perfolizer; using Perfolizer.Horology; +using Perfolizer.Mathematics.Common; +using Perfolizer.Mathematics.SignificanceTesting; +using Perfolizer.Mathematics.SignificanceTesting.MannWhitney; using Perfolizer.Metrology; using Xunit; using Xunit.Abstractions; namespace BenchmarkDotNet.IntegrationTests.ManualRunning { - public class ExpectedBenchmarkResultsTests : BenchmarkTestExecutor + public class ExpectedBenchmarkResultsTests(ITestOutputHelper output) : BenchmarkTestExecutor(output) { // NativeAot takes a long time to build, so not including it in these tests. // We also don't test InProcessNoEmitToolchain because it is known to be less accurate than code-gen toolchains. private static readonly TimeInterval FallbackCpuResolutionValue = TimeInterval.FromNanoseconds(0.2d); - public ExpectedBenchmarkResultsTests(ITestOutputHelper output) : base(output) { } - private static IEnumerable EmptyBenchmarkTypes() => - new[] - { + [ typeof(EmptyVoid), typeof(EmptyByte), typeof(EmptySByte), @@ -45,7 +46,7 @@ public class ExpectedBenchmarkResultsTests : BenchmarkTestExecutor typeof(EmptyUIntPtr), typeof(EmptyVoidPointer), typeof(EmptyClass) - }; + ]; public static IEnumerable InProcessData() { @@ -59,8 +60,8 @@ public static IEnumerable CoreData() { foreach (var type in EmptyBenchmarkTypes()) { - yield return new object[] { type, RuntimeMoniker.Net70 }; - yield return new object[] { type, RuntimeMoniker.Mono70 }; + yield return new object[] { type, RuntimeMoniker.Net80 }; + yield return new object[] { type, RuntimeMoniker.Mono80 }; } } @@ -75,17 +76,19 @@ public static IEnumerable FrameworkData() [Theory] [MemberData(nameof(InProcessData))] - public void EmptyBenchmarksReportZeroTimeAndAllocated_InProcess(Type benchmarkType) + public void EmptyBenchmarkReportsZeroTimeAndAllocated_InProcess(Type benchmarkType) { AssertZeroResults(benchmarkType, ManualConfig.CreateEmpty() .AddJob(Job.Default .WithToolchain(InProcessEmitToolchain.Instance) - )); + // IL Emit has incorrect overhead measurement. https://github.com/dotnet/runtime/issues/89685 + // We multiply the threshold to account for it. + ), multiplyThresholdBy: RuntimeInformation.IsNetCore ? 3 : 1); } [TheoryEnvSpecific("To not repeat tests in both Full .NET Framework and Core", EnvRequirement.DotNetCoreOnly)] [MemberData(nameof(CoreData))] - public void EmptyBenchmarksReportZeroTimeAndAllocated_Core(Type benchmarkType, RuntimeMoniker runtimeMoniker) + public void EmptyBenchmarkReportsZeroTimeAndAllocated_Core(Type benchmarkType, RuntimeMoniker runtimeMoniker) { AssertZeroResults(benchmarkType, ManualConfig.CreateEmpty() .AddJob(Job.Default @@ -95,7 +98,7 @@ public void EmptyBenchmarksReportZeroTimeAndAllocated_Core(Type benchmarkType, R [TheoryEnvSpecific("Can only run Full .NET Framework and Mono tests from Framework host", EnvRequirement.FullFrameworkOnly)] [MemberData(nameof(FrameworkData))] - public void EmptyBenchmarksReportZeroTimeAndAllocated_Framework(Type benchmarkType, RuntimeMoniker runtimeMoniker) + public void EmptyBenchmarkReportsZeroTimeAndAllocated_Framework(Type benchmarkType, RuntimeMoniker runtimeMoniker) { AssertZeroResults(benchmarkType, ManualConfig.CreateEmpty() .AddJob(Job.Default @@ -103,7 +106,7 @@ public void EmptyBenchmarksReportZeroTimeAndAllocated_Framework(Type benchmarkTy )); } - private void AssertZeroResults(Type benchmarkType, IConfig config) + private void AssertZeroResults(Type benchmarkType, IConfig config, int multiplyThresholdBy = 1) { var summary = CanExecute(benchmarkType, config .WithSummaryStyle(SummaryStyle.Default.WithTimeUnit(TimeUnit.Nanosecond)) @@ -111,7 +114,7 @@ private void AssertZeroResults(Type benchmarkType, IConfig config) ); var cpuResolution = RuntimeInformation.GetCpuInfo().MaxFrequency?.ToResolution() ?? FallbackCpuResolutionValue; - var threshold = new NumberValue(cpuResolution.Nanoseconds).ToThreshold(); + var threshold = new NumberValue(cpuResolution.Nanoseconds * multiplyThresholdBy).ToThreshold(); foreach (var report in summary.Reports) { @@ -125,87 +128,137 @@ private void AssertZeroResults(Type benchmarkType, IConfig config) } } - [Fact] - public void DifferentSizedStructsBenchmarksReportsNonZeroTimeAndZeroAllocated_InProcess() + private static IEnumerable NonEmptyBenchmarkTypes() => + [ + typeof(DifferentSizedStructs), + typeof(ActualWork) + ]; + + public static IEnumerable NonEmptyInProcessData() + { + foreach (var type in NonEmptyBenchmarkTypes()) + { + yield return new object[] { type }; + } + } + + public static IEnumerable NonEmptyCoreData() + { + foreach (var type in NonEmptyBenchmarkTypes()) + { + yield return new object[] { type, RuntimeMoniker.Net80 }; + yield return new object[] { type, RuntimeMoniker.Mono80 }; + } + } + + public static IEnumerable NonEmptyFrameworkData() { - AssertDifferentSizedStructsResults(ManualConfig.CreateEmpty() + foreach (var type in NonEmptyBenchmarkTypes()) + { + yield return new object[] { type, RuntimeMoniker.Net462 }; + yield return new object[] { type, RuntimeMoniker.Mono }; + } + } + + [Theory] + [MemberData(nameof(NonEmptyInProcessData))] + public void NonEmptyBenchmarkReportsNonZeroTimeAndZeroAllocated_InProcess(Type benchmarkType) + { + AssertNonZeroResults(benchmarkType, ManualConfig.CreateEmpty() .AddJob(Job.Default .WithToolchain(InProcessEmitToolchain.Instance) - )); + // InProcess overhead measurements are incorrect, so we adjust the results to account for it. https://github.com/dotnet/runtime/issues/89685 + ), subtractOverheadByClocks: RuntimeInformation.IsNetCore ? 3 : 1); } [TheoryEnvSpecific("To not repeat tests in both Full .NET Framework and Core", EnvRequirement.DotNetCoreOnly)] - [InlineData(RuntimeMoniker.Net70)] - [InlineData(RuntimeMoniker.Mono70)] - public void DifferentSizedStructsBenchmarksReportsNonZeroTimeAndZeroAllocated_Core(RuntimeMoniker runtimeMoniker) + [MemberData(nameof(NonEmptyCoreData))] + public void NonEmptyBenchmarkReportsNonZeroTimeAndZeroAllocated_Core(Type benchmarkType, RuntimeMoniker runtimeMoniker) { - AssertDifferentSizedStructsResults(ManualConfig.CreateEmpty() + AssertNonZeroResults(benchmarkType, ManualConfig.CreateEmpty() .AddJob(Job.Default .WithRuntime(runtimeMoniker.GetRuntime()) )); } - [TheoryEnvSpecific("Can only run Full .NET Framework and Mono tests from Framework host", EnvRequirement.FullFrameworkOnly)] - [InlineData(RuntimeMoniker.Net462)] - [InlineData(RuntimeMoniker.Mono)] - public void DifferentSizedStructsBenchmarksReportsNonZeroTimeAndZeroAllocated_Framework(RuntimeMoniker runtimeMoniker) + [TheoryEnvSpecific("Can only run Mono tests from Framework host", EnvRequirement.FullFrameworkOnly)] + [MemberData(nameof(NonEmptyFrameworkData))] + public void NonEmptyBenchmarkReportsNonZeroTimeAndZeroAllocated_Framework(Type benchmarkType, RuntimeMoniker runtimeMoniker) { - AssertDifferentSizedStructsResults(ManualConfig.CreateEmpty() + AssertNonZeroResults(benchmarkType, ManualConfig.CreateEmpty() .AddJob(Job.Default .WithRuntime(runtimeMoniker.GetRuntime()) )); } - private void AssertDifferentSizedStructsResults(IConfig config) + private void AssertNonZeroResults(Type benchmarkType, IConfig config, int subtractOverheadByClocks = 0) { - var summary = CanExecute(config + var summary = CanExecute(benchmarkType, config .WithSummaryStyle(SummaryStyle.Default.WithTimeUnit(TimeUnit.Nanosecond)) .AddDiagnoser(new MemoryDiagnoser(new MemoryDiagnoserConfig(false))) ); var cpuResolution = RuntimeInformation.GetCpuInfo().MaxFrequency?.ToResolution() ?? FallbackCpuResolutionValue; - var threshold = (cpuResolution / 2).ToThreshold(); + // Modern cpus can execute multiple instructions per clock cycle, + // resulting in measurements greater than 0 but less than 1 clock cycle. + // (example: Intel Core i9-9880H CPU 2.30GHz reports 0.2852 ns for `_field++;`) + var threshold = new NumberValue(cpuResolution.Nanoseconds / 4).ToThreshold(); + // InProcess overhead measurements are incorrect, so we adjust the results to account for it. https://github.com/dotnet/runtime/issues/89685 + var overheadSubtraction = cpuResolution.Nanoseconds * subtractOverheadByClocks; foreach (var report in summary.Reports) { var workloadMeasurements = report.AllMeasurements.Where(m => m.Is(IterationMode.Workload, IterationStage.Actual)).GetStatistics().Sample; - var overheadMeasurements = report.AllMeasurements.Where(m => m.Is(IterationMode.Overhead, IterationStage.Actual)).GetStatistics().Sample; + var overheadMeasurements = new Sample(report.AllMeasurements + .Where(m => m.Is(IterationMode.Overhead, IterationStage.Actual)) + .GetStatistics().OriginalValues + .Select(x => x - overheadSubtraction).ToArray()); - bool isZero = ZeroMeasurementHelper.AreIndistinguishable(workloadMeasurements, overheadMeasurements, threshold); - Assert.False(isZero, $"Actual time was 0."); + var comparisonResult = new SimpleEquivalenceTest(MannWhitneyTest.Instance).Perform(workloadMeasurements, overheadMeasurements, threshold, SignificanceLevel.P1E5); + Assert.True(comparisonResult == ComparisonResult.Greater, "Workload measurements are not greater than overhead."); Assert.True((report.GcStats.GetBytesAllocatedPerOperation(report.BenchmarkCase) ?? 0L) == 0L, "Memory allocations measured above 0."); } } } +} - public struct Struct16 - { - public long l1, l2; - } +// Types outside of namespace so it's easier to read in the test explorer. +#pragma warning disable CA1050 // Declare types in namespaces +public struct Struct16 +{ + public long l1, l2; +} - public struct Struct32 - { - public long l1, l2, l3, l4; - } +public struct Struct32 +{ + public long l1, l2, l3, l4; +} - public struct Struct64 - { - public long l1, l2, l3, l4, l5, l6, l7, l8; - } +public struct Struct64 +{ + public long l1, l2, l3, l4, l5, l6, l7, l8; +} - public struct Struct128 - { - public long l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, l16; - } +public struct Struct128 +{ + public long l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, l16; +} - public class DifferentSizedStructs - { - [Benchmark] public Struct16 Struct16() => default; - [Benchmark] public Struct32 Struct32() => default; - [Benchmark] public Struct64 Struct64() => default; - [Benchmark] public Struct128 Struct128() => default; - } +public class DifferentSizedStructs +{ + [Benchmark] public Struct16 Struct16() => default; + [Benchmark] public Struct32 Struct32() => default; + [Benchmark] public Struct64 Struct64() => default; + [Benchmark] public Struct128 Struct128() => default; +} + +public class ActualWork +{ + public int _field; + + [Benchmark] + public void IncrementField() => _field++; } public class EmptyVoid