Skip to content
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