Skip to content

Commit 21a2940

Browse files
authoredSep 22, 2022
Implement apples to apples comparison mode (#2116)
* InvocationCount should be represented with Int64, not Int32 * implement apples-to-apples comparison
1 parent 0cee169 commit 21a2940

File tree

10 files changed

+125
-8
lines changed

10 files changed

+125
-8
lines changed
 

‎src/BenchmarkDotNet/Configs/ConfigOptions.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ public enum ConfigOptions
4040
/// <summary>
4141
/// Determines whether to generate msbuild binlogs
4242
/// </summary>
43-
GenerateMSBuildBinLog = 1 << 7
43+
GenerateMSBuildBinLog = 1 << 7,
44+
/// <summary>
45+
/// Performs apples-to-apples comparison for provided benchmarks and jobs. Experimental, will change in the near future!
46+
/// </summary>
47+
ApplesToApples = 1 << 8
4448
}
4549

4650
internal static class ConfigOptionsExtensions

‎src/BenchmarkDotNet/Configs/ManualConfig.cs

+4
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,10 @@ public static ManualConfig Union(IConfig globalConfig, IConfig localConfig)
300300
return manualConfig;
301301
}
302302

303+
internal void RemoveAllJobs() => jobs.Clear();
304+
305+
internal void RemoveAllDiagnosers() => diagnosers.Clear();
306+
303307
private static TimeSpan GetBuildTimeout(TimeSpan current, TimeSpan other)
304308
=> current == DefaultConfig.Instance.BuildTimeout
305309
? other

‎src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ public bool UseDisassemblyDiagnoser
135135
public int? MaxIterationCount { get; set; }
136136

137137
[Option("invocationCount", Required = false, HelpText = "Invocation count in a single iteration. By default calculated by the heuristic.")]
138-
public int? InvocationCount { get; set; }
138+
public long? InvocationCount { get; set; }
139139

140140
[Option("unrollFactor", Required = false, HelpText = "How many times the benchmark method will be invoked per one iteration of a generated loop. 16 by default")]
141141
public int? UnrollFactor { get; set; }
@@ -152,6 +152,9 @@ public bool UseDisassemblyDiagnoser
152152
[Option("info", Required = false, Default = false, HelpText = "Print environment information.")]
153153
public bool PrintInformation { get; set; }
154154

155+
[Option("apples", Required = false, Default = false, HelpText = "Runs apples-to-apples comparison for specified Jobs.")]
156+
public bool ApplesToApples { get; set; }
157+
155158
[Option("list", Required = false, Default = ListBenchmarkCaseMode.Disabled, HelpText = "Prints all of the available benchmark names. Flat/Tree")]
156159
public ListBenchmarkCaseMode ListBenchmarkCaseMode { get; set; }
157160

‎src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs

+1
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ private static IConfig CreateConfig(CommandLineOptions options, IConfig globalCo
244244
config.WithOption(ConfigOptions.DisableLogFile, options.DisableLogFile);
245245
config.WithOption(ConfigOptions.LogBuildOutput, options.LogBuildOutput);
246246
config.WithOption(ConfigOptions.GenerateMSBuildBinLog, options.GenerateMSBuildBinLog);
247+
config.WithOption(ConfigOptions.ApplesToApples, options.ApplesToApples);
247248

248249
if (options.MaxParameterColumnWidth.HasValue)
249250
config.WithSummaryStyle(SummaryStyle.Default.WithMaxParameterColumnWidth(options.MaxParameterColumnWidth.Value));

‎src/BenchmarkDotNet/Jobs/JobExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ public static Job WithHeapAffinitizeMask(this Job job, int heapAffinitizeMask) =
170170
/// If specified, <see cref="RunMode.IterationTime"/> will be ignored.
171171
/// If specified, it must be a multiple of <see cref="RunMode.UnrollFactor"/>.
172172
/// </summary>
173-
public static Job WithInvocationCount(this Job job, int count) => job.WithCore(j => j.Run.InvocationCount = count);
173+
public static Job WithInvocationCount(this Job job, long count) => job.WithCore(j => j.Run.InvocationCount = count);
174174

175175
/// <summary>
176176
/// How many times the benchmark method will be invoked per one iteration of a generated loop.

‎src/BenchmarkDotNet/Jobs/RunMode.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public sealed class RunMode : JobMode<RunMode>
1212
public static readonly Characteristic<RunStrategy> RunStrategyCharacteristic = Characteristic.Create<RunMode, RunStrategy>(nameof(RunStrategy), RunStrategy.Throughput);
1313

1414
public static readonly Characteristic<int> LaunchCountCharacteristic = CreateCharacteristic<int>(nameof(LaunchCount));
15-
public static readonly Characteristic<int> InvocationCountCharacteristic = CreateCharacteristic<int>(nameof(InvocationCount));
15+
public static readonly Characteristic<long> InvocationCountCharacteristic = CreateCharacteristic<long>(nameof(InvocationCount));
1616
public static readonly Characteristic<int> UnrollFactorCharacteristic = CreateCharacteristic<int>(nameof(UnrollFactor));
1717
public static readonly Characteristic<int> IterationCountCharacteristic = CreateCharacteristic<int>(nameof(IterationCount));
1818
public static readonly Characteristic<int> MinIterationCountCharacteristic = CreateCharacteristic<int>(nameof(MinIterationCount));
@@ -125,7 +125,7 @@ public TimeInterval IterationTime
125125
/// If specified, <see cref="IterationTime"/> will be ignored.
126126
/// If specified, it must be a multiple of <see cref="UnrollFactor"/>.
127127
/// </summary>
128-
public int InvocationCount
128+
public long InvocationCount
129129
{
130130
get { return InvocationCountCharacteristic[this]; }
131131
set { InvocationCountCharacteristic[this] = value; }

‎src/BenchmarkDotNet/Parameters/ParameterInstances.cs

+23-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
namespace BenchmarkDotNet.Parameters
77
{
8-
public class ParameterInstances : IDisposable
8+
public class ParameterInstances : IEquatable<ParameterInstances>, IDisposable
99
{
1010
public IReadOnlyList<ParameterInstance> Items { get; }
1111
public int Count => Items.Count;
@@ -36,5 +36,27 @@ public void Dispose()
3636
public string PrintInfo => printInfo ?? (printInfo = string.Join("&", Items.Select(p => $"{p.Name}={p.ToDisplayText()}")));
3737

3838
public ParameterInstance GetArgument(string name) => Items.Single(parameter => parameter.IsArgument && parameter.Name == name);
39+
40+
public bool Equals(ParameterInstances other)
41+
{
42+
if (other.Count != Count)
43+
{
44+
return false;
45+
}
46+
47+
for (int i = 0; i < Count; i++)
48+
{
49+
if (!Items[i].Value.Equals(other[i].Value))
50+
{
51+
return false;
52+
}
53+
}
54+
55+
return true;
56+
}
57+
58+
public override bool Equals(object obj) => obj is ParameterInstances other && Equals(other);
59+
60+
public override int GetHashCode() => FolderInfo.GetHashCode();
3961
}
4062
}

‎src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs

+77
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
using BenchmarkDotNet.ConsoleArguments.ListBenchmarks;
1010
using BenchmarkDotNet.Environments;
1111
using BenchmarkDotNet.Extensions;
12+
using BenchmarkDotNet.Jobs;
1213
using BenchmarkDotNet.Loggers;
14+
using BenchmarkDotNet.Parameters;
1315
using BenchmarkDotNet.Reports;
1416
using JetBrains.Annotations;
17+
using Perfolizer.Mathematics.OutlierDetection;
1518

1619
namespace BenchmarkDotNet.Running
1720
{
@@ -109,6 +112,11 @@ internal IEnumerable<Summary> RunWithDirtyAssemblyResolveHelper(string[] args, I
109112
? allAvailableTypesWithRunnableBenchmarks
110113
: userInteraction.AskUser(allAvailableTypesWithRunnableBenchmarks, logger);
111114

115+
if (effectiveConfig.Options.HasFlag(ConfigOptions.ApplesToApples))
116+
{
117+
return ApplesToApples(effectiveConfig, benchmarksToFilter, logger, options);
118+
}
119+
112120
var filteredBenchmarks = TypeFilter.Filter(effectiveConfig, benchmarksToFilter);
113121

114122
if (filteredBenchmarks.IsEmpty())
@@ -131,5 +139,74 @@ private static void PrintList(ILogger nonNullLogger, IConfig effectiveConfig, IR
131139

132140
printer.Print(testNames, nonNullLogger);
133141
}
142+
143+
private IEnumerable<Summary> ApplesToApples(ManualConfig effectiveConfig, IReadOnlyList<Type> benchmarksToFilter, ILogger logger, CommandLineOptions options)
144+
{
145+
var jobs = effectiveConfig.GetJobs().ToArray();
146+
if (jobs.Length <= 1)
147+
{
148+
logger.WriteError("To use apples-to-apples comparison you must specify at least two Job objects.");
149+
return Array.Empty<Summary>();
150+
}
151+
var baselineJob = jobs.SingleOrDefault(job => job.Meta.Baseline);
152+
if (baselineJob == default)
153+
{
154+
logger.WriteError("To use apples-to-apples comparison you must specify exactly ONE baseline Job object.");
155+
return Array.Empty<Summary>();
156+
}
157+
else if (jobs.Any(job => !job.Run.HasValue(RunMode.IterationCountCharacteristic)))
158+
{
159+
logger.WriteError("To use apples-to-apples comparison you must specify the number of iterations in explicit way.");
160+
return Array.Empty<Summary>();
161+
}
162+
163+
Job invocationCountJob = baselineJob
164+
.WithWarmupCount(1)
165+
.WithIterationCount(1)
166+
.WithEvaluateOverhead(false);
167+
168+
ManualConfig invocationCountConfig = ManualConfig.Create(effectiveConfig);
169+
invocationCountConfig.RemoveAllJobs();
170+
invocationCountConfig.RemoveAllDiagnosers();
171+
invocationCountConfig.AddJob(invocationCountJob);
172+
173+
var invocationCountBenchmarks = TypeFilter.Filter(invocationCountConfig, benchmarksToFilter);
174+
if (invocationCountBenchmarks.IsEmpty())
175+
{
176+
userInteraction.PrintWrongFilterInfo(benchmarksToFilter, logger, options.Filters.ToArray());
177+
return Array.Empty<Summary>();
178+
}
179+
180+
logger.WriteLineHeader("Each benchmark is going to be executed just once to get invocation counts.");
181+
Summary[] invocationCountSummaries = BenchmarkRunnerClean.Run(invocationCountBenchmarks);
182+
183+
Dictionary<(Descriptor Descriptor, ParameterInstances Parameters), Measurement> dictionary = invocationCountSummaries
184+
.SelectMany(summary => summary.Reports)
185+
.ToDictionary(
186+
report => (report.BenchmarkCase.Descriptor, report.BenchmarkCase.Parameters),
187+
report => report.AllMeasurements.Single(measurement => measurement.IsWorkload() && measurement.IterationStage == Engines.IterationStage.Actual));
188+
189+
int iterationCount = baselineJob.Run.IterationCount;
190+
BenchmarkRunInfo[] benchmarksWithoutInvocationCount = TypeFilter.Filter(effectiveConfig, benchmarksToFilter);
191+
BenchmarkRunInfo[] benchmarksWithInvocationCount = benchmarksWithoutInvocationCount
192+
.Select(benchmarkInfo => new BenchmarkRunInfo(
193+
benchmarkInfo.BenchmarksCases.Select(benchmark =>
194+
new BenchmarkCase(
195+
benchmark.Descriptor,
196+
benchmark.Job
197+
.WithIterationCount(iterationCount)
198+
.WithEvaluateOverhead(false)
199+
.WithWarmupCount(1)
200+
.WithOutlierMode(OutlierMode.DontRemove)
201+
.WithInvocationCount(dictionary[(benchmark.Descriptor, benchmark.Parameters)].Operations)
202+
.WithUnrollFactor(dictionary[(benchmark.Descriptor, benchmark.Parameters)].Operations % 16 == 0 ? 16 : 1),
203+
benchmark.Parameters,
204+
benchmark.Config)).ToArray(),
205+
benchmarkInfo.Type, benchmarkInfo.Config))
206+
.ToArray();
207+
208+
logger.WriteLineHeader("Actual benchmarking is going to happen now!");
209+
return BenchmarkRunnerClean.Run(benchmarksWithInvocationCount);
210+
}
134211
}
135212
}

‎src/BenchmarkDotNet/Running/Descriptor.cs

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
namespace BenchmarkDotNet.Running
1010
{
11-
public class Descriptor
11+
public class Descriptor : IEquatable<Descriptor>
1212
{
1313
public Type Type { get; }
1414
public MethodInfo WorkloadMethod { get; }
@@ -70,5 +70,11 @@ private static string FormatDescription([CanBeNull] string description)
7070
public bool HasCategory(string category) => Categories.Any(c => c.EqualsWithIgnoreCase(category));
7171

7272
public string GetFilterName() => $"{Type.GetCorrectCSharpTypeName(includeGenericArgumentsNamespace: false)}.{WorkloadMethod.Name}";
73+
74+
public bool Equals(Descriptor other) => GetFilterName().Equals(other.GetFilterName());
75+
76+
public override bool Equals(object obj) => obj is Descriptor && Equals((Descriptor)obj);
77+
78+
public override int GetHashCode() => GetFilterName().GetHashCode();
7379
}
7480
}

‎src/BenchmarkDotNet/Validators/RunModeValidator.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public IEnumerable<ValidationError> Validate(ValidationParameters validationPara
2828
}
2929
else if (run.HasValue(RunMode.InvocationCountCharacteristic))
3030
{
31-
int invocationCount = run.InvocationCount;
31+
long invocationCount = run.InvocationCount;
3232
if (invocationCount % unrollFactor != 0)
3333
{
3434
string message = $"Specified InvocationCount ({invocationCount}) must be a multiple of UnrollFactor ({unrollFactor})";

0 commit comments

Comments
 (0)
Please sign in to comment.