Skip to content

Overhead match workload #2309

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
20 changes: 10 additions & 10 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 @@ -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))
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down
71 changes: 37 additions & 34 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 @@ -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)
Expand All @@ -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});";
}
}

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

Expand All @@ -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
Expand All @@ -164,7 +167,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
46 changes: 7 additions & 39 deletions src/BenchmarkDotNet/Engines/Consumer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,14 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using BenchmarkDotNet.Extensions;
using JetBrains.Annotations;

// ReSharper disable NotAccessedField.Local
namespace BenchmarkDotNet.Engines
{
public class Consumer
{
private static readonly HashSet<Type> SupportedTypes
= new HashSet<Type>(
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;
Expand Down Expand Up @@ -123,39 +117,13 @@ public void Consume<T>(T objectValue) where T : class // class constraint preven

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Consume<T>(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<T, byte>(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)
{
Expand Down
37 changes: 34 additions & 3 deletions src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down
Loading