Skip to content

Commit 8b14cbb

Browse files
timcassellCorniel
andauthored
Constant stack size (#2688)
* Inline engine stages. Apply AggressiveOptimization to engine methods. * Update src/BenchmarkDotNet/Engines/Engine.cs Co-authored-by: Corniel Nobel <corniel@gmail.com> * Refactor to use `IEngineStageEvaluator` for constant instruction location and simpler engine code. * Refactored according to PR comments. * Fix measurementsForStatistics. --------- Co-authored-by: Corniel Nobel <corniel@gmail.com>
1 parent 3afc4e7 commit 8b14cbb

19 files changed

+359
-799
lines changed

src/BenchmarkDotNet/Engines/Engine.cs

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.Globalization;
45
using System.Linq;
56
using System.Runtime.CompilerServices;
67
using BenchmarkDotNet.Characteristics;
8+
using BenchmarkDotNet.Environments;
79
using BenchmarkDotNet.Jobs;
10+
using BenchmarkDotNet.Mathematics;
811
using BenchmarkDotNet.Portability;
912
using BenchmarkDotNet.Reports;
1013
using JetBrains.Annotations;
@@ -15,8 +18,6 @@ namespace BenchmarkDotNet.Engines
1518
[UsedImplicitly]
1619
public class Engine : IEngine
1720
{
18-
public const int MinInvokeCount = 4;
19-
2021
[PublicAPI] public IHost Host { get; }
2122
[PublicAPI] public Action<long> WorkloadAction { get; }
2223
[PublicAPI] public Action Dummy1Action { get; }
@@ -41,9 +42,6 @@ public class Engine : IEngine
4142
private bool MemoryRandomization { get; }
4243

4344
private readonly List<Measurement> jittingMeasurements = new (10);
44-
private readonly EnginePilotStage pilotStage;
45-
private readonly EngineWarmupStage warmupStage;
46-
private readonly EngineActualStage actualStage;
4745
private readonly bool includeExtraStats;
4846
private readonly Random random;
4947

@@ -79,10 +77,6 @@ internal Engine(
7977
EvaluateOverhead = targetJob.ResolveValue(AccuracyMode.EvaluateOverheadCharacteristic, Resolver);
8078
MemoryRandomization = targetJob.ResolveValue(RunMode.MemoryRandomizationCharacteristic, Resolver);
8179

82-
warmupStage = new EngineWarmupStage(this);
83-
pilotStage = new EnginePilotStage(this);
84-
actualStage = new EngineActualStage(this);
85-
8680
random = new Random(12345); // we are using constant seed to try to get repeatable results
8781
}
8882

@@ -102,6 +96,9 @@ public void Dispose()
10296
}
10397
}
10498

99+
// AggressiveOptimization forces the method to go straight to tier1 JIT, and will never be re-jitted,
100+
// eliminating tiered JIT as a potential variable in measurements.
101+
[MethodImpl(CodeGenHelper.AggressiveOptimizationOption)]
105102
public RunResults Run()
106103
{
107104
var measurements = new List<Measurement>();
@@ -112,30 +109,34 @@ public RunResults Run()
112109
if (EngineEventSource.Log.IsEnabled())
113110
EngineEventSource.Log.BenchmarkStart(BenchmarkName);
114111

115-
if (Strategy != RunStrategy.ColdStart)
116-
{
117-
if (Strategy != RunStrategy.Monitoring)
118-
{
119-
var pilotStageResult = pilotStage.Run();
120-
invokeCount = pilotStageResult.PerfectInvocationCount;
121-
measurements.AddRange(pilotStageResult.Measurements);
122-
123-
if (EvaluateOverhead)
124-
{
125-
measurements.AddRange(warmupStage.RunOverhead(invokeCount, UnrollFactor));
126-
measurements.AddRange(actualStage.RunOverhead(invokeCount, UnrollFactor));
127-
}
112+
// Enumerate the stages and run iterations in a loop to ensure each benchmark invocation is called with a constant stack size.
113+
// #1120
114+
foreach (var stage in EngineStage.EnumerateStages(this, Strategy, EvaluateOverhead))
115+
{
116+
if (stage.Stage == IterationStage.Actual && stage.Mode == IterationMode.Workload)
117+
{
118+
Host.BeforeMainRun();
119+
}
120+
121+
var stageMeasurements = stage.GetMeasurementList();
122+
// 1-based iterationIndex
123+
int iterationIndex = 1;
124+
while (stage.GetShouldRunIteration(stageMeasurements, ref invokeCount))
125+
{
126+
var measurement = RunIteration(new IterationData(stage.Mode, stage.Stage, iterationIndex, invokeCount, UnrollFactor));
127+
stageMeasurements.Add(measurement);
128+
++iterationIndex;
129+
}
130+
measurements.AddRange(stageMeasurements);
131+
132+
WriteLine();
133+
134+
if (stage.Stage == IterationStage.Actual && stage.Mode == IterationMode.Workload)
135+
{
136+
Host.AfterMainRun();
128137
}
129-
130-
measurements.AddRange(warmupStage.RunWorkload(invokeCount, UnrollFactor, Strategy));
131138
}
132139

133-
Host.BeforeMainRun();
134-
135-
measurements.AddRange(actualStage.RunWorkload(invokeCount, UnrollFactor, forceSpecific: Strategy == RunStrategy.Monitoring));
136-
137-
Host.AfterMainRun();
138-
139140
(GcStats workGcHasDone, ThreadingStats threadingStats, double exceptionFrequency) = includeExtraStats
140141
? GetExtraStats(new IterationData(IterationMode.Workload, IterationStage.Actual, 0, invokeCount, UnrollFactor))
141142
: (GcStats.Empty, ThreadingStats.Empty, 0);
@@ -148,11 +149,15 @@ public RunResults Run()
148149
return new RunResults(measurements, outlierMode, workGcHasDone, threadingStats, exceptionFrequency);
149150
}
150151

152+
[MethodImpl(CodeGenHelper.AggressiveOptimizationOption)]
151153
public Measurement RunIteration(IterationData data)
152154
{
153155
// Initialization
154156
long invokeCount = data.InvokeCount;
155157
int unrollFactor = data.UnrollFactor;
158+
if (invokeCount % unrollFactor != 0)
159+
throw new ArgumentOutOfRangeException(nameof(data), $"InvokeCount({invokeCount}) should be a multiple of UnrollFactor({unrollFactor}).");
160+
156161
long totalOperations = invokeCount * OperationsPerInvoke;
157162
bool isOverhead = data.IterationMode == IterationMode.Overhead;
158163
bool randomizeMemory = !isOverhead && MemoryRandomization;
@@ -167,7 +172,7 @@ public Measurement RunIteration(IterationData data)
167172
EngineEventSource.Log.IterationStart(data.IterationMode, data.IterationStage, totalOperations);
168173

169174
var clockSpan = randomizeMemory
170-
? MeasureWithRandomMemory(action, invokeCount / unrollFactor)
175+
? MeasureWithRandomStack(action, invokeCount / unrollFactor)
171176
: Measure(action, invokeCount / unrollFactor);
172177

173178
if (EngineEventSource.Log.IsEnabled())
@@ -193,8 +198,8 @@ public Measurement RunIteration(IterationData data)
193198
// This is in a separate method, because stackalloc can affect code alignment,
194199
// resulting in unexpected measurements on some AMD cpus,
195200
// even if the stackalloc branch isn't executed. (#2366)
196-
[MethodImpl(MethodImplOptions.NoInlining)]
197-
private unsafe ClockSpan MeasureWithRandomMemory(Action<long> action, long invokeCount)
201+
[MethodImpl(MethodImplOptions.NoInlining | CodeGenHelper.AggressiveOptimizationOption)]
202+
private unsafe ClockSpan MeasureWithRandomStack(Action<long> action, long invokeCount)
198203
{
199204
byte* stackMemory = stackalloc byte[random.Next(32)];
200205
var clockSpan = Measure(action, invokeCount);
@@ -205,6 +210,7 @@ private unsafe ClockSpan MeasureWithRandomMemory(Action<long> action, long invok
205210
[MethodImpl(MethodImplOptions.NoInlining)]
206211
private unsafe void Consume(byte* _) { }
207212

213+
[MethodImpl(MethodImplOptions.NoInlining | CodeGenHelper.AggressiveOptimizationOption)]
208214
private ClockSpan Measure(Action<long> action, long invokeCount)
209215
{
210216
var clock = Clock.Start();
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using BenchmarkDotNet.Characteristics;
4+
using BenchmarkDotNet.Jobs;
5+
using BenchmarkDotNet.Mathematics;
6+
using BenchmarkDotNet.Reports;
7+
using Perfolizer.Horology;
8+
using Perfolizer.Mathematics.OutlierDetection;
9+
10+
namespace BenchmarkDotNet.Engines
11+
{
12+
internal abstract class EngineActualStage(IterationMode iterationMode) : EngineStage(IterationStage.Actual, iterationMode)
13+
{
14+
internal const int MaxOverheadIterationCount = 20;
15+
16+
internal static EngineActualStage GetOverhead(IEngine engine)
17+
=> new EngineActualStageAuto(engine.TargetJob, engine.Resolver, IterationMode.Overhead);
18+
19+
internal static EngineActualStage GetWorkload(IEngine engine, RunStrategy strategy)
20+
{
21+
var targetJob = engine.TargetJob;
22+
int? iterationCount = targetJob.ResolveValueAsNullable(RunMode.IterationCountCharacteristic);
23+
const int DefaultWorkloadCount = 10;
24+
return iterationCount == null && strategy != RunStrategy.Monitoring
25+
? new EngineActualStageAuto(targetJob, engine.Resolver, IterationMode.Workload)
26+
: new EngineActualStageSpecific(iterationCount ?? DefaultWorkloadCount, IterationMode.Workload);
27+
}
28+
}
29+
30+
internal sealed class EngineActualStageAuto : EngineActualStage
31+
{
32+
private readonly double maxRelativeError;
33+
private readonly TimeInterval? maxAbsoluteError;
34+
private readonly OutlierMode outlierMode;
35+
private readonly int minIterationCount;
36+
private readonly int maxIterationCount;
37+
private readonly List<Measurement> measurementsForStatistics;
38+
private int iterationCounter = 0;
39+
40+
public EngineActualStageAuto(Job targetJob, IResolver resolver, IterationMode iterationMode) : base(iterationMode)
41+
{
42+
maxRelativeError = targetJob.ResolveValue(AccuracyMode.MaxRelativeErrorCharacteristic, resolver);
43+
maxAbsoluteError = targetJob.ResolveValueAsNullable(AccuracyMode.MaxAbsoluteErrorCharacteristic);
44+
outlierMode = targetJob.ResolveValue(AccuracyMode.OutlierModeCharacteristic, resolver);
45+
minIterationCount = targetJob.ResolveValue(RunMode.MinIterationCountCharacteristic, resolver);
46+
maxIterationCount = targetJob.ResolveValue(RunMode.MaxIterationCountCharacteristic, resolver);
47+
measurementsForStatistics = GetMeasurementList();
48+
}
49+
50+
internal override List<Measurement> GetMeasurementList() => new (maxIterationCount);
51+
52+
internal override bool GetShouldRunIteration(List<Measurement> measurements, ref long invokeCount)
53+
{
54+
if (measurements.Count == 0)
55+
{
56+
return true;
57+
}
58+
59+
const double MaxOverheadRelativeError = 0.05;
60+
bool isOverhead = Mode == IterationMode.Overhead;
61+
double effectiveMaxRelativeError = isOverhead ? MaxOverheadRelativeError : maxRelativeError;
62+
iterationCounter++;
63+
var measurement = measurements[measurements.Count - 1];
64+
measurementsForStatistics.Add(measurement);
65+
66+
var statistics = MeasurementsStatistics.Calculate(measurementsForStatistics, outlierMode);
67+
double actualError = statistics.LegacyConfidenceInterval.Margin;
68+
69+
double maxError1 = effectiveMaxRelativeError * statistics.Mean;
70+
double maxError2 = maxAbsoluteError?.Nanoseconds ?? double.MaxValue;
71+
double maxError = Math.Min(maxError1, maxError2);
72+
73+
if (iterationCounter >= minIterationCount && actualError < maxError)
74+
{
75+
return false;
76+
}
77+
78+
if (iterationCounter >= maxIterationCount || isOverhead && iterationCounter >= MaxOverheadIterationCount)
79+
{
80+
return false;
81+
}
82+
83+
return true;
84+
}
85+
}
86+
87+
internal sealed class EngineActualStageSpecific(int maxIterationCount, IterationMode iterationMode) : EngineActualStage(iterationMode)
88+
{
89+
private int iterationCount = 0;
90+
91+
internal override List<Measurement> GetMeasurementList() => new (maxIterationCount);
92+
93+
internal override bool GetShouldRunIteration(List<Measurement> measurements, ref long invokeCount)
94+
=> ++iterationCount <= maxIterationCount;
95+
}
96+
}

src/BenchmarkDotNet/Engines/EngineGeneralStage.cs

Lines changed: 0 additions & 90 deletions
This file was deleted.

0 commit comments

Comments
 (0)