From 878842f76b567fb10a7d7084f3f92f13eb2b92eb Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 10 Jun 2023 00:36:18 -0400 Subject: [PATCH] Overhead reads from field if the return type is large struct and runtime is old Mono. --- src/BenchmarkDotNet/Code/CodeGenerator.cs | 19 +++---- .../Code/DeclarationsProvider.cs | 50 +++++++++++++++---- .../Extensions/ReflectionExtensions.cs | 38 ++++++++++++++ .../Templates/BenchmarkType.txt | 2 +- .../Emitters/ByRefConsumeEmitter.cs | 8 ++- .../Emitters/ConsumableConsumeEmitter.cs | 40 ++++++++++++++- 6 files changed, 131 insertions(+), 26 deletions(-) diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs index ea3195a21e..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); @@ -53,6 +53,7 @@ internal static string Generate(BuildPartition buildPartition) .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)) @@ -147,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)) @@ -170,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 4b8f349962..84e3a16ae7 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(); @@ -47,6 +48,8 @@ internal abstract class DeclarationsProvider public abstract string OverheadImplementation { get; } + public virtual string OverheadDefaultValueHolderDeclaration => null; + private string GetMethodName(MethodInfo method) { if (method == null) @@ -69,7 +72,7 @@ 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"; @@ -78,7 +81,12 @@ internal class VoidDeclarationsProvider : DeclarationsProvider internal class NonVoidDeclarationsProvider : DeclarationsProvider { - public NonVoidDeclarationsProvider(Descriptor descriptor) : base(descriptor) { } + private readonly bool overheadReturnsDefault; + + public NonVoidDeclarationsProvider(BenchmarkCase benchmark) : base(benchmark) + { + overheadReturnsDefault = WorkloadMethodReturnType.IsByRefLike() || WorkloadMethodReturnType.IsDefaultFasterThanField(Benchmark.GetRuntime().RuntimeMoniker == Jobs.RuntimeMoniker.Mono); + } public override string ConsumeField => !Consumer.IsConsumable(WorkloadMethodReturnType) && Consumer.HasConsumableField(WorkloadMethodReturnType, out var field) @@ -86,7 +94,22 @@ public override string ConsumeField : null; public override string OverheadImplementation - => $"return default({WorkloadMethodReturnType.GetCorrectCSharpTypeName()});"; + => overheadReturnsDefault + ? $"return default({WorkloadMethodReturnType.GetCorrectCSharpTypeName()});" + : "return overheadDefaultValueHolder;"; + + public override string OverheadDefaultValueHolderDeclaration + { + get + { + if (overheadReturnsDefault) + { + return null; + } + string typeName = WorkloadMethodReturnType.GetCorrectCSharpTypeName(); + return $"private {typeName} overheadDefaultValueHolder = default({typeName});"; + } + } public override string ReturnsDefinition => Consumer.IsConsumable(WorkloadMethodReturnType) || Consumer.HasConsumableField(WorkloadMethodReturnType, out _) @@ -96,7 +119,7 @@ public override string ReturnsDefinition internal class ByRefDeclarationsProvider : NonVoidDeclarationsProvider { - public ByRefDeclarationsProvider(Descriptor descriptor) : base(descriptor) { } + public ByRefDeclarationsProvider(BenchmarkCase benchmark) : base(benchmark) { } public override string WorkloadMethodReturnTypeName => base.WorkloadMethodReturnTypeName.Replace("&", string.Empty); @@ -104,6 +127,15 @@ internal class ByRefDeclarationsProvider : NonVoidDeclarationsProvider 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"; public override string WorkloadMethodReturnTypeModifiers => "ref"; @@ -111,14 +143,14 @@ internal class ByRefDeclarationsProvider : NonVoidDeclarationsProvider internal class ByReadOnlyRefDeclarationsProvider : ByRefDeclarationsProvider { - public ByReadOnlyRefDeclarationsProvider(Descriptor descriptor) : base(descriptor) { } + 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 @@ -135,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/Extensions/ReflectionExtensions.cs b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs index be926645cf..af2838d033 100644 --- a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs @@ -211,5 +211,43 @@ private static bool IsRunnableGenericType(TypeInfo typeInfo) 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. + // Classic Mono runs `default` slower than reading a field for very large structs. `default` is faster for all types in all other runtimes. + internal static bool IsDefaultFasterThanField(this Type type, bool isClassicMono) + => !isClassicMono || type.SizeOfDefault() <= MonoDefaultCutoffSize; + + private static int SizeOfDefault(this Type type) => type switch + { + _ when type == typeof(byte) || type == typeof(sbyte) + => 1, + + _ when type == typeof(short) || type == typeof(ushort) || type == typeof(char) + => 2, + + _ when type == typeof(int) || type == typeof(uint) + => 4, + + _ when type == typeof(long) || type == typeof(ulong) + => 8, + + _ when type.IsPointer || type.IsClass || type.IsInterface || type == typeof(IntPtr) || type == typeof(UIntPtr) + => IntPtr.Size, + + _ when type.IsEnum + => Enum.GetUnderlyingType(type).SizeOfDefault(), + + // Note: the runtime pads structs for alignment purposes, and it enforces a minimum of 1 byte, even for empty structs, + // but we don't need to worry about either of those cases for the purpose this serves (calculating whether to use `default` or read a field in Mono for the overhead method). + _ when type.IsValueType + => type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Aggregate(0, (count, field) => field.FieldType.SizeOfDefault() + count), + + _ => throw new Exception("Unknown type size: " + type.FullName) + }; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Templates/BenchmarkType.txt b/src/BenchmarkDotNet/Templates/BenchmarkType.txt index d67596479d..6b03042851 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkType.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkType.txt @@ -71,6 +71,7 @@ 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 @@ -248,7 +249,6 @@ #elif RETURNS_BYREF_$ID$ - private $WorkloadMethodReturnType$ overheadDefaultValueHolder = default($WorkloadMethodReturnType$); // Do NOT change name "overheadDefaultValueHolder" (used in ByRefDeclarationsProvider). private BenchmarkDotNet.Engines.Consumer consumer = new BenchmarkDotNet.Engines.Consumer(); #if NETCOREAPP3_0_OR_GREATER diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ByRefConsumeEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ByRefConsumeEmitter.cs index 17ed8d126d..a999ab40ac 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ByRefConsumeEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ByRefConsumeEmitter.cs @@ -1,4 +1,4 @@ -using BenchmarkDotNet.Helpers.Reflection.Emit; +using BenchmarkDotNet.Engines; using System; using System.Reflection; using System.Reflection.Emit; @@ -8,14 +8,10 @@ namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation { internal class ByRefConsumeEmitter : ConsumableConsumeEmitter { - private FieldBuilder overheadDefaultValueHolderField; - public ByRefConsumeEmitter(ConsumableTypeInfo consumableTypeInfo) : base(consumableTypeInfo) { } protected override void OnDefineFieldsOverride(TypeBuilder runnableBuilder) { - base.OnDefineFieldsOverride(runnableBuilder); - var nonRefType = ConsumableInfo.WorkloadMethodReturnType.GetElementType(); if (nonRefType == null) throw new InvalidOperationException($"Bug: type {ConsumableInfo.WorkloadMethodReturnType} is non-ref type."); @@ -23,6 +19,8 @@ protected override void OnDefineFieldsOverride(TypeBuilder runnableBuilder) overheadDefaultValueHolderField = runnableBuilder.DefineField( OverheadDefaultValueHolderFieldName, nonRefType, FieldAttributes.Private); + + consumerField = runnableBuilder.DefineField(ConsumerFieldName, typeof(Consumer), FieldAttributes.Private); } protected override void EmitDisassemblyDiagnoserReturnDefaultOverride(ILGenerator ilBuilder) diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumableConsumeEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumableConsumeEmitter.cs index 790ca54482..d8c1b88600 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumableConsumeEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumableConsumeEmitter.cs @@ -2,25 +2,35 @@ using System.Linq; using System.Reflection; using System.Reflection.Emit; -using System.Runtime.CompilerServices; 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 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.IsByRefLike() || 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); } @@ -78,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) { /*