Skip to content

Commit 157c620

Browse files
committed
Async refactor WIP
1 parent cb5d392 commit 157c620

File tree

51 files changed

+1815
-908
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1815
-908
lines changed

BenchmarkDotNet.slnx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<Folder Name="/samples/">
77
<File Path="samples/Directory.Build.props" />
88
<Project Path="samples/BenchmarkDotNet.Samples.FSharp/BenchmarkDotNet.Samples.FSharp.fsproj" />
9-
<Project Path="samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj" DefaultStartup="true" />
9+
<Project Path="samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj" />
1010
</Folder>
1111
<Folder Name="/src/">
1212
<Project Path="src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj" />

build/common.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060

6161
<PropertyGroup>
6262
<!-- Increment this when the BenchmarkDotNet.Weaver package needs to be re-packed. -->
63-
<WeaverVersionSuffix>-1</WeaverVersionSuffix>
63+
<WeaverVersionSuffix>-2</WeaverVersionSuffix>
6464
</PropertyGroup>
6565

6666
<ItemGroup>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
3+
namespace BenchmarkDotNet.Attributes;
4+
5+
/// <summary>
6+
/// When applied to an async benchmark method, overrides the return type of the async method that calls the benchmark method.
7+
/// </summary>
8+
[AttributeUsage(AttributeTargets.Method)]
9+
public sealed class AsyncCallerTypeAttribute(Type asyncCallerType) : Attribute
10+
{
11+
/// <summary>
12+
/// The return type of the async method that calls the benchmark method.
13+
/// </summary>
14+
public Type AsyncCallerType { get; private set; } = asyncCallerType;
15+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
using System.Runtime.CompilerServices;
3+
4+
namespace BenchmarkDotNet.Attributes.CompilerServices;
5+
6+
// MethodImplOptions.AggressiveOptimization is applied to all methods to force them to go straight to tier1 JIT,
7+
// eliminating tiered JIT as a potential variable in measurements.
8+
// This is necessary because C# does not support any way to apply attributes to compiler-generated state machine methods.
9+
// This is applied both to the core Engine and auto-generated classes.
10+
#pragma warning disable CS1574
11+
/// <summary>
12+
/// Instructs the BenchmarkDotNet assembly weaver to apply <see cref="MethodImplOptions.AggressiveOptimization"/> to all declared
13+
/// methods in the annotated type and nested types that are not already annotated with <see cref="MethodImplOptions.NoOptimization"/>.
14+
/// </summary>
15+
#pragma warning restore CS1574
16+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
17+
public sealed class AggressivelyOptimizeMethodsAttribute : Attribute
18+
{
19+
}

src/BenchmarkDotNet.Weaver/buildTransitive/netstandard2.0/BenchmarkDotNet.Weaver.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
Inputs="$(BenchmarkDotNetWeaveAssemblyPath)"
1818
Outputs="$(BenchmarkDotNetWeaveAssembliesStampFile)">
1919

20-
<WeaveAssemblyTask TargetAssembly="$(BenchmarkDotNetWeaveAssemblyPath)" />
20+
<WeaveAssemblyTask TargetAssembly="$(BenchmarkDotNetWeaveAssemblyPath)" TreatWarningsAsErrors="$(TreatWarningsAsErrors)" />
2121

2222
<!-- Create stamp file for incrementality -->
2323
<Touch Files="$(BenchmarkDotNetWeaveAssembliesStampFile)" AlwaysCreate="true" />
Binary file not shown.
Binary file not shown.

src/BenchmarkDotNet.Weaver/src/WeaveAssemblyTask.cs

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,45 +24,75 @@ public sealed class WeaveAssemblyTask : Task
2424
[Required]
2525
public string TargetAssembly { get; set; }
2626

27+
/// <summary>
28+
/// Whether to treat warnings as errors.
29+
/// </summary>
30+
public bool TreatWarningsAsErrors { get; set; }
31+
2732
/// <summary>
2833
/// Runs the weave assembly task.
2934
/// </summary>
3035
/// <returns><see langword="true"/> if successful; <see langword="false"/> otherwise.</returns>
3136
public override bool Execute()
32-
{
37+
{
3338
if (!File.Exists(TargetAssembly))
3439
{
3540
Log.LogError($"Assembly not found: {TargetAssembly}");
3641
return false;
3742
}
3843

39-
4044
bool benchmarkMethodsImplAdjusted = false;
4145
try
4246
{
4347
var module = ModuleDefinition.FromFile(TargetAssembly);
4448

49+
bool anyAdjustments = false;
4550
foreach (var type in module.GetAllTypes())
4651
{
47-
// We can skip non-public types as they are not valid for benchmarks.
48-
if (type.IsNotPublic)
52+
if (type.CustomAttributes.Any(attr => attr.Constructor.DeclaringType.FullName == "BenchmarkDotNet.Attributes.CompilerServices.AggressivelyOptimizeMethodsAttribute"))
4953
{
50-
continue;
54+
ApplyAggressiveOptimizationToMethods(type);
55+
56+
void ApplyAggressiveOptimizationToMethods(TypeDefinition type)
57+
{
58+
// Apply AggressiveOptimization to all methods in the type and nested types that
59+
// aren't annotated with NoOptimization (this includes compiler-generated state machines).
60+
foreach (var method in type.Methods)
61+
{
62+
if ((method.ImplAttributes & MethodImplAttributes.NoOptimization) == 0)
63+
{
64+
var oldImpl = method.ImplAttributes;
65+
method.ImplAttributes |= MethodImplAttributes.AggressiveOptimization;
66+
anyAdjustments |= (oldImpl & MethodImplAttributes.AggressiveOptimization) == 0;
67+
}
68+
}
69+
70+
// Recurse into nested types
71+
foreach (var nested in type.NestedTypes)
72+
{
73+
ApplyAggressiveOptimizationToMethods(nested);
74+
}
75+
}
5176
}
5277

53-
foreach (var method in type.Methods)
78+
// We can skip non-public types as they are not valid for benchmarks.
79+
if (type.IsPublic)
5480
{
55-
if (method.CustomAttributes.Any(IsBenchmarkAttribute))
81+
foreach (var method in type.Methods)
5682
{
57-
var oldImpl = method.ImplAttributes;
58-
// Remove AggressiveInlining and add NoInlining.
59-
method.ImplAttributes = (oldImpl & ~MethodImplAttributes.AggressiveInlining) | MethodImplAttributes.NoInlining;
60-
benchmarkMethodsImplAdjusted |= (oldImpl & MethodImplAttributes.NoInlining) == 0;
83+
if (method.CustomAttributes.Any(IsBenchmarkAttribute))
84+
{
85+
var oldImpl = method.ImplAttributes;
86+
// Remove AggressiveInlining and add NoInlining.
87+
method.ImplAttributes = (oldImpl & ~MethodImplAttributes.AggressiveInlining) | MethodImplAttributes.NoInlining;
88+
benchmarkMethodsImplAdjusted |= (oldImpl & MethodImplAttributes.NoInlining) == 0;
89+
anyAdjustments |= benchmarkMethodsImplAdjusted;
90+
}
6191
}
6292
}
6393
}
6494

65-
if (benchmarkMethodsImplAdjusted)
95+
if (anyAdjustments)
6696
{
6797
// Write to a memory stream before overwriting the original file in case an exception occurs during the write (like unsupported platform).
6898
// https://github.com/Washi1337/AsmResolver/issues/640
@@ -90,9 +120,17 @@ public override bool Execute()
90120
}
91121
catch (Exception e)
92122
{
93-
Log.LogWarning($"Assembly weaving failed. Benchmark methods found requiring NoInlining: {benchmarkMethodsImplAdjusted}. Error:{Environment.NewLine}{e}");
123+
if (TreatWarningsAsErrors)
124+
{
125+
Log.LogError($"Assembly weaving failed. Benchmark methods found requiring NoInlining: {benchmarkMethodsImplAdjusted}.");
126+
Log.LogErrorFromException(e, true, true, null);
127+
}
128+
else
129+
{
130+
Log.LogWarning($"Assembly weaving failed. Benchmark methods found requiring NoInlining: {benchmarkMethodsImplAdjusted}. Error:{Environment.NewLine}{e}");
131+
}
94132
}
95-
return true;
133+
return !Log.HasLoggedErrors;
96134
}
97135

98136
private static bool IsBenchmarkAttribute(CustomAttribute attribute)

src/BenchmarkDotNet/BenchmarkDotNet.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<Import Project="..\..\build\common.props" />
3+
34
<PropertyGroup>
45
<AssemblyTitle>BenchmarkDotNet</AssemblyTitle>
56
<TargetFrameworks>netstandard2.0;net6.0;net8.0;net9.0;net10.0</TargetFrameworks>
@@ -49,5 +50,10 @@
4950
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
5051
</PackageReference>
5152
</ItemGroup>
53+
<!-- The transitive weaver reference is stripped during full pack, so we need to reference it directly. -->
54+
<ItemGroup Condition="'$(IsFullPack)' == 'true'">
55+
<PackageReference Include="BenchmarkDotNet.Weaver" Version="$(Version)$(WeaverVersionSuffix)" PrivateAssets="all" />
56+
</ItemGroup>
57+
5258
<Import Project="..\..\build\common.targets" />
5359
</Project>

src/BenchmarkDotNet/Code/CodeGenerator.cs

Lines changed: 24 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,17 @@ internal static string Generate(BuildPartition buildPartition)
3131
{
3232
var benchmark = buildInfo.BenchmarkCase;
3333

34-
var provider = GetDeclarationsProvider(benchmark.Descriptor);
35-
36-
string passArguments = GetPassArguments(benchmark);
37-
38-
string benchmarkTypeCode = new SmartStringBuilder(ResourceHelper.LoadTemplate("BenchmarkType.txt"))
34+
string benchmarkTypeCode = GetDeclarationsProvider(benchmark)
35+
.ReplaceTemplate(new SmartStringBuilder(ResourceHelper.LoadTemplate("BenchmarkType.txt")))
3936
.Replace("$ID$", buildInfo.Id.ToString())
40-
.Replace("$OperationsPerInvoke$", provider.OperationsPerInvoke)
41-
.Replace("$WorkloadTypeName$", provider.WorkloadTypeName)
42-
.Replace("$GlobalSetupMethodName$", provider.GlobalSetupMethodName)
43-
.Replace("$GlobalCleanupMethodName$", provider.GlobalCleanupMethodName)
44-
.Replace("$IterationSetupMethodName$", provider.IterationSetupMethodName)
45-
.Replace("$IterationCleanupMethodName$", provider.IterationCleanupMethodName)
4637
.Replace("$JobSetDefinition$", GetJobsSetDefinition(benchmark))
4738
.Replace("$ParamsContent$", GetParamsContent(benchmark))
4839
.Replace("$ArgumentsDefinition$", GetArgumentsDefinition(benchmark))
4940
.Replace("$DeclareArgumentFields$", GetDeclareArgumentFields(benchmark))
5041
.Replace("$InitializeArgumentFields$", GetInitializeArgumentFields(benchmark))
51-
.Replace("$LoadArguments$", GetLoadArguments(benchmark))
52-
.Replace("$PassArguments$", passArguments)
5342
.Replace("$EngineFactoryType$", GetEngineFactoryTypeName(benchmark))
5443
.Replace("$RunExtraIteration$", buildInfo.Config.HasExtraIterationDiagnoser(benchmark) ? "true" : "false")
5544
.Replace("$DisassemblerEntryMethodName$", DisassemblerConstants.DisassemblerEntryMethodName)
56-
.Replace("$WorkloadMethodCall$", provider.GetWorkloadMethodCall(passArguments))
5745
.Replace("$InProcessDiagnoserRouters$", GetInProcessDiagnoserRouters(buildInfo))
5846
.ToString();
5947

@@ -122,27 +110,21 @@ private static string GetJobsSetDefinition(BenchmarkCase benchmarkCase)
122110
Replace("; ", ";\n ");
123111
}
124112

125-
private static DeclarationsProvider GetDeclarationsProvider(Descriptor descriptor)
113+
private static DeclarationsProvider GetDeclarationsProvider(BenchmarkCase benchmark)
126114
{
127-
var method = descriptor.WorkloadMethod;
115+
var method = benchmark.Descriptor.WorkloadMethod;
128116

129-
if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(ValueTask))
130-
{
131-
return new AsyncDeclarationsProvider(descriptor);
132-
}
133-
if (method.ReturnType.GetTypeInfo().IsGenericType
134-
&& (method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>)
135-
|| method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>)))
117+
if (method.ReturnType.IsAwaitable())
136118
{
137-
return new AsyncDeclarationsProvider(descriptor);
119+
return new AsyncDeclarationsProvider(benchmark);
138120
}
139121

140122
if (method.ReturnType == typeof(void) && method.HasAttribute<AsyncStateMachineAttribute>())
141123
{
142124
throw new NotSupportedException("async void is not supported by design");
143125
}
144126

145-
return new SyncDeclarationsProvider(descriptor);
127+
return new SyncDeclarationsProvider(benchmark);
146128
}
147129

148130
// internal for tests
@@ -158,31 +140,19 @@ private static string GetArgumentsDefinition(BenchmarkCase benchmarkCase)
158140
=> string.Join(
159141
", ",
160142
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
161-
.Select((parameter, index) => $"{GetParameterModifier(parameter)} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index}"));
143+
.Select((parameter, index) => $"{GetParameterModifier(parameter)} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index}"));
162144

163145
private static string GetDeclareArgumentFields(BenchmarkCase benchmarkCase)
164146
=> string.Join(
165147
Environment.NewLine,
166148
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
167-
.Select((parameter, index) => $"private {GetFieldType(parameter.ParameterType, benchmarkCase.Parameters.GetArgument(parameter.Name)).GetCorrectCSharpTypeName()} __argField{index};"));
149+
.Select((parameter, index) => $"public {GetFieldType(parameter.ParameterType, benchmarkCase.Parameters.GetArgument(parameter.Name)).GetCorrectCSharpTypeName()} __argField{index};"));
168150

169151
private static string GetInitializeArgumentFields(BenchmarkCase benchmarkCase)
170152
=> string.Join(
171153
Environment.NewLine,
172154
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
173-
.Select((parameter, index) => $"this.__argField{index} = {benchmarkCase.Parameters.GetArgument(parameter.Name).ToSourceCode()};")); // we init the fields in ctor to provoke all possible allocations and overhead of other type
174-
175-
private static string GetLoadArguments(BenchmarkCase benchmarkCase)
176-
=> string.Join(
177-
Environment.NewLine,
178-
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
179-
.Select((parameter, index) => $"{(parameter.ParameterType.IsByRef ? "ref" : string.Empty)} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index} = {(parameter.ParameterType.IsByRef ? "ref" : string.Empty)} this.__argField{index};"));
180-
181-
private static string GetPassArguments(BenchmarkCase benchmarkCase)
182-
=> string.Join(
183-
", ",
184-
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
185-
.Select((parameter, index) => $"{GetParameterModifier(parameter)} arg{index}"));
155+
.Select((parameter, index) => $"this.__fieldsContainer.__argField{index} = {benchmarkCase.Parameters.GetArgument(parameter.Name).ToSourceCode()};")); // we init the fields in ctor to provoke all possible allocations and overhead of other type
186156

187157
private static string GetExtraAttributes(BuildPartition buildPartition)
188158
{
@@ -236,7 +206,7 @@ private static string GetInProcessDiagnoserRouters(BenchmarkBuildInfo buildInfo)
236206
}
237207
}
238208

239-
private static string GetParameterModifier(ParameterInfo parameterInfo)
209+
internal static string GetParameterModifier(ParameterInfo parameterInfo)
240210
{
241211
if (!parameterInfo.ParameterType.IsByRef)
242212
return string.Empty;
@@ -260,28 +230,21 @@ private static Type GetFieldType(Type argumentType, ParameterInstance argument)
260230

261231
return argumentType;
262232
}
233+
}
263234

264-
private class SmartStringBuilder
265-
{
266-
private readonly string originalText;
267-
private readonly StringBuilder builder;
268-
269-
public SmartStringBuilder(string text)
270-
{
271-
originalText = text;
272-
builder = new StringBuilder(text);
273-
}
274-
275-
public SmartStringBuilder Replace(string oldValue, string? newValue)
276-
{
277-
if (originalText.Contains(oldValue))
278-
builder.Replace(oldValue, newValue);
279-
else
280-
builder.Append($"\n// '{oldValue}' not found");
281-
return this;
282-
}
235+
internal class SmartStringBuilder(string text)
236+
{
237+
private readonly StringBuilder builder = new(text);
283238

284-
public override string ToString() => builder.ToString();
239+
public SmartStringBuilder Replace(string oldValue, string? newValue)
240+
{
241+
if (text.Contains(oldValue))
242+
builder.Replace(oldValue, newValue);
243+
else
244+
builder.Append($"\n// '{oldValue}' not found");
245+
return this;
285246
}
247+
248+
public override string ToString() => builder.ToString();
286249
}
287250
}

0 commit comments

Comments
 (0)