diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs index 52ae0e5478..71749c8f63 100644 --- a/src/BenchmarkDotNet/Code/CodeGenerator.cs +++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs @@ -33,7 +33,7 @@ internal static string Generate(BuildPartition buildPartition) { var benchmark = buildInfo.BenchmarkCase; - var provider = GetDeclarationsProvider(benchmark.Descriptor); + var provider = GetDeclarationsProvider(benchmark); string passArguments = GetPassArguments(benchmark); @@ -48,12 +48,12 @@ internal static string Generate(BuildPartition buildPartition) .Replace("$WorkloadMethodDelegate$", provider.WorkloadMethodDelegate(passArguments)) .Replace("$WorkloadMethodReturnType$", provider.WorkloadMethodReturnTypeName) .Replace("$WorkloadMethodReturnTypeModifiers$", provider.WorkloadMethodReturnTypeModifiers) - .Replace("$OverheadMethodReturnTypeName$", provider.OverheadMethodReturnTypeName) .Replace("$GlobalSetupMethodName$", provider.GlobalSetupMethodName) .Replace("$GlobalCleanupMethodName$", provider.GlobalCleanupMethodName) .Replace("$IterationSetupMethodName$", provider.IterationSetupMethodName) .Replace("$IterationCleanupMethodName$", provider.IterationCleanupMethodName) .Replace("$OverheadImplementation$", provider.OverheadImplementation) + .Replace("$OverheadDefaultValueHolderField$", provider.OverheadDefaultValueHolderDeclaration) .Replace("$ConsumeField$", provider.ConsumeField) .Replace("$JobSetDefinition$", GetJobsSetDefinition(benchmark)) .Replace("$ParamsContent$", GetParamsContent(benchmark)) @@ -148,19 +148,19 @@ private static string GetJobsSetDefinition(BenchmarkCase benchmarkCase) Replace("; ", ";\n "); } - private static DeclarationsProvider GetDeclarationsProvider(Descriptor descriptor) + private static DeclarationsProvider GetDeclarationsProvider(BenchmarkCase benchmark) { - var method = descriptor.WorkloadMethod; + var method = benchmark.Descriptor.WorkloadMethod; if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(ValueTask)) { - return new TaskDeclarationsProvider(descriptor); + return new TaskDeclarationsProvider(benchmark); } if (method.ReturnType.GetTypeInfo().IsGenericType && (method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) || method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>))) { - return new GenericTaskDeclarationsProvider(descriptor); + return new GenericTaskDeclarationsProvider(benchmark); } if (method.ReturnType == typeof(void)) @@ -171,19 +171,19 @@ private static DeclarationsProvider GetDeclarationsProvider(Descriptor descripto throw new NotSupportedException("async void is not supported by design"); } - return new VoidDeclarationsProvider(descriptor); + return new VoidDeclarationsProvider(benchmark); } if (method.ReturnType.IsByRef) { // System.Runtime.CompilerServices.IsReadOnlyAttribute is part of .NET Standard 2.1, we can't use it here.. if (method.ReturnParameter.GetCustomAttributes().Any(attribute => attribute.GetType().Name == "IsReadOnlyAttribute")) - return new ByReadOnlyRefDeclarationsProvider(descriptor); + return new ByReadOnlyRefDeclarationsProvider(benchmark); else - return new ByRefDeclarationsProvider(descriptor); + return new ByRefDeclarationsProvider(benchmark); } - return new NonVoidDeclarationsProvider(descriptor); + return new NonVoidDeclarationsProvider(benchmark); } // internal for tests diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs index ddf78eb572..5cd3b382e4 100644 --- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs +++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs @@ -14,9 +14,10 @@ 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; + protected readonly BenchmarkCase Benchmark; + protected Descriptor Descriptor => Benchmark.Descriptor; - internal DeclarationsProvider(Descriptor descriptor) => Descriptor = descriptor; + internal DeclarationsProvider(BenchmarkCase benchmark) => Benchmark = benchmark; public string OperationsPerInvoke => Descriptor.OperationsPerInvoke.ToString(); @@ -44,12 +45,11 @@ internal abstract class DeclarationsProvider public virtual string ConsumeField => null; - protected abstract Type OverheadMethodReturnType { get; } - - public string OverheadMethodReturnTypeName => OverheadMethodReturnType.GetCorrectCSharpTypeName(); public abstract string OverheadImplementation { get; } + public virtual string OverheadDefaultValueHolderDeclaration => null; + private string GetMethodName(MethodInfo method) { if (method == null) @@ -72,44 +72,42 @@ private string GetMethodName(MethodInfo method) internal class VoidDeclarationsProvider : DeclarationsProvider { - public VoidDeclarationsProvider(Descriptor descriptor) : base(descriptor) { } + public VoidDeclarationsProvider(BenchmarkCase benchmark) : base(benchmark) { } public override string ReturnsDefinition => "RETURNS_VOID"; - protected override Type OverheadMethodReturnType => typeof(void); - public override string OverheadImplementation => string.Empty; } internal class NonVoidDeclarationsProvider : DeclarationsProvider { - public NonVoidDeclarationsProvider(Descriptor descriptor) : base(descriptor) { } + private readonly bool overheadReturnsDefault; + + public NonVoidDeclarationsProvider(BenchmarkCase benchmark) : base(benchmark) + { + overheadReturnsDefault = WorkloadMethodReturnType.IsDefaultFasterThanField(Benchmark.GetRuntime().RuntimeMoniker == Jobs.RuntimeMoniker.Mono); + } public override string ConsumeField => !Consumer.IsConsumable(WorkloadMethodReturnType) && Consumer.HasConsumableField(WorkloadMethodReturnType, out var field) ? $".{field.Name}" : null; - protected override Type OverheadMethodReturnType - => Consumer.IsConsumable(WorkloadMethodReturnType) - ? WorkloadMethodReturnType - : (Consumer.HasConsumableField(WorkloadMethodReturnType, out var field) - ? field.FieldType - : typeof(int)); // we return this simple type because creating bigger ValueType could take longer than benchmarked method itself - public override string OverheadImplementation + => overheadReturnsDefault + ? $"return default({WorkloadMethodReturnType.GetCorrectCSharpTypeName()});" + : "return overheadDefaultValueHolder;"; + + public override string OverheadDefaultValueHolderDeclaration { get { - string value; - var type = OverheadMethodReturnType; - if (type.GetTypeInfo().IsPrimitive) - value = $"default({type.GetCorrectCSharpTypeName()})"; - else if (type.GetTypeInfo().IsClass || type.GetTypeInfo().IsInterface) - value = "null"; - else - value = SourceCodeHelper.ToSourceCode(Activator.CreateInstance(type)) + ";"; - return $"return {value};"; + if (overheadReturnsDefault) + { + return null; + } + string typeName = WorkloadMethodReturnType.GetCorrectCSharpTypeName(); + return $"private {typeName} overheadDefaultValueHolder = default({typeName});"; } } @@ -121,15 +119,22 @@ public override string ReturnsDefinition internal class ByRefDeclarationsProvider : NonVoidDeclarationsProvider { - public ByRefDeclarationsProvider(Descriptor descriptor) : base(descriptor) { } - - protected override Type OverheadMethodReturnType => typeof(IntPtr); + public ByRefDeclarationsProvider(BenchmarkCase benchmark) : base(benchmark) { } public override string WorkloadMethodReturnTypeName => base.WorkloadMethodReturnTypeName.Replace("&", string.Empty); public override string ConsumeField => null; - public override string OverheadImplementation => $"return default(System.{nameof(IntPtr)});"; + public override string OverheadImplementation => $"return ref overheadDefaultValueHolder;"; + + public override string OverheadDefaultValueHolderDeclaration + { + get + { + string typeName = WorkloadMethodReturnType.GetCorrectCSharpTypeName(); + return $"private {typeName} overheadDefaultValueHolder = default({typeName});"; + } + } public override string ReturnsDefinition => "RETURNS_BYREF"; @@ -138,16 +143,14 @@ public ByRefDeclarationsProvider(Descriptor descriptor) : base(descriptor) { } internal class ByReadOnlyRefDeclarationsProvider : ByRefDeclarationsProvider { - public ByReadOnlyRefDeclarationsProvider(Descriptor descriptor) : base(descriptor) { } - - public override string ReturnsDefinition => "RETURNS_BYREF_READONLY"; + public ByReadOnlyRefDeclarationsProvider(BenchmarkCase benchmark) : base(benchmark) { } public override string WorkloadMethodReturnTypeModifiers => "ref readonly"; } internal class TaskDeclarationsProvider : VoidDeclarationsProvider { - public TaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { } + public TaskDeclarationsProvider(BenchmarkCase benchmark) : base(benchmark) { } // 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 @@ -164,7 +167,7 @@ public override string WorkloadMethodDelegate(string passArguments) /// internal class GenericTaskDeclarationsProvider : NonVoidDeclarationsProvider { - public GenericTaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { } + public GenericTaskDeclarationsProvider(BenchmarkCase benchmark) : base(benchmark) { } protected override Type WorkloadMethodReturnType => Descriptor.WorkloadMethod.ReturnType.GetTypeInfo().GetGenericArguments().Single(); diff --git a/src/BenchmarkDotNet/Engines/Consumer.cs b/src/BenchmarkDotNet/Engines/Consumer.cs index 1abedec0d6..1a3f622638 100644 --- a/src/BenchmarkDotNet/Engines/Consumer.cs +++ b/src/BenchmarkDotNet/Engines/Consumer.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; +using BenchmarkDotNet.Extensions; using JetBrains.Annotations; // ReSharper disable NotAccessedField.Local @@ -11,13 +12,6 @@ namespace BenchmarkDotNet.Engines { public class Consumer { - private static readonly HashSet SupportedTypes - = new HashSet( - typeof(Consumer).GetTypeInfo() - .DeclaredFields - .Where(field => !field.IsStatic) // exclude this HashSet itself - .Select(field => field.FieldType)); - #pragma warning disable IDE0052 // Remove unread private members private volatile byte byteHolder; private volatile sbyte sbyteHolder; @@ -123,39 +117,13 @@ public void Consume(T objectValue) where T : class // class constraint preven [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Consume(in T value) - { - if (typeof(T) == typeof(byte)) - byteHolder = (byte)(object)value; - else if (typeof(T) == typeof(sbyte)) - sbyteHolder = (sbyte)(object)value; - else if (typeof(T) == typeof(short)) - shortHolder = (short)(object)value; - else if (typeof(T) == typeof(ushort)) - ushortHolder = (ushort)(object)value; - else if (typeof(T) == typeof(int)) - intHolder = (int)(object)value; - else if (typeof(T) == typeof(uint)) - uintHolder = (uint)(object)value; - else if (typeof(T) == typeof(bool)) - boolHolder = (bool)(object)value; - else if (typeof(T) == typeof(char)) - charHolder = (char)(object)value; - else if (typeof(T) == typeof(float)) - floatHolder = (float)(object)value; - else if (typeof(T) == typeof(double)) - Volatile.Write(ref doubleHolder, (double)(object)value); - else if (typeof(T) == typeof(long)) - Volatile.Write(ref longHolder, (long)(object)value); - else if (typeof(T) == typeof(ulong)) - Volatile.Write(ref ulongHolder, (ulong)(object)value); - else if (default(T) == null && !typeof(T).IsValueType) - Consume((object) value); - else - DeadCodeEliminationHelper.KeepAliveWithoutBoxingReadonly(value); // non-primitive and nullable value types - } + // Read the value as a byte and write it to a volatile field. + // This prevents copying large structs, and prevents dead code elimination and out-of-order execution. + // (reading as a type larger than byte could possibly read past the memory bounds, causing the application to crash) + // This also works for empty structs, because the runtime enforces a minimum size of 1 byte. + => byteHolder = Unsafe.As(ref Unsafe.AsRef(in value)); - internal static bool IsConsumable(Type type) - => SupportedTypes.Contains(type) || type.GetTypeInfo().IsClass || type.GetTypeInfo().IsInterface; + internal static bool IsConsumable(Type type) => !type.IsByRefLike(); internal static bool HasConsumableField(Type type, out FieldInfo consumableField) { diff --git a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs index b38691f2c9..3186077d62 100644 --- a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using BenchmarkDotNet.Attributes; namespace BenchmarkDotNet.Extensions @@ -185,9 +187,7 @@ internal static bool IsStackOnlyWithImplicitCast(this Type argumentType, object? if (argumentInstance == null) return false; - // IsByRefLikeAttribute is not exposed for older runtimes, so we need to check it in an ugly way ;) - bool isByRefLike = argumentType.GetCustomAttributes().Any(attribute => attribute.ToString()?.Contains("IsByRefLike") ?? false); - if (!isByRefLike) + if (!argumentType.IsByRefLike()) return false; var instanceType = argumentInstance.GetType(); @@ -209,5 +209,36 @@ private static bool IsRunnableGenericType(TypeInfo typeInfo) && typeInfo.DeclaredConstructors.Any(ctor => ctor.IsPublic && ctor.GetParameters().Length == 0); // we need public parameterless ctor to create it internal static bool IsLinqPad(this Assembly assembly) => assembly.FullName.IndexOf("LINQPAD", StringComparison.OrdinalIgnoreCase) >= 0; + + internal static bool IsByRefLike(this Type type) + // Type.IsByRefLike is not available in netstandard2.0. + => type.IsValueType && type.CustomAttributes.Any(attr => attr.AttributeType.FullName == "System.Runtime.CompilerServices.IsByRefLikeAttribute"); + + // Struct size of 64 bytes was observed to be the point at which `default` may be slower in classic Mono, from benchmarks. + // Between 64 and 128 bytes, both methods may be about the same speed, depending on the complexity of the struct. + // For all types > 128 bytes, reading from a field is faster than `default`. + private const int MonoDefaultCutoffSize = 64; + + // We use the fastest possible method to return a value of the workload return type in order to prevent the overhead method from taking longer than the workload method. + internal static bool IsDefaultFasterThanField(this Type type, bool isClassicMono) + // Classic Mono runs `default` slower than reading a field for very large structs. `default` is faster for all types in all other runtimes. + => !isClassicMono + // ByRefLike and pointer cannot be used as generic arguments, so check for them before getting the size. + || type.IsByRefLike() || type.IsPointer + // We don't need to check the size for primitives and reference types. + || type.IsPrimitive || type.IsEnum || !type.IsValueType + || SizeOf(type) <= MonoDefaultCutoffSize; + + private static int SizeOf(Type type) + { + return (int) GetGenericSizeOfMethod(type).Invoke(null, null); + } + + private static MethodInfo GetGenericSizeOfMethod(Type type) + { + return typeof(Unsafe).GetMethods(BindingFlags.Static | BindingFlags.Public) + .Single(m => m.Name == nameof(Unsafe.SizeOf) && m.IsGenericMethodDefinition && m.ReturnType == typeof(int) && m.GetParameters().Length == 0) + .MakeGenericMethod(type); + } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorDefaultValueExtensions.cs b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorDefaultValueExtensions.cs index 4b23db0141..45651bacfb 100644 --- a/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorDefaultValueExtensions.cs +++ b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorDefaultValueExtensions.cs @@ -19,6 +19,9 @@ public static void EmitSetLocalToDefault(this ILGenerator ilBuilder, LocalBuilde { case Type t when t == typeof(void): break; + case Type t when t.IsPointer: // Type.IsClass returns true for pointers, so we have to check for pointer type first. + EmitInitObj(ilBuilder, resultType, local); + break; case Type t when t.IsClass || t.IsInterface: ilBuilder.Emit(OpCodes.Ldnull); ilBuilder.EmitStloc(local); @@ -39,6 +42,14 @@ public static void EmitReturnDefault(this ILGenerator ilBuilder, Type resultType { case Type t when t == typeof(void): break; + case Type t when t.IsPointer: // Type.IsClass returns true for pointers, so we have to check for pointer type first. + /* + IL_0000: ldc.i4.0 + IL_0001: conv.u + */ + ilBuilder.Emit(OpCodes.Ldc_I4_0); + ilBuilder.Emit(OpCodes.Conv_U); + break; case Type t when t.IsClass || t.IsInterface: ilBuilder.Emit(OpCodes.Ldnull); break; diff --git a/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorEmitOpExtensions.cs b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorEmitOpExtensions.cs index 50a3392f44..67ecf20232 100644 --- a/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorEmitOpExtensions.cs +++ b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorEmitOpExtensions.cs @@ -115,129 +115,5 @@ public static void EmitLdarg(this ILGenerator ilBuilder, ParameterInfo argument) break; } } - - public static void EmitLdindStind(this ILGenerator ilBuilder, Type resultType) - { - if (!resultType.IsByRef) - throw new NotSupportedException($"Cannot emit indirect op for non-reference {resultType}."); - - // The primitive types are Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double, and Single - var valueType = resultType.GetElementType(); - if (valueType?.IsEnum ?? false) - valueType = valueType.GetEnumUnderlyingType(); - - switch (valueType) - { - case Type t when t == typeof(bool): - /* - IL_0018: ldind.u1 - IL_0019: stind.i1 - */ - ilBuilder.Emit(OpCodes.Ldind_U1); - ilBuilder.Emit(OpCodes.Stind_I1); - break; - case Type t when t == typeof(byte): - /* - IL_0018: ldind.u1 - IL_0019: stind.i1 - */ - ilBuilder.Emit(OpCodes.Ldind_U1); - ilBuilder.Emit(OpCodes.Stind_I1); - break; - case Type t when t == typeof(sbyte): - /* - IL_0018: ldind.i1 - IL_0019: stind.i1 - */ - ilBuilder.Emit(OpCodes.Ldind_I1); - ilBuilder.Emit(OpCodes.Stind_I1); - break; - case Type t when t == typeof(short): - /* - IL_0018: ldind.i2 - IL_0019: stind.i2 - */ - ilBuilder.Emit(OpCodes.Ldind_I2); - ilBuilder.Emit(OpCodes.Stind_I2); - break; - case Type t1 when t1 == typeof(ushort): - case Type t2 when t2 == typeof(char): - /* - IL_0018: ldind.u2 - IL_0019: stind.i2 - */ - ilBuilder.Emit(OpCodes.Ldind_U2); - ilBuilder.Emit(OpCodes.Stind_I2); - break; - case Type t when t == typeof(int): - /* - IL_0018: ldind.i4 - IL_0019: stind.i4 - */ - ilBuilder.Emit(OpCodes.Ldind_I4); - ilBuilder.Emit(OpCodes.Stind_I4); - break; - case Type t when t == typeof(uint): - /* - IL_0018: ldind.i4 - IL_0019: stind.i4 - */ - ilBuilder.Emit(OpCodes.Ldind_U4); - ilBuilder.Emit(OpCodes.Stind_I4); - break; - case Type t1 when t1 == typeof(ulong): - case Type t2 when t2 == typeof(long): - /* - IL_0018: ldind.i8 - IL_0019: stind.i8 - */ - ilBuilder.Emit(OpCodes.Ldind_I8); - ilBuilder.Emit(OpCodes.Stind_I8); - break; - case Type t1 when t1 == typeof(IntPtr): - case Type t2 when t2 == typeof(UIntPtr): - /* - IL_0018: ldind.i - IL_0019: stind.i - */ - ilBuilder.Emit(OpCodes.Ldind_I); - ilBuilder.Emit(OpCodes.Stind_I); - break; - case Type t when t == typeof(double): - /* - IL_0018: ldind.r8 - IL_0019: stind.i8 - */ - ilBuilder.Emit(OpCodes.Ldind_R8); - ilBuilder.Emit(OpCodes.Stind_R8); - break; - case Type t when t == typeof(float): - /* - IL_0018: ldind.r4 - IL_0019: stind.i4 - */ - ilBuilder.Emit(OpCodes.Ldind_R4); - ilBuilder.Emit(OpCodes.Stind_R4); - break; - case Type t when t.IsClass || t.IsInterface: - /* - IL_0018: ldind.ref - IL_0019: stind.ref - */ - ilBuilder.Emit(OpCodes.Ldind_Ref); - ilBuilder.Emit(OpCodes.Stind_Ref); - break; - case Type t when t.IsEnum || t.IsValueType: - /* - IL_0018: ldobj valuetype [mscorlib]System.Nullable`1 - IL_0019: stobj valuetype [mscorlib]System.Nullable`1 - */ - ilBuilder.Emit(OpCodes.Ldobj, valueType); - ilBuilder.Emit(OpCodes.Stobj, valueType); - break; - default: - throw new NotSupportedException($"Cannot emit indirect store for {resultType}."); - } - } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Templates/BenchmarkType.txt b/src/BenchmarkDotNet/Templates/BenchmarkType.txt index d8f15f9138..6b03042851 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkType.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkType.txt @@ -51,8 +51,6 @@ } } - public delegate $OverheadMethodReturnTypeName$ OverheadDelegate($ArgumentsDefinition$); - public delegate $WorkloadMethodReturnTypeModifiers$ $WorkloadMethodReturnType$ WorkloadDelegate($ArgumentsDefinition$); public Runnable_$ID$() @@ -70,9 +68,10 @@ 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 overheadDelegate; private BenchmarkDotNet.Autogenerated.Runnable_$ID$.WorkloadDelegate workloadDelegate; $DeclareArgumentFields$ + $OverheadDefaultValueHolderField$ // this method is used only for the disassembly diagnoser purposes // the goal is to get this and the benchmarked method jitted, but without executing the benchmarked method itself @@ -104,7 +103,7 @@ @DummyUnroll@ } - private $OverheadMethodReturnTypeName$ __Overhead($ArgumentsDefinition$) // __ is to avoid possible name conflict + private $WorkloadMethodReturnTypeModifiers$ $WorkloadMethodReturnType$ __Overhead($ArgumentsDefinition$) // __ is to avoid possible name conflict { $OverheadImplementation$ } @@ -121,7 +120,7 @@ $LoadArguments$ for (System.Int64 i = 0; i < invokeCount; i++) { - consumer.Consume(overheadDelegate($PassArguments$));@Unroll@ + consumer.Consume(overheadDelegate($PassArguments$)$ConsumeField$);@Unroll@ } } @@ -133,7 +132,7 @@ $LoadArguments$ for (System.Int64 i = 0; i < invokeCount; i++) { - consumer.Consume(overheadDelegate($PassArguments$)); + consumer.Consume(overheadDelegate($PassArguments$)$ConsumeField$); } } @@ -181,12 +180,12 @@ private void OverheadActionUnroll(System.Int64 invokeCount) { $LoadArguments$ - $OverheadMethodReturnTypeName$ result = default($OverheadMethodReturnTypeName$); + $WorkloadMethodReturnType$ result = default($WorkloadMethodReturnType$); for (System.Int64 i = 0; i < invokeCount; i++) { result = overheadDelegate($PassArguments$);@Unroll@ } - BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(result); + NonGenericKeepAliveWithoutBoxing(result); } #if NETCOREAPP3_0_OR_GREATER @@ -195,12 +194,12 @@ private void OverheadActionNoUnroll(System.Int64 invokeCount) { $LoadArguments$ - $OverheadMethodReturnTypeName$ result = default($OverheadMethodReturnTypeName$); + $WorkloadMethodReturnType$ result = default($WorkloadMethodReturnType$); for (System.Int64 i = 0; i < invokeCount; i++) { result = overheadDelegate($PassArguments$); } - BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(result); + NonGenericKeepAliveWithoutBoxing(result); } #if NETCOREAPP3_0_OR_GREATER @@ -249,6 +248,8 @@ } #elif RETURNS_BYREF_$ID$ + + private BenchmarkDotNet.Engines.Consumer consumer = new BenchmarkDotNet.Engines.Consumer(); #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] @@ -256,12 +257,10 @@ private void OverheadActionUnroll(System.Int64 invokeCount) { $LoadArguments$ - $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); for (System.Int64 i = 0; i < invokeCount; i++) { - value = overheadDelegate($PassArguments$);@Unroll@ + consumer.Consume(in overheadDelegate($PassArguments$));@Unroll@ } - BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); } #if NETCOREAPP3_0_OR_GREATER @@ -270,28 +269,22 @@ private void OverheadActionNoUnroll(System.Int64 invokeCount) { $LoadArguments$ - $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); for (System.Int64 i = 0; i < invokeCount; i++) { - value = overheadDelegate($PassArguments$); + consumer.Consume(in overheadDelegate($PassArguments$)); } - BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); } - private $WorkloadMethodReturnType$ workloadDefaultValueHolder = default($WorkloadMethodReturnType$); - #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif private void WorkloadActionUnroll(System.Int64 invokeCount) { $LoadArguments$ - ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; for (System.Int64 i = 0; i < invokeCount; i++) { - alias = workloadDelegate($PassArguments$);@Unroll@ + consumer.Consume(in workloadDelegate($PassArguments$));@Unroll@ } - BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(ref alias); } #if NETCOREAPP3_0_OR_GREATER @@ -300,16 +293,14 @@ private void WorkloadActionNoUnroll(System.Int64 invokeCount) { $LoadArguments$ - ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; for (System.Int64 i = 0; i < invokeCount; i++) { - alias = workloadDelegate($PassArguments$); + consumer.Consume(in workloadDelegate($PassArguments$)); } - BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(ref alias); } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - public ref $WorkloadMethodReturnType$ $DisassemblerEntryMethodName$() + public $WorkloadMethodReturnTypeModifiers$ $WorkloadMethodReturnType$ $DisassemblerEntryMethodName$() { if (NotEleven == 11) { @@ -317,79 +308,9 @@ return ref $WorkloadMethodCall$; } - return ref workloadDefaultValueHolder; - } -#elif RETURNS_BYREF_READONLY_$ID$ - -#if NETCOREAPP3_0_OR_GREATER - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] -#endif - private void OverheadActionUnroll(System.Int64 invokeCount) - { - $LoadArguments$ - $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); - for (System.Int64 i = 0; i < invokeCount; i++) - { - value = overheadDelegate($PassArguments$);@Unroll@ - } - BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); - } - -#if NETCOREAPP3_0_OR_GREATER - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] -#endif - private void OverheadActionNoUnroll(System.Int64 invokeCount) - { - $LoadArguments$ - $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); - for (System.Int64 i = 0; i < invokeCount; i++) - { - value = overheadDelegate($PassArguments$); - } - BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); - } - - private $WorkloadMethodReturnType$ workloadDefaultValueHolder = default($WorkloadMethodReturnType$); - -#if NETCOREAPP3_0_OR_GREATER - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] -#endif - private void WorkloadActionUnroll(System.Int64 invokeCount) - { - $LoadArguments$ - ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; - for (System.Int64 i = 0; i < invokeCount; i++) - { - alias = workloadDelegate($PassArguments$);@Unroll@ - } - BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxingReadonly(alias); + return ref overheadDefaultValueHolder; } -#if NETCOREAPP3_0_OR_GREATER - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] -#endif - private void WorkloadActionNoUnroll(System.Int64 invokeCount) - { - $LoadArguments$ - ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; - for (System.Int64 i = 0; i < invokeCount; i++) - { - alias = workloadDelegate($PassArguments$); - } - BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxingReadonly(alias); - } - - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - public ref readonly $WorkloadMethodReturnType$ $DisassemblerEntryMethodName$() - { - if (NotEleven == 11) - { - $LoadArguments$ - return ref $WorkloadMethodCall$; - } - - return ref workloadDefaultValueHolder; - } #elif RETURNS_VOID_$ID$ #if NETCOREAPP3_0_OR_GREATER diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/ConsumableTypeInfo.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/ConsumableTypeInfo.cs index 0fde3ac1ee..0b70ce9a8d 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/ConsumableTypeInfo.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/ConsumableTypeInfo.cs @@ -1,5 +1,4 @@ using BenchmarkDotNet.Engines; -using JetBrains.Annotations; using System; using System.Reflection; using System.Runtime.CompilerServices; @@ -48,32 +47,21 @@ public ConsumableTypeInfo(Type methodReturnType) if (WorkloadMethodReturnType == typeof(void)) { IsVoid = true; - OverheadMethodReturnType = WorkloadMethodReturnType; } else if (WorkloadMethodReturnType.IsByRef) { IsByRef = true; - OverheadMethodReturnType = typeof(IntPtr); } else if (Consumer.IsConsumable(WorkloadMethodReturnType) || Consumer.HasConsumableField(WorkloadMethodReturnType, out consumableField)) { IsConsumable = true; WorkloadConsumableField = consumableField; - OverheadMethodReturnType = consumableField?.FieldType ?? WorkloadMethodReturnType; } - else - { - OverheadMethodReturnType = typeof(int); // we return this simple type because creating bigger ValueType could take longer than benchmarked method itself - } - - if (OverheadMethodReturnType == null) - throw new InvalidOperationException("Bug: (OverheadResultType == null"); } public Type OriginMethodReturnType { get; } public Type WorkloadMethodReturnType { get; } - public Type OverheadMethodReturnType { get; } public MethodInfo? GetAwaiterMethod { get; } public MethodInfo? GetResultMethod { get; } diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ByRefConsumeEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ByRefConsumeEmitter.cs index 96ed6d9578..a999ab40ac 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ByRefConsumeEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ByRefConsumeEmitter.cs @@ -1,20 +1,13 @@ -using System; -using System.Linq; +using BenchmarkDotNet.Engines; +using System; using System.Reflection; using System.Reflection.Emit; -using BenchmarkDotNet.Engines; -using BenchmarkDotNet.Helpers.Reflection.Emit; using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation { - internal class ByRefConsumeEmitter : ConsumeEmitter + internal class ByRefConsumeEmitter : ConsumableConsumeEmitter { - private FieldBuilder workloadDefaultValueHolderField; - private MethodInfo overheadKeepAliveWithoutBoxingMethod; - private MethodInfo workloadKeepAliveWithoutBoxingMethod; - private LocalBuilder resultLocal; - public ByRefConsumeEmitter(ConsumableTypeInfo consumableTypeInfo) : base(consumableTypeInfo) { } protected override void OnDefineFieldsOverride(TypeBuilder runnableBuilder) @@ -23,130 +16,57 @@ protected override void OnDefineFieldsOverride(TypeBuilder runnableBuilder) if (nonRefType == null) throw new InvalidOperationException($"Bug: type {ConsumableInfo.WorkloadMethodReturnType} is non-ref type."); - workloadDefaultValueHolderField = runnableBuilder.DefineField( - WorkloadDefaultValueHolderFieldName, + overheadDefaultValueHolderField = runnableBuilder.DefineField( + OverheadDefaultValueHolderFieldName, nonRefType, FieldAttributes.Private); + + consumerField = runnableBuilder.DefineField(ConsumerFieldName, typeof(Consumer), FieldAttributes.Private); } protected override void EmitDisassemblyDiagnoserReturnDefaultOverride(ILGenerator ilBuilder) { /* - // return ref workloadDefaultValueHolder; + // return ref overheadDefaultValueHolder; IL_0031: ldarg.0 - IL_0032: ldflda int32 BenchmarkDotNet.Autogenerated.Runnable_0::workloadDefaultValueHolder + IL_0032: ldflda int32 BenchmarkDotNet.Autogenerated.Runnable_0::overheadDefaultValueHolder IL_0037: ret */ ilBuilder.Emit(OpCodes.Ldarg_0); - ilBuilder.Emit(OpCodes.Ldflda, workloadDefaultValueHolderField); + ilBuilder.Emit(OpCodes.Ldflda, overheadDefaultValueHolderField); ilBuilder.Emit(OpCodes.Ret); } - protected override void OnEmitMembersOverride(TypeBuilder runnableBuilder) + public override void EmitOverheadImplementation(ILGenerator ilBuilder, Type returnType) { - overheadKeepAliveWithoutBoxingMethod = typeof(DeadCodeEliminationHelper).GetMethods() - .First(m => m.Name == nameof(DeadCodeEliminationHelper.KeepAliveWithoutBoxing) - && m.GetParameterTypes().First().IsByRef == false) - .MakeGenericMethod(ConsumableInfo.OverheadMethodReturnType); - - workloadKeepAliveWithoutBoxingMethod = typeof(DeadCodeEliminationHelper).GetMethods() - .First(m => m.Name == nameof(DeadCodeEliminationHelper.KeepAliveWithoutBoxing) - && m.GetParameterTypes().First().IsByRef) - .MakeGenericMethod(ConsumableInfo.WorkloadMethodReturnType.GetElementType()); + EmitDisassemblyDiagnoserReturnDefaultOverride(ilBuilder); } - protected override void DeclareActionLocalsOverride(ILGenerator ilBuilder) + protected override void DeclareLoopLocalsOverride(ILGenerator ilBuilder) { - /* - .locals init ( - [4] native int, - ) - -or- - .locals init ( - [4] int32&, - ) - */ - if (ActionKind == RunnableActionKind.Overhead) - resultLocal = ilBuilder.DeclareLocal(ConsumableInfo.OverheadMethodReturnType); - else - resultLocal = ilBuilder.DeclareLocal(ConsumableInfo.WorkloadMethodReturnType); + // We don't need a local to pass by reference since it's returned by reference. } - /// Emits the action before loop override. - /// The il builder. - /// EmitActionKind - null - protected override void EmitActionBeforeLoopOverride(ILGenerator ilBuilder) + protected override void EmitActionAfterCallOverride(ILGenerator ilBuilder) { /* - // IntPtr value = default(IntPtr); - IL_001c: ldloca.s 4 - IL_001e: initobj [mscorlib]System.IntPtr - -or- - // ref int reference = ref workloadDefaultValueHolder; - IL_001c: ldarg.0 - IL_001d: ldflda int32 BenchmarkDotNet.Autogenerated.Runnable_0::workloadDefaultValueHolder - IL_0022: stloc.s 4 + // ... .Consume( ... ) + IL_003f: callvirt instance void Consumer::Consume(!!0&) */ - if (ActionKind == RunnableActionKind.Overhead) - { - ilBuilder.EmitLdloca(resultLocal); - ilBuilder.Emit(OpCodes.Initobj, ConsumableInfo.OverheadMethodReturnType); - } - else - { - ilBuilder.Emit(OpCodes.Ldarg_0); - ilBuilder.Emit(OpCodes.Ldflda, workloadDefaultValueHolderField); - ilBuilder.EmitStloc(resultLocal); - } + var consumeMethod = GetConsumeMethod(ConsumableInfo.WorkloadMethodReturnType); + ilBuilder.Emit(OpCodes.Callvirt, consumeMethod); } - protected override void EmitActionBeforeCallOverride(ILGenerator ilBuilder) + private static MethodInfo GetConsumeMethod(Type consumableType) { - /* - - -or- - // reference = ... - IL_002a: ldloc.s 4 - */ - if (ActionKind != RunnableActionKind.Overhead) - { - ilBuilder.EmitLdloc(resultLocal); - } - } + // Consume(in T value) + var consumeMethod = GetGenericConsumeMethod(consumableType.GetElementType(), param => param.IsIn); - protected override void EmitActionAfterCallOverride(ILGenerator ilBuilder) - { - /* - IL_0039: stloc.s 4 - -or- - // reference = ... - IL_003b: ldind.i4 - IL_003c: stind.i4 - */ - if (ActionKind == RunnableActionKind.Overhead) + if (consumeMethod == null) { - ilBuilder.EmitStloc(resultLocal); + throw new InvalidOperationException($"Cannot consume result of {consumableType}."); } - else - { - ilBuilder.EmitLdindStind(resultLocal.LocalType); - } - } - protected override void EmitActionAfterLoopOverride(ILGenerator ilBuilder) - { - /* - // DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); - IL_007a: ldloc.s 4 - IL_007c: call void [BenchmarkDotNet]BenchmarkDotNet.Engines.DeadCodeEliminationHelper::KeepAliveWithoutBoxing(!!0) - -or- - // DeadCodeEliminationHelper.KeepAliveWithoutBoxing(ref reference); - IL_0082: ldloc.s 4 - IL_0084: call void [BenchmarkDotNet]BenchmarkDotNet.Engines.DeadCodeEliminationHelper::KeepAliveWithoutBoxing(!!0&) - */ - if (ActionKind == RunnableActionKind.Overhead) - ilBuilder.EmitStaticCall(overheadKeepAliveWithoutBoxingMethod, resultLocal); - else - ilBuilder.EmitStaticCall(workloadKeepAliveWithoutBoxingMethod, resultLocal); + return consumeMethod; } } } \ 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..7bdf83580e 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumableConsumeEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumableConsumeEmitter.cs @@ -3,59 +3,34 @@ using System.Reflection; using System.Reflection.Emit; using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers.Reflection.Emit; +using BenchmarkDotNet.Portability; namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation { internal class ConsumableConsumeEmitter : ConsumeEmitter { - private static MethodInfo GetConsumeMethod(Type consumableType) - { - var consumeMethod = typeof(Consumer).GetMethod(nameof(Consumer.Consume), new[] { consumableType }); - - // Use generic method for ref types - if (consumeMethod == null || consumeMethod.GetParameterTypes().FirstOrDefault() == typeof(object)) - { - if (consumableType.IsClass || consumableType.IsInterface) - { - consumeMethod = typeof(Consumer) - .GetMethods() - .Single(m => - { - Type argType = m.GetParameterTypes().FirstOrDefault(); - - return m.Name == nameof(Consumer.Consume) && m.IsGenericMethodDefinition - && !argType.IsByRef // we are not interested in "Consume(in T value)" - && argType.IsPointer == consumableType.IsPointer; // use "Consume(T objectValue) where T : class" or "Consume(T* ptrValue) where T: unmanaged" - }); - - consumeMethod = consumableType.IsPointer - ? consumeMethod.MakeGenericMethod(consumableType.GetElementType()) // consumableType is T*, we need T for Consume(T* ptrValue) - : consumeMethod.MakeGenericMethod(consumableType); - } - else - { - consumeMethod = null; - } - } - - if (consumeMethod == null) - { - throw new InvalidOperationException($"Cannot consume result of {consumableType}."); - } - - return consumeMethod; - } - - private FieldBuilder consumerField; + protected FieldBuilder overheadDefaultValueHolderField; + protected FieldBuilder consumerField; private LocalBuilder disassemblyDiagnoserLocal; + private LocalBuilder resultLocal; + private readonly bool overheadReturnsDefault; public ConsumableConsumeEmitter(ConsumableTypeInfo consumableTypeInfo) : base(consumableTypeInfo) { + overheadReturnsDefault = consumableTypeInfo.WorkloadMethodReturnType.IsDefaultFasterThanField(RuntimeInformation.IsOldMono); } protected override void OnDefineFieldsOverride(TypeBuilder runnableBuilder) { + if (!overheadReturnsDefault) + { + overheadDefaultValueHolderField = runnableBuilder.DefineField( + RunnableConstants.OverheadDefaultValueHolderFieldName, + ConsumableInfo.WorkloadMethodReturnType, FieldAttributes.Private); + } + consumerField = runnableBuilder.DefineField(RunnableConstants.ConsumerFieldName, typeof(Consumer), FieldAttributes.Private); } @@ -70,6 +45,32 @@ protected override void EmitDisassemblyDiagnoserReturnDefaultOverride(ILGenerato ilBuilder.EmitReturnDefault(ConsumableInfo.WorkloadMethodReturnType, disassemblyDiagnoserLocal); } + protected override void DeclareLoopLocalsOverride(ILGenerator ilBuilder) + { + /* + .locals init ( + [4] native int, + ) + */ + var consumeField = ConsumableInfo.WorkloadConsumableField; + if (consumeField == null) + { + GetConsumeMethod(ConsumableInfo.WorkloadMethodReturnType, out bool passByRef); + if (passByRef) + { + resultLocal = ilBuilder.DeclareLocal(ConsumableInfo.WorkloadMethodReturnType); + } + } + else + { + GetConsumeMethod(consumeField.FieldType, out bool passByRef); + if (passByRef) + { + resultLocal = ilBuilder.DeclareLocal(consumeField.FieldType); + } + } + } + protected override void OnEmitCtorBodyOverride(ConstructorBuilder constructorBuilder, ILGenerator ilBuilder) { var ctor = typeof(Consumer).GetConstructor(Array.Empty()); @@ -87,6 +88,32 @@ protected override void OnEmitCtorBodyOverride(ConstructorBuilder constructorBui ilBuilder.Emit(OpCodes.Stfld, consumerField); } + public override void EmitOverheadImplementation(ILGenerator ilBuilder, Type returnType) + { + if (overheadReturnsDefault) + { + /* + // return default; + IL_0000: ldc.i4.0 + IL_0001: ret + */ + // optional local if default(T) uses .initobj + var optionalLocalForInitobj = ilBuilder.DeclareOptionalLocalForReturnDefault(returnType); + ilBuilder.EmitReturnDefault(returnType, optionalLocalForInitobj); + return; + } + + /* + // return overheadDefaultValueHolder; + IL_0000: ldarg.0 + IL_0001: ldfld int32 C::'field' + IL_0006: ret + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, overheadDefaultValueHolderField); + ilBuilder.Emit(OpCodes.Ret); + } + protected override void EmitActionBeforeCallOverride(ILGenerator ilBuilder) { /* @@ -107,29 +134,80 @@ protected override void EmitActionAfterCallOverride(ILGenerator ilBuilder) // ... .Consume( ... .ConsumableField); IL_001e: callvirt instance void [BenchmarkDotNet]BenchmarkDotNet.Engines.Consumer::Consume(int32) // -or- .Consume( ... ); - IL_001e: ldfld int32 BenchmarkDotNet.Samples.CustomWithConsumable::ConsumableField - IL_0023: callvirt instance void [BenchmarkDotNet]BenchmarkDotNet.Engines.Consumer::Consume(int32) + IL_0018: valuetype [System.Runtime]System.Decimal int32 BenchmarkDotNet.Samples.CustomWithConsumable::ConsumableField + IL_001d: stloc.1 + IL_001e: ldloca.s 1 + IL_0020: callvirt instance void [BenchmarkDotNet]BenchmarkDotNet.Engines.Consumer::Consume(!!0&) */ - if (ActionKind == RunnableActionKind.Overhead) + var consumeField = ConsumableInfo.WorkloadConsumableField; + if (consumeField == null) { - var overheadConsumeMethod = GetConsumeMethod(ConsumableInfo.OverheadMethodReturnType); - ilBuilder.Emit(OpCodes.Callvirt, overheadConsumeMethod); + var consumeMethod = GetConsumeMethod(ConsumableInfo.WorkloadMethodReturnType, out bool passByRef); + if (passByRef) + { + ilBuilder.EmitStloc(resultLocal); + ilBuilder.EmitLdloca(resultLocal); + } + ilBuilder.Emit(OpCodes.Callvirt, consumeMethod); } else { - var consumeField = ConsumableInfo.WorkloadConsumableField; - if (consumeField == null) + var consumeMethod = GetConsumeMethod(consumeField.FieldType, out bool passByRef); + if (passByRef) { - var consumeMethod = GetConsumeMethod(ConsumableInfo.WorkloadMethodReturnType); - ilBuilder.Emit(OpCodes.Callvirt, consumeMethod); + ilBuilder.EmitStloc(resultLocal); + ilBuilder.EmitLdloca(resultLocal); + ilBuilder.Emit(OpCodes.Ldflda, consumeField); } else { - var consumeMethod = GetConsumeMethod(consumeField.FieldType); ilBuilder.Emit(OpCodes.Ldfld, consumeField); - ilBuilder.Emit(OpCodes.Callvirt, consumeMethod); + } + ilBuilder.Emit(OpCodes.Callvirt, consumeMethod); + } + } + + protected static MethodInfo GetGenericConsumeMethod(Type consumableType, Func comparator) + { + return typeof(Consumer).GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Single(m => m.Name == nameof(Consumer.Consume) && m.IsGenericMethodDefinition && comparator(m.GetParameters().Single())) + .MakeGenericMethod(consumableType); + } + + private static MethodInfo GetConsumeMethod(Type consumableType, out bool passByRef) + { + passByRef = false; + var consumeMethod = typeof(Consumer).GetMethod(nameof(Consumer.Consume), new[] { consumableType }); + + if (consumeMethod == null + // Use generic method for ref types, except base System.Object. + || (consumeMethod.GetParameterTypes().FirstOrDefault() == typeof(object) && consumableType != typeof(object))) + { + // Consume(T* ptrValue) + if (consumableType.IsPointer) + { + consumeMethod = GetGenericConsumeMethod(consumableType.GetElementType(), param => param.ParameterType.IsPointer); + } + // Consume(T value) where T : class + else if (consumableType.IsClass || consumableType.IsInterface) + { + consumeMethod = GetGenericConsumeMethod(consumableType, param => !param.IsIn && !param.ParameterType.IsPointer); + } + // Everything else, covers non-primitve structs, nullable values, and byref returns. + // Consume(in T value) + else + { + passByRef = true; + consumeMethod = GetGenericConsumeMethod(consumableType, param => param.IsIn); } } + + if (consumeMethod == null) + { + throw new InvalidOperationException($"Cannot consume result of {consumableType}."); + } + + return consumeMethod; } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumeEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumeEmitter.cs index 62fe06c649..476c329852 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumeEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumeEmitter.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using System.Reflection.Emit; +using BenchmarkDotNet.Helpers.Reflection.Emit; using JetBrains.Annotations; namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation @@ -130,6 +131,18 @@ protected virtual void EmitDisassemblyDiagnoserReturnDefaultOverride(ILGenerator { } + public virtual void EmitOverheadImplementation(ILGenerator ilBuilder, Type returnType) + { + /* + // return default; + IL_0000: ldc.i4.0 + IL_0001: ret + */ + // optional local if default(T) uses .initobj + var optionalLocalForInitobj = ilBuilder.DeclareOptionalLocalForReturnDefault(returnType); + ilBuilder.EmitReturnDefault(returnType, optionalLocalForInitobj); + } + public void BeginEmitAction( MethodBuilder actionMethodBuilder, ILGenerator ilBuilder, @@ -180,6 +193,18 @@ protected virtual void DeclareActionLocalsOverride(ILGenerator ilBuilder) { } + public void DeclareLoopLocals(ILGenerator ilBuilder) + { + + AssertHasBuilder(ilBuilder); + + DeclareLoopLocalsOverride(ilBuilder); + } + + protected virtual void DeclareLoopLocalsOverride(ILGenerator ilBuilder) + { + } + public void EmitActionBeforeLoop(ILGenerator ilBuilder) { AssertHasBuilder(ilBuilder); diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/NonConsumableConsumeEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/NonConsumableConsumeEmitter.cs index 4087165f3b..a3d0f1d179 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/NonConsumableConsumeEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/NonConsumableConsumeEmitter.cs @@ -10,7 +10,6 @@ namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation { internal class NonConsumableConsumeEmitter : ConsumeEmitter { - private MethodInfo overheadKeepAliveWithoutBoxingMethod; private MethodInfo nonGenericKeepAliveWithoutBoxingMethod; private LocalBuilder resultLocal; private LocalBuilder disassemblyDiagnoserLocal; @@ -21,11 +20,6 @@ public NonConsumableConsumeEmitter(ConsumableTypeInfo consumableTypeInfo) : base protected override void OnEmitMembersOverride(TypeBuilder runnableBuilder) { - overheadKeepAliveWithoutBoxingMethod = typeof(DeadCodeEliminationHelper).GetMethods() - .First(m => m.Name == nameof(DeadCodeEliminationHelper.KeepAliveWithoutBoxing) - && !m.GetParameterTypes().First().IsByRef) - .MakeGenericMethod(ConsumableInfo.OverheadMethodReturnType); - // we must not simply use DeadCodeEliminationHelper.KeepAliveWithoutBoxing because it's generic method // and stack-only types like Span can not be generic type arguments http://adamsitnik.com/Span/#span-must-not-be-a-generic-type-argument nonGenericKeepAliveWithoutBoxingMethod = EmitNonGenericKeepAliveWithoutBoxing( @@ -49,7 +43,7 @@ private MethodBuilder EmitNonGenericKeepAliveWithoutBoxing(string methodName, Ty /* method private hidebysig instance void NonGenericKeepAliveWithoutBoxing( - valuetype BenchmarkDotNet.Samples.CustomStructNonConsumable _ + valuetype BenchmarkDotNet.Samples.EmptyStruct _ ) cil managed noinlining */ var valueArg = new EmitParameterInfo( @@ -79,17 +73,10 @@ protected override void DeclareActionLocalsOverride(ILGenerator ilBuilder) { /* .locals init ( - [2] int32 - ) - -or- - .locals init ( - [2] valuetype BenchmarkDotNet.Samples.CustomStructNonConsumable, + [2] valuetype BenchmarkDotNet.Samples.EmptyStruct, ) */ - if (ActionKind == RunnableActionKind.Overhead) - resultLocal = ilBuilder.DeclareLocal(ConsumableInfo.OverheadMethodReturnType); - else - resultLocal = ilBuilder.DeclareLocal(ConsumableInfo.WorkloadMethodReturnType); + resultLocal = ilBuilder.DeclareLocal(ConsumableInfo.WorkloadMethodReturnType); } /// Emits the action before loop override. @@ -102,9 +89,9 @@ protected override void EmitActionBeforeLoopOverride(ILGenerator ilBuilder) IL_000e: ldc.i4.0 IL_000f: stloc.2 -or- - // CustomStructNonConsumable _ = default(CustomStructNonConsumable); + // EmptyStruct _ = default(EmptyStruct); IL_000e: ldloca.s 2 - IL_0010: initobj BenchmarkDotNet.Samples.CustomStructNonConsumable + IL_0010: initobj BenchmarkDotNet.Samples.EmptyStruct */ ilBuilder.EmitSetLocalToDefault(resultLocal); } @@ -118,28 +105,17 @@ protected override void EmitActionAfterCallOverride(ILGenerator ilBuilder) protected override void EmitActionAfterLoopOverride(ILGenerator ilBuilder) { /* - // DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); - IL_002c: ldloc.2 - IL_002d: call void [BenchmarkDotNet]BenchmarkDotNet.Engines.DeadCodeEliminationHelper::KeepAliveWithoutBoxing(!!0) - -or- // NonGenericKeepAliveWithoutBoxing(_); IL_0032: ldarg.0 IL_0033: ldloc.2 - IL_0034: call instance void BenchmarkDotNet.Autogenerated.Runnable_0::NonGenericKeepAliveWithoutBoxing(valuetype BenchmarkDotNet.Samples.CustomStructNonConsumable) + IL_0034: call instance void BenchmarkDotNet.Autogenerated.Runnable_0::NonGenericKeepAliveWithoutBoxing(valuetype BenchmarkDotNet.Samples.EmptyStruct) */ - if (ActionKind == RunnableActionKind.Overhead) - { - ilBuilder.EmitStaticCall(overheadKeepAliveWithoutBoxingMethod, resultLocal); - } - else - { - ilBuilder.Emit(OpCodes.Ldarg_0); - ilBuilder.EmitInstanceCallThisValueOnStack( - null, - nonGenericKeepAliveWithoutBoxingMethod, - new[] { resultLocal }, - forceDirectCall: true); - } + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitInstanceCallThisValueOnStack( + null, + nonGenericKeepAliveWithoutBoxingMethod, + new[] { resultLocal }, + forceDirectCall: true); } } } \ 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 7f9d47c62f..3c80e74622 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs @@ -9,6 +9,7 @@ using System.Security; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers.Reflection.Emit; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; @@ -240,7 +241,6 @@ private static void EmitNoArgsMethodCallPopReturn( private int jobUnrollFactor; private int dummyUnrollFactor; - private Type overheadDelegateType; private Type workloadDelegateType; private TypeBuilder runnableBuilder; private ConsumableTypeInfo consumableInfo; @@ -359,33 +359,9 @@ private void InitForEmitRunnable(BenchmarkBuildInfo newBenchmark) // Init types 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 @@ -420,7 +396,7 @@ private void DefineFields() iterationCleanupActionField = runnableBuilder.DefineField(IterationCleanupActionFieldName, typeof(Action), FieldAttributes.Private); overheadDelegateField = - runnableBuilder.DefineField(OverheadDelegateFieldName, overheadDelegateType, FieldAttributes.Private); + runnableBuilder.DefineField(OverheadDelegateFieldName, workloadDelegateType, FieldAttributes.Private); workloadDelegateField = runnableBuilder.DefineField(WorkloadDelegateFieldName, workloadDelegateType, FieldAttributes.Private); @@ -439,7 +415,7 @@ private void DefineFields() argFieldType = argLocalsType.GetElementType() ?? throw new InvalidOperationException($"Bug: cannot get field type from {argLocalsType}"); } - else if (IsRefLikeType(parameterType) && argValue.Value != null) + else if (parameterType.IsByRefLike() && argValue.Value != null) { argLocalsType = parameterType; @@ -456,7 +432,7 @@ private void DefineFields() argFieldType = parameterType; } - if (IsRefLikeType(argFieldType)) + if (argFieldType.IsByRefLike()) throw new NotSupportedException( $"Passing ref readonly structs by ref is not supported (cannot store {argFieldType} as a class field)."); @@ -538,7 +514,7 @@ private MethodBuilder EmitDummyMethod(string methodName, int unrollFactor) private MethodBuilder EmitOverheadImplementation(string methodName) { - var overheadInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(overheadDelegateType); + var overheadInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(workloadDelegateType); //.method private hidebysig // instance int32 __Overhead(int64 arg0) cil managed @@ -548,17 +524,7 @@ private MethodBuilder EmitOverheadImplementation(string methodName) overheadInvokeMethod.ReturnParameter, overheadInvokeMethod.GetParameters()); - var ilBuilder = methodBuilder.GetILGenerator(); - var returnType = methodBuilder.ReturnType; - - /* - // return default; - IL_0000: ldc.i4.0 - IL_0001: ret - */ - // optional local if default(T) uses .initobj - var optionalLocalForInitobj = ilBuilder.DeclareOptionalLocalForReturnDefault(returnType); - ilBuilder.EmitReturnDefault(returnType, optionalLocalForInitobj); + consumeEmitter.EmitOverheadImplementation(methodBuilder.GetILGenerator(), methodBuilder.ReturnType); return methodBuilder; } @@ -643,7 +609,7 @@ private MethodBuilder EmitActionImpl(string methodName, RunnableActionKind actio { case RunnableActionKind.Overhead: actionDelegateField = overheadDelegateField; - actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(overheadDelegateType); + actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(workloadDelegateType); break; case RunnableActionKind.Workload: actionDelegateField = workloadDelegateField; @@ -680,6 +646,7 @@ private MethodBuilder EmitActionImpl(string methodName, RunnableActionKind actio var loopStartLabel = ilBuilder.DefineLabel(); var loopHeadLabel = ilBuilder.DefineLabel(); ilBuilder.EmitLoopBeginFromLocToArg(loopStartLabel, loopHeadLabel, indexLocal, toArg); + consumeEmitter.DeclareLoopLocals(ilBuilder); { /* // overheadDelegate(); @@ -696,6 +663,7 @@ private MethodBuilder EmitActionImpl(string methodName, RunnableActionKind actio 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); diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs index c6e8cd8ae1..2cdcf5dcbf 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs @@ -6,13 +6,11 @@ /// public class RunnableConstants { - public const string IsByRefLikeAttributeTypeName = "System.Runtime.CompilerServices.IsByRefLikeAttribute"; public const string OpImplicitMethodName = "op_Implicit"; public const string DynamicAssemblySuffix = "Emitted"; public const string EmittedTypePrefix = "BenchmarkDotNet.Autogenerated.Runnable_"; public const string WorkloadDelegateTypeSuffix = "WorkloadDelegate"; - public const string OverheadDelegateTypeSuffix = "OverheadDelegate"; public const string ArgFieldPrefix = "__argField"; public const string ArgParamPrefix = "arg"; @@ -41,7 +39,7 @@ public class RunnableConstants public const string ConsumerFieldName = "consumer"; public const string NonGenericKeepAliveWithoutBoxingMethodName = "NonGenericKeepAliveWithoutBoxing"; public const string DummyParamName = "_"; - public const string WorkloadDefaultValueHolderFieldName = "workloadDefaultValueHolder"; + public const string OverheadDefaultValueHolderFieldName = "overheadDefaultValueHolder"; public const string GlobalSetupMethodName = "GlobalSetup"; public const string GlobalCleanupMethodName = "GlobalCleanup"; diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReflectionHelpers.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReflectionHelpers.cs index f8cfd02ad1..63404cba71 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReflectionHelpers.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReflectionHelpers.cs @@ -37,12 +37,6 @@ private static object TryChangeType(object value, Type targetType) return value; } - public static bool IsRefLikeType(Type t) - { - return t.IsValueType - && t.GetCustomAttributes().Any(a => a.GetType().FullName == IsByRefLikeAttributeTypeName); - } - public static MethodInfo GetImplicitConversionOpFromTo(Type from, Type to) { return GetImplicitConversionOpCore(to, from, to) diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableStructCaseBenchmark.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableStructCaseBenchmark.cs index 1a2feea77b..da47081cee 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableStructCaseBenchmark.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableStructCaseBenchmark.cs @@ -209,15 +209,15 @@ public class RunnableStructCaseBenchmark [Benchmark, Arguments(null, "19", 0.19)] public TimeSpan? StructCase19(TimeSpan? x, ref string y, double? z) => default; - // ---- Begin StructCase(CustomEnumNonConsumable) ---- + // ---- Begin StructCase(EmptyEnum) ---- - private CustomEnumNonConsumable _refResultHolder20; + private EmptyEnum _refResultHolder20; [Benchmark] - public ref CustomEnumNonConsumable RefReturnStructCase20() => ref _refResultHolder20; + public ref EmptyEnum RefReturnStructCase20() => ref _refResultHolder20; [Benchmark, Arguments(20, "20", 0.20)] - public CustomEnumNonConsumable StructCase20(int x, ref string y, double? z) => default; + public EmptyEnum StructCase20(int x, ref string y, double? z) => default; // ---- Begin StructCase(CustomEnumConsumable) ---- @@ -229,15 +229,10 @@ public class RunnableStructCaseBenchmark [Benchmark, Arguments(21, "21", 0.21)] public CustomEnumConsumable StructCase21(int x, ref string y, double? z) => default; - // ---- Begin StructCase(CustomStructNonConsumable) ---- - - private CustomStructNonConsumable _refResultHolder22; - - [Benchmark] - public ref CustomStructNonConsumable RefReturnStructCase22() => ref _refResultHolder22; + // ---- Begin StructCase(EmptyStruct) ---- [Benchmark, Arguments(22, "22", 0.22)] - public CustomStructNonConsumable StructCase22(int x, ref string y, double? z) => default; + public EmptyStruct StructCase22(int x, ref string y, double? z) => default; // ---- Begin StructCase(CustomStructConsumable) ---- diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/RunnableTestCasesHelperTypes.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/RunnableTestCasesHelperTypes.cs index 197a5b90a4..40476a896a 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/RunnableTestCasesHelperTypes.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/RunnableTestCasesHelperTypes.cs @@ -4,7 +4,7 @@ namespace BenchmarkDotNet.IntegrationTests.InProcess.EmitTests { - public enum CustomEnumNonConsumable + public enum EmptyEnum { } @@ -14,7 +14,7 @@ public enum CustomEnumConsumable } - public struct CustomStructNonConsumable + public struct EmptyStruct { } diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/SampleBenchmark.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/SampleBenchmark.cs index 31fc7a9283..186f4b6b2d 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/SampleBenchmark.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/SampleBenchmark.cs @@ -28,7 +28,7 @@ public string ReturnSingleArgCase(int i) } [Benchmark, Arguments(123.0, 4, "5", null)] - public CustomStructNonConsumable ReturnManyArgsCase(ref double i, int j, string k, object l) + public EmptyStruct ReturnManyArgsCase(ref double i, int j, string k, object l) { Thread.Sleep(100); return default; @@ -43,6 +43,27 @@ public ref int RefReturnManyArgsCase(ref double i, int j, string k, object l) return ref refValueHolder; } + [Benchmark] + public unsafe int* ReturnsIntPointer() + { + Thread.Sleep(100); + return default; + } + + [Benchmark] + public unsafe void* ReturnsVoidPointer() + { + Thread.Sleep(100); + return default; + } + + [Benchmark] + public unsafe EmptyStruct* ReturnsStructPointer() + { + Thread.Sleep(100); + return default; + } + [Benchmark, Arguments(12)] public Task TaskSample(long arg) {