Skip to content

Commit

Permalink
Overhead reads from field if the return type is large struct and runt…
Browse files Browse the repository at this point in the history
…ime is old Mono.
  • Loading branch information
timcassell committed Jun 10, 2023
1 parent c4f5f0d commit 54c0b54
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 26 deletions.
19 changes: 10 additions & 9 deletions src/BenchmarkDotNet/Code/CodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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))
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down
48 changes: 39 additions & 9 deletions src/BenchmarkDotNet/Code/DeclarationsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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)
Expand All @@ -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";

Expand All @@ -78,15 +81,33 @@ internal class VoidDeclarationsProvider : DeclarationsProvider

internal class NonVoidDeclarationsProvider : DeclarationsProvider
{
public NonVoidDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
public NonVoidDeclarationsProvider(BenchmarkCase benchmark) : base(benchmark) { }

public override string ConsumeField
=> !Consumer.IsConsumable(WorkloadMethodReturnType) && Consumer.HasConsumableField(WorkloadMethodReturnType, out var field)
? $".{field.Name}"
: null;

private bool OverheadReturnsDefault
=> WorkloadMethodReturnType.IsByRefLike() || WorkloadMethodReturnType.IsDefaultFasterThanField(Benchmark.GetRuntime().RuntimeMoniker == Jobs.RuntimeMoniker.Mono);

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 _)
Expand All @@ -96,29 +117,38 @@ 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);

public override string ConsumeField => null;

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";
}

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
Expand All @@ -135,7 +165,7 @@ public override string WorkloadMethodDelegate(string passArguments)
/// </summary>
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();

Expand Down
38 changes: 38 additions & 0 deletions src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 128 bytes was observed to be the point at which `default` becomes slower in classic Mono, from benchmarks.
// For all types > 128 bytes, reading from a field is faster than `default`.
// Between 64 and 128 bytes, both methods are about the same speed.
private const int MonoDefaultCutoffSize = 128;

// 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)
};
}
}
2 changes: 1 addition & 1 deletion src/BenchmarkDotNet/Templates/BenchmarkType.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using BenchmarkDotNet.Helpers.Reflection.Emit;
using BenchmarkDotNet.Engines;
using System;
using System.Reflection;
using System.Reflection.Emit;
Expand All @@ -8,21 +8,19 @@ 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.");

overheadDefaultValueHolderField = runnableBuilder.DefineField(
OverheadDefaultValueHolderFieldName,
nonRefType, FieldAttributes.Private);

consumerField = runnableBuilder.DefineField(ConsumerFieldName, typeof(Consumer), FieldAttributes.Private);
}

protected override void EmitDisassemblyDiagnoserReturnDefaultOverride(ILGenerator ilBuilder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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)
{
/*
Expand Down

0 comments on commit 54c0b54

Please sign in to comment.