Skip to content

Commit 8403c15

Browse files
committed
Overhead reads from field if the return type is large struct and runtime is old Mono.
1 parent c4f5f0d commit 8403c15

File tree

6 files changed

+131
-26
lines changed

6 files changed

+131
-26
lines changed

src/BenchmarkDotNet/Code/CodeGenerator.cs

+10-9
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ internal static string Generate(BuildPartition buildPartition)
3333
{
3434
var benchmark = buildInfo.BenchmarkCase;
3535

36-
var provider = GetDeclarationsProvider(benchmark.Descriptor);
36+
var provider = GetDeclarationsProvider(benchmark);
3737

3838
string passArguments = GetPassArguments(benchmark);
3939

@@ -53,6 +53,7 @@ internal static string Generate(BuildPartition buildPartition)
5353
.Replace("$IterationSetupMethodName$", provider.IterationSetupMethodName)
5454
.Replace("$IterationCleanupMethodName$", provider.IterationCleanupMethodName)
5555
.Replace("$OverheadImplementation$", provider.OverheadImplementation)
56+
.Replace("$OverheadDefaultValueHolderField$", provider.OverheadDefaultValueHolderDeclaration)
5657
.Replace("$ConsumeField$", provider.ConsumeField)
5758
.Replace("$JobSetDefinition$", GetJobsSetDefinition(benchmark))
5859
.Replace("$ParamsContent$", GetParamsContent(benchmark))
@@ -147,19 +148,19 @@ private static string GetJobsSetDefinition(BenchmarkCase benchmarkCase)
147148
Replace("; ", ";\n ");
148149
}
149150

150-
private static DeclarationsProvider GetDeclarationsProvider(Descriptor descriptor)
151+
private static DeclarationsProvider GetDeclarationsProvider(BenchmarkCase benchmark)
151152
{
152-
var method = descriptor.WorkloadMethod;
153+
var method = benchmark.Descriptor.WorkloadMethod;
153154

154155
if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(ValueTask))
155156
{
156-
return new TaskDeclarationsProvider(descriptor);
157+
return new TaskDeclarationsProvider(benchmark);
157158
}
158159
if (method.ReturnType.GetTypeInfo().IsGenericType
159160
&& (method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>)
160161
|| method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>)))
161162
{
162-
return new GenericTaskDeclarationsProvider(descriptor);
163+
return new GenericTaskDeclarationsProvider(benchmark);
163164
}
164165

165166
if (method.ReturnType == typeof(void))
@@ -170,19 +171,19 @@ private static DeclarationsProvider GetDeclarationsProvider(Descriptor descripto
170171
throw new NotSupportedException("async void is not supported by design");
171172
}
172173

173-
return new VoidDeclarationsProvider(descriptor);
174+
return new VoidDeclarationsProvider(benchmark);
174175
}
175176

176177
if (method.ReturnType.IsByRef)
177178
{
178179
// System.Runtime.CompilerServices.IsReadOnlyAttribute is part of .NET Standard 2.1, we can't use it here..
179180
if (method.ReturnParameter.GetCustomAttributes().Any(attribute => attribute.GetType().Name == "IsReadOnlyAttribute"))
180-
return new ByReadOnlyRefDeclarationsProvider(descriptor);
181+
return new ByReadOnlyRefDeclarationsProvider(benchmark);
181182
else
182-
return new ByRefDeclarationsProvider(descriptor);
183+
return new ByRefDeclarationsProvider(benchmark);
183184
}
184185

185-
return new NonVoidDeclarationsProvider(descriptor);
186+
return new NonVoidDeclarationsProvider(benchmark);
186187
}
187188

188189
// internal for tests

src/BenchmarkDotNet/Code/DeclarationsProvider.cs

+41-9
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ internal abstract class DeclarationsProvider
1414
// "GlobalSetup" or "GlobalCleanup" methods are optional, so default to an empty delegate, so there is always something that can be invoked
1515
private const string EmptyAction = "() => { }";
1616

17-
protected readonly Descriptor Descriptor;
17+
protected readonly BenchmarkCase Benchmark;
18+
protected Descriptor Descriptor => Benchmark.Descriptor;
1819

19-
internal DeclarationsProvider(Descriptor descriptor) => Descriptor = descriptor;
20+
internal DeclarationsProvider(BenchmarkCase benchmark) => Benchmark = benchmark;
2021

2122
public string OperationsPerInvoke => Descriptor.OperationsPerInvoke.ToString();
2223

@@ -47,6 +48,8 @@ internal abstract class DeclarationsProvider
4748

4849
public abstract string OverheadImplementation { get; }
4950

51+
public virtual string OverheadDefaultValueHolderDeclaration => null;
52+
5053
private string GetMethodName(MethodInfo method)
5154
{
5255
if (method == null)
@@ -69,7 +72,7 @@ private string GetMethodName(MethodInfo method)
6972

7073
internal class VoidDeclarationsProvider : DeclarationsProvider
7174
{
72-
public VoidDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
75+
public VoidDeclarationsProvider(BenchmarkCase benchmark) : base(benchmark) { }
7376

7477
public override string ReturnsDefinition => "RETURNS_VOID";
7578

@@ -78,15 +81,35 @@ public VoidDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
7881

7982
internal class NonVoidDeclarationsProvider : DeclarationsProvider
8083
{
81-
public NonVoidDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
84+
private readonly bool overheadReturnsDefault;
85+
86+
public NonVoidDeclarationsProvider(BenchmarkCase benchmark) : base(benchmark)
87+
{
88+
overheadReturnsDefault = WorkloadMethodReturnType.IsByRefLike() || WorkloadMethodReturnType.IsDefaultFasterThanField(Benchmark.GetRuntime().RuntimeMoniker == Jobs.RuntimeMoniker.Mono);
89+
}
8290

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

8896
public override string OverheadImplementation
89-
=> $"return default({WorkloadMethodReturnType.GetCorrectCSharpTypeName()});";
97+
=> overheadReturnsDefault
98+
? $"return default({WorkloadMethodReturnType.GetCorrectCSharpTypeName()});"
99+
: "return overheadDefaultValueHolder;";
100+
101+
public override string OverheadDefaultValueHolderDeclaration
102+
{
103+
get
104+
{
105+
if (overheadReturnsDefault)
106+
{
107+
return null;
108+
}
109+
string typeName = WorkloadMethodReturnType.GetCorrectCSharpTypeName();
110+
return $"private {typeName} overheadDefaultValueHolder = default({typeName});";
111+
}
112+
}
90113

91114
public override string ReturnsDefinition
92115
=> Consumer.IsConsumable(WorkloadMethodReturnType) || Consumer.HasConsumableField(WorkloadMethodReturnType, out _)
@@ -96,29 +119,38 @@ public override string ReturnsDefinition
96119

97120
internal class ByRefDeclarationsProvider : NonVoidDeclarationsProvider
98121
{
99-
public ByRefDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
122+
public ByRefDeclarationsProvider(BenchmarkCase benchmark) : base(benchmark) { }
100123

101124
public override string WorkloadMethodReturnTypeName => base.WorkloadMethodReturnTypeName.Replace("&", string.Empty);
102125

103126
public override string ConsumeField => null;
104127

105128
public override string OverheadImplementation => $"return ref overheadDefaultValueHolder;";
106129

130+
public override string OverheadDefaultValueHolderDeclaration
131+
{
132+
get
133+
{
134+
string typeName = WorkloadMethodReturnType.GetCorrectCSharpTypeName();
135+
return $"private {typeName} overheadDefaultValueHolder = default({typeName});";
136+
}
137+
}
138+
107139
public override string ReturnsDefinition => "RETURNS_BYREF";
108140

109141
public override string WorkloadMethodReturnTypeModifiers => "ref";
110142
}
111143

112144
internal class ByReadOnlyRefDeclarationsProvider : ByRefDeclarationsProvider
113145
{
114-
public ByReadOnlyRefDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
146+
public ByReadOnlyRefDeclarationsProvider(BenchmarkCase benchmark) : base(benchmark) { }
115147

116148
public override string WorkloadMethodReturnTypeModifiers => "ref readonly";
117149
}
118150

119151
internal class TaskDeclarationsProvider : VoidDeclarationsProvider
120152
{
121-
public TaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
153+
public TaskDeclarationsProvider(BenchmarkCase benchmark) : base(benchmark) { }
122154

123155
// we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way,
124156
// and will eventually throw actual exception, not aggregated one
@@ -135,7 +167,7 @@ public override string WorkloadMethodDelegate(string passArguments)
135167
/// </summary>
136168
internal class GenericTaskDeclarationsProvider : NonVoidDeclarationsProvider
137169
{
138-
public GenericTaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
170+
public GenericTaskDeclarationsProvider(BenchmarkCase benchmark) : base(benchmark) { }
139171

140172
protected override Type WorkloadMethodReturnType => Descriptor.WorkloadMethod.ReturnType.GetTypeInfo().GetGenericArguments().Single();
141173

src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs

+38
Original file line numberDiff line numberDiff line change
@@ -211,5 +211,43 @@ private static bool IsRunnableGenericType(TypeInfo typeInfo)
211211
internal static bool IsByRefLike(this Type type)
212212
// Type.IsByRefLike is not available in netstandard2.0.
213213
=> type.IsValueType && type.CustomAttributes.Any(attr => attr.AttributeType.FullName == "System.Runtime.CompilerServices.IsByRefLikeAttribute");
214+
215+
// Struct size of 128 bytes was observed to be the point at which `default` becomes slower in classic Mono, from benchmarks.
216+
// For all types > 128 bytes, reading from a field is faster than `default`.
217+
// Between 64 and 128 bytes, both methods are about the same speed.
218+
private const int MonoDefaultCutoffSize = 128;
219+
220+
// 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.
221+
// Classic Mono runs `default` slower than reading a field for very large structs. `default` is faster for all types in all other runtimes.
222+
internal static bool IsDefaultFasterThanField(this Type type, bool isClassicMono)
223+
=> !isClassicMono || type.SizeOfDefault() <= MonoDefaultCutoffSize;
224+
225+
private static int SizeOfDefault(this Type type) => type switch
226+
{
227+
_ when type == typeof(byte) || type == typeof(sbyte)
228+
=> 1,
229+
230+
_ when type == typeof(short) || type == typeof(ushort) || type == typeof(char)
231+
=> 2,
232+
233+
_ when type == typeof(int) || type == typeof(uint)
234+
=> 4,
235+
236+
_ when type == typeof(long) || type == typeof(ulong)
237+
=> 8,
238+
239+
_ when type.IsPointer || type.IsClass || type.IsInterface || type == typeof(IntPtr) || type == typeof(UIntPtr)
240+
=> IntPtr.Size,
241+
242+
_ when type.IsEnum
243+
=> Enum.GetUnderlyingType(type).SizeOfDefault(),
244+
245+
// Note: the runtime pads structs for alignment purposes, and it enforces a minimum of 1 byte, even for empty structs,
246+
// 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).
247+
_ when type.IsValueType
248+
=> type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Aggregate(0, (count, field) => field.FieldType.SizeOfDefault() + count),
249+
250+
_ => throw new Exception("Unknown type size: " + type.FullName)
251+
};
214252
}
215253
}

src/BenchmarkDotNet/Templates/BenchmarkType.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
private BenchmarkDotNet.Autogenerated.Runnable_$ID$.WorkloadDelegate overheadDelegate;
7272
private BenchmarkDotNet.Autogenerated.Runnable_$ID$.WorkloadDelegate workloadDelegate;
7373
$DeclareArgumentFields$
74+
$OverheadDefaultValueHolderField$
7475

7576
// this method is used only for the disassembly diagnoser purposes
7677
// the goal is to get this and the benchmarked method jitted, but without executing the benchmarked method itself
@@ -248,7 +249,6 @@
248249

249250
#elif RETURNS_BYREF_$ID$
250251

251-
private $WorkloadMethodReturnType$ overheadDefaultValueHolder = default($WorkloadMethodReturnType$); // Do NOT change name "overheadDefaultValueHolder" (used in ByRefDeclarationsProvider).
252252
private BenchmarkDotNet.Engines.Consumer consumer = new BenchmarkDotNet.Engines.Consumer();
253253

254254
#if NETCOREAPP3_0_OR_GREATER

src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ByRefConsumeEmitter.cs

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using BenchmarkDotNet.Helpers.Reflection.Emit;
1+
using BenchmarkDotNet.Engines;
22
using System;
33
using System.Reflection;
44
using System.Reflection.Emit;
@@ -8,21 +8,19 @@ namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation
88
{
99
internal class ByRefConsumeEmitter : ConsumableConsumeEmitter
1010
{
11-
private FieldBuilder overheadDefaultValueHolderField;
12-
1311
public ByRefConsumeEmitter(ConsumableTypeInfo consumableTypeInfo) : base(consumableTypeInfo) { }
1412

1513
protected override void OnDefineFieldsOverride(TypeBuilder runnableBuilder)
1614
{
17-
base.OnDefineFieldsOverride(runnableBuilder);
18-
1915
var nonRefType = ConsumableInfo.WorkloadMethodReturnType.GetElementType();
2016
if (nonRefType == null)
2117
throw new InvalidOperationException($"Bug: type {ConsumableInfo.WorkloadMethodReturnType} is non-ref type.");
2218

2319
overheadDefaultValueHolderField = runnableBuilder.DefineField(
2420
OverheadDefaultValueHolderFieldName,
2521
nonRefType, FieldAttributes.Private);
22+
23+
consumerField = runnableBuilder.DefineField(ConsumerFieldName, typeof(Consumer), FieldAttributes.Private);
2624
}
2725

2826
protected override void EmitDisassemblyDiagnoserReturnDefaultOverride(ILGenerator ilBuilder)

src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumableConsumeEmitter.cs

+38-2
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,35 @@
22
using System.Linq;
33
using System.Reflection;
44
using System.Reflection.Emit;
5-
using System.Runtime.CompilerServices;
65
using BenchmarkDotNet.Engines;
76
using BenchmarkDotNet.Extensions;
87
using BenchmarkDotNet.Helpers.Reflection.Emit;
8+
using BenchmarkDotNet.Portability;
99

1010
namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation
1111
{
1212
internal class ConsumableConsumeEmitter : ConsumeEmitter
1313
{
14-
private FieldBuilder consumerField;
14+
protected FieldBuilder overheadDefaultValueHolderField;
15+
protected FieldBuilder consumerField;
1516
private LocalBuilder disassemblyDiagnoserLocal;
1617
private LocalBuilder resultLocal;
18+
private readonly bool overheadReturnsDefault;
1719

1820
public ConsumableConsumeEmitter(ConsumableTypeInfo consumableTypeInfo) : base(consumableTypeInfo)
1921
{
22+
overheadReturnsDefault = consumableTypeInfo.WorkloadMethodReturnType.IsByRefLike() || consumableTypeInfo.WorkloadMethodReturnType.IsDefaultFasterThanField(RuntimeInformation.IsOldMono);
2023
}
2124

2225
protected override void OnDefineFieldsOverride(TypeBuilder runnableBuilder)
2326
{
27+
if (!overheadReturnsDefault)
28+
{
29+
overheadDefaultValueHolderField = runnableBuilder.DefineField(
30+
RunnableConstants.OverheadDefaultValueHolderFieldName,
31+
ConsumableInfo.WorkloadMethodReturnType, FieldAttributes.Private);
32+
}
33+
2434
consumerField = runnableBuilder.DefineField(RunnableConstants.ConsumerFieldName, typeof(Consumer), FieldAttributes.Private);
2535
}
2636

@@ -78,6 +88,32 @@ protected override void OnEmitCtorBodyOverride(ConstructorBuilder constructorBui
7888
ilBuilder.Emit(OpCodes.Stfld, consumerField);
7989
}
8090

91+
public override void EmitOverheadImplementation(ILGenerator ilBuilder, Type returnType)
92+
{
93+
if (overheadReturnsDefault)
94+
{
95+
/*
96+
// return default;
97+
IL_0000: ldc.i4.0
98+
IL_0001: ret
99+
*/
100+
// optional local if default(T) uses .initobj
101+
var optionalLocalForInitobj = ilBuilder.DeclareOptionalLocalForReturnDefault(returnType);
102+
ilBuilder.EmitReturnDefault(returnType, optionalLocalForInitobj);
103+
return;
104+
}
105+
106+
/*
107+
// return overheadDefaultValueHolder;
108+
IL_0000: ldarg.0
109+
IL_0001: ldfld int32 C::'field'
110+
IL_0006: ret
111+
*/
112+
ilBuilder.Emit(OpCodes.Ldarg_0);
113+
ilBuilder.Emit(OpCodes.Ldfld, overheadDefaultValueHolderField);
114+
ilBuilder.Emit(OpCodes.Ret);
115+
}
116+
81117
protected override void EmitActionBeforeCallOverride(ILGenerator ilBuilder)
82118
{
83119
/*

0 commit comments

Comments
 (0)