Skip to content

Commit a9458f1

Browse files
committed
Updated jit stage to account for OSR.
Don't jit overhead methods if the job is configured to not measure it. Remove extra call counting delay for in-process benchmarks. Set CallCountingDelayMs env var if DisassemblyDiagnoser is not used. Added a test for very long first invocation time.
1 parent 1251e63 commit a9458f1

File tree

8 files changed

+157
-80
lines changed

8 files changed

+157
-80
lines changed

src/BenchmarkDotNet/Configs/ImmutableConfig.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ internal ImmutableConfig(
120120

121121
internal bool HasPerfCollectProfiler() => diagnosers.OfType<PerfCollectProfiler>().Any();
122122

123+
internal bool HasDisassemblyDiagnoser() => diagnosers.OfType<DisassemblyDiagnoser>().Any();
124+
123125
public bool HasExtraStatsDiagnoser() => HasMemoryDiagnoser() || HasThreadingDiagnoser() || HasExceptionDiagnoser();
124126

125127
public IDiagnoser? GetCompositeDiagnoser(BenchmarkCase benchmarkCase, RunMode runMode)

src/BenchmarkDotNet/Engines/Engine.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Threading;
66
using BenchmarkDotNet.Characteristics;
77
using BenchmarkDotNet.Environments;
8+
using BenchmarkDotNet.Helpers;
89
using BenchmarkDotNet.Jobs;
910
using BenchmarkDotNet.Portability;
1011
using BenchmarkDotNet.Reports;
@@ -213,12 +214,9 @@ private ClockSpan Measure(Action<long> action, long invokeCount)
213214
ForceGcCollect();
214215

215216
// #1542
216-
if (JitInfo.BackgroundCompilationDelay > TimeSpan.Zero)
217-
{
218-
// We put the current thread to sleep so tiered jit can kick in, compile its stuff,
219-
// and NOT allocate anything on the background thread when we are measuring allocations.
220-
Thread.Sleep(JitInfo.BackgroundCompilationDelay);
221-
}
217+
// If the jit is tiered, we put the current thread to sleep so it can kick in, compile its stuff,
218+
// and NOT allocate anything on the background thread when we are measuring allocations.
219+
SleepHelper.SleepIfPositive(JitInfo.BackgroundCompilationDelay);
222220

223221
GcStats gcStats;
224222
using (FinalizerBlocker.MaybeStart())

src/BenchmarkDotNet/Engines/EngineJitStage.cs

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Threading;
4+
using BenchmarkDotNet.Helpers;
45
using BenchmarkDotNet.Jobs;
56
using BenchmarkDotNet.Portability;
67
using BenchmarkDotNet.Reports;
@@ -27,27 +28,35 @@ internal sealed class EngineFirstJitStage : EngineJitStage
2728
// It is not worth spending a long time in jit stage for macro-benchmarks.
2829
private static readonly TimeInterval MaxTieringTime = TimeInterval.FromSeconds(10);
2930

31+
// Jit call counting delay is only for when the app starts up. We don't need to wait for every benchmark if multiple benchmarks are ran in-process.
32+
private static TimeSpan tieredDelay = JitInfo.TieredDelay;
33+
3034
internal bool didStopEarly = false;
3135
internal Measurement lastMeasurement;
3236

3337
private readonly IEnumerator<IterationData> enumerator;
38+
private readonly bool evaluateOverhead;
3439

35-
internal EngineFirstJitStage(EngineParameters parameters) : base(parameters)
40+
internal EngineFirstJitStage(bool evaluateOverhead, EngineParameters parameters) : base(parameters)
3641
{
3742
enumerator = EnumerateIterations();
43+
this.evaluateOverhead = evaluateOverhead;
3844
}
3945

4046
internal override List<Measurement> GetMeasurementList() => new(GetMaxMeasurementCount());
4147

42-
private static int GetMaxMeasurementCount()
48+
private int GetMaxMeasurementCount()
4349
{
44-
int tieredCallCountThreshold = JitInfo.TieredCallCountThreshold;
45-
if (JitInfo.IsDPGO)
50+
if (!JitInfo.IsTiered)
4651
{
47-
tieredCallCountThreshold *= 2;
52+
return 1;
4853
}
49-
// +1 for first jit, x2 for overhead + workload
50-
return (tieredCallCountThreshold + 1) * 2;
54+
int count = JitInfo.MaxTierPromotions* JitInfo.TieredCallCountThreshold + 2;
55+
if (evaluateOverhead)
56+
{
57+
count *= 2;
58+
}
59+
return count;
5160
}
5261

5362
internal override bool GetShouldRunIteration(List<Measurement> measurements, out IterationData iterationData)
@@ -73,8 +82,11 @@ internal override bool GetShouldRunIteration(List<Measurement> measurements, out
7382
private IEnumerator<IterationData> EnumerateIterations()
7483
{
7584
++iterationIndex;
76-
yield return GetDummyIterationData(dummy1Action);
77-
yield return GetOverheadIterationData();
85+
if (evaluateOverhead)
86+
{
87+
yield return GetDummyIterationData(dummy1Action);
88+
yield return GetOverheadIterationData();
89+
}
7890
yield return GetDummyIterationData(dummy2Action);
7991
yield return GetWorkloadIterationData();
8092
yield return GetDummyIterationData(dummy3Action);
@@ -86,12 +98,14 @@ private IEnumerator<IterationData> EnumerateIterations()
8698
}
8799

88100
// Wait enough time for jit call counting to begin.
89-
MaybeSleep(JitInfo.TieredDelay);
101+
SleepHelper.SleepIfPositive(tieredDelay);
102+
// Don't make the next jit stage wait if it's ran in the same process.
103+
tieredDelay = TimeSpan.Zero;
90104

91105
// Attempt to promote methods to tier1, but don't spend too much time in jit stage.
92106
StartedClock startedClock = parameters.TargetJob.ResolveValue(InfrastructureMode.ClockCharacteristic, parameters.Resolver).Start();
93107

94-
int remainingTiers = JitInfo.IsDPGO ? 2 : 1;
108+
int remainingTiers = JitInfo.MaxTierPromotions;
95109
while (remainingTiers > 0)
96110
{
97111
--remainingTiers;
@@ -100,7 +114,10 @@ private IEnumerator<IterationData> EnumerateIterations()
100114
{
101115
--remainingCalls;
102116
++iterationIndex;
103-
yield return GetOverheadIterationData();
117+
if (evaluateOverhead)
118+
{
119+
yield return GetOverheadIterationData();
120+
}
104121
yield return GetWorkloadIterationData();
105122

106123
if ((remainingTiers + remainingCalls) > 0
@@ -111,13 +128,16 @@ private IEnumerator<IterationData> EnumerateIterations()
111128
}
112129
}
113130

114-
MaybeSleep(JitInfo.BackgroundCompilationDelay);
131+
SleepHelper.SleepIfPositive(JitInfo.BackgroundCompilationDelay);
115132
}
116133

117-
// Empirical evidence shows that the first call after the method is tiered up takes longer,
134+
// Empirical evidence shows that the first call after the method is tiered up may take longer,
118135
// so we run an extra iteration to ensure the next stage gets a stable measurement.
119136
++iterationIndex;
120-
yield return GetOverheadIterationData();
137+
if (evaluateOverhead)
138+
{
139+
yield return GetOverheadIterationData();
140+
}
121141
yield return GetWorkloadIterationData();
122142
}
123143

@@ -126,32 +146,22 @@ private IterationData GetOverheadIterationData()
126146

127147
private IterationData GetWorkloadIterationData()
128148
=> new(IterationMode.Workload, IterationStage.Jitting, iterationIndex, 1, 1, parameters.IterationSetupAction, parameters.IterationCleanupAction, parameters.WorkloadActionNoUnroll);
129-
130-
private static void MaybeSleep(TimeSpan timeSpan)
131-
{
132-
if (timeSpan > TimeSpan.Zero)
133-
{
134-
Thread.Sleep(timeSpan);
135-
}
136-
}
137149
}
138150

139-
internal sealed class EngineSecondJitStage(int unrollFactor, EngineParameters parameters) : EngineJitStage(parameters)
151+
internal sealed class EngineSecondJitStage : EngineJitStage
140152
{
141-
private readonly int unrollFactor = unrollFactor;
153+
private readonly int unrollFactor;
154+
private readonly bool evaluateOverhead;
142155

143-
internal override List<Measurement> GetMeasurementList() => new(GetMaxCallCount());
144-
145-
private static int GetMaxCallCount()
156+
public EngineSecondJitStage(int unrollFactor, bool evaluateOverhead, EngineParameters parameters) : base(parameters)
146157
{
147-
int tieredCallCountThreshold = JitInfo.TieredCallCountThreshold;
148-
if (JitInfo.IsDPGO)
149-
{
150-
tieredCallCountThreshold *= 2;
151-
}
152-
return tieredCallCountThreshold + 1;
158+
this.unrollFactor = unrollFactor;
159+
this.evaluateOverhead = evaluateOverhead;
160+
iterationIndex = evaluateOverhead ? 0 : 2;
153161
}
154162

163+
internal override List<Measurement> GetMeasurementList() => new(evaluateOverhead ? 5 : 3);
164+
155165
// The benchmark method has already been jitted via *NoUnroll, we only need to jit the *Unroll methods here, which aren't tiered.
156166
internal override bool GetShouldRunIteration(List<Measurement> measurements, out IterationData iterationData)
157167
{

src/BenchmarkDotNet/Engines/EngineStage.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ internal static IEnumerable<EngineStage> EnumerateStages(EngineParameters parame
3333
int minInvokeCount = parameters.TargetJob.ResolveValue(AccuracyMode.MinInvokeCountCharacteristic, parameters.Resolver);
3434

3535
// AOT technically doesn't have a JIT, but we run jit stage regardless because of static constructors. #2004
36-
var jitStage = new EngineFirstJitStage(parameters);
36+
var jitStage = new EngineFirstJitStage(evaluateOverhead, parameters);
3737
yield return jitStage;
3838

3939
bool hasUnrollFactor = parameters.TargetJob.HasValue(RunMode.UnrollFactorCharacteristic);
@@ -62,7 +62,7 @@ internal static IEnumerable<EngineStage> EnumerateStages(EngineParameters parame
6262
// TODO: This stage can be removed after we refactor the engine/codegen to pass the clock into the delegates.
6363
if (!RuntimeInformation.IsAot && unrollFactor != 1)
6464
{
65-
yield return new EngineSecondJitStage(unrollFactor, parameters);
65+
yield return new EngineSecondJitStage(unrollFactor, evaluateOverhead, parameters);
6666
}
6767

6868
if (!skipPilotStage)

src/BenchmarkDotNet/Extensions/ProcessExtensions.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,11 @@ internal static void SetEnvironmentVariables(this ProcessStartInfo start, Benchm
150150
// disable ReSharper's Dynamic Program Analysis (see https://github.com/dotnet/BenchmarkDotNet/issues/1871 for details)
151151
start.EnvironmentVariables["JETBRAINS_DPA_AGENT_ENABLE"] = "0";
152152

153-
// TODO: set CallCountingDelayMs without breaking DisassemblyDiagnoser. https://github.com/dotnet/runtime/issues/117339
154-
//SetClrEnvironmentVariables(start, JitInfo.CallCountingDelayMsEnv, "0");
153+
// CallCountingDelayMs=0 breaks DisassemblyDiagnoser, so we only set it if the job doesn't need disassembly. https://github.com/dotnet/runtime/issues/117339
154+
if (!benchmarkCase.Config.HasDisassemblyDiagnoser())
155+
{
156+
SetClrEnvironmentVariables(start, JitInfo.EnvCallCountingDelayMs, "0");
157+
}
155158

156159
if (!benchmarkCase.Job.HasValue(EnvironmentMode.EnvironmentVariablesCharacteristic))
157160
return;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
using System.Threading;
3+
4+
namespace BenchmarkDotNet.Helpers
5+
{
6+
internal static class SleepHelper
7+
{
8+
public static void SleepIfPositive(TimeSpan timeSpan)
9+
{
10+
if (timeSpan > TimeSpan.Zero)
11+
{
12+
Thread.Sleep(timeSpan);
13+
}
14+
}
15+
}
16+
}

src/BenchmarkDotNet/Portability/JitInfo.cs

Lines changed: 75 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,25 @@ namespace BenchmarkDotNet.Portability
77
{
88
// Implementation is based on article https://medium.com/@meriffa/net-core-concepts-tiered-compilation-10f7da3a29c7
99
// documentation https://learn.microsoft.com/en-us/dotnet/core/runtime-config/compilation
10-
// and source https://github.com/dotnet/runtime/blob/71c30b405516b1fe774a1bfdbc43cd804468568f/src/coreclr/vm/eeconfig.cpp
10+
// and source https://github.com/dotnet/runtime/blob/3fb6fbb3efaa7f2ae5e76bf235615d7f70005201/src/coreclr/vm/eeconfig.cpp
11+
// https://github.com/dotnet/runtime/blob/3fb6fbb3efaa7f2ae5e76bf235615d7f70005201/src/coreclr/jit/jitconfigvalues.h
1112
internal static class JitInfo
1213
{
13-
public const string MinOptsEnv = "JITMinOpts";
14-
public const string TieredCompilationEnv = "TieredCompilation";
15-
public const string DynamicPGOEnv = "TieredPGO";
16-
public const string AggressiveTieringEnv = "TC_AggressiveTiering";
17-
public const string CallCountThresholdEnv = "TC_CallCountThreshold";
18-
public const string CallCountingDelayMsEnv = "TC_CallCountingDelayMs";
14+
public const string EnvCallCountingDelayMs = "TC_CallCountingDelayMs";
15+
16+
private const string EnvMinOpts = "JITMinOpts";
17+
private const string EnvTieredCompilation = "TieredCompilation";
18+
private const string EnvQuickJit = "TC_QuickJit";
19+
private const string EnvPGO = "TieredPGO";
20+
private const string EnvCallCountThreshold = "TC_CallCountThreshold";
21+
private const string EnvAggressiveTiering = "TC_AggressiveTiering";
22+
private const string EnvOSR = "TC_OnStackReplacement";
23+
24+
private const string KnobTieredCompilation = "System.Runtime.TieredCompilation";
25+
private const string KnobQuickJit = "System.Runtime.TieredCompilation.QuickJit";
26+
private const string KnobPGO = "System.Runtime.TieredPGO";
27+
private const string KnobCallCountThreshold = "System.Runtime.TieredCompilation.CallCountThreshold";
28+
private const string KnobCallCountingDelayMs = "System.Runtime.TieredCompilation.CallCountingDelayMs";
1929

2030
// .Net 5 and older uses COMPlus_ prefix,
2131
// .Net 6+ uses DOTNET_ prefix, but still supports legacy COMPlus_.
@@ -40,34 +50,71 @@ private static bool IsKnobDisabled(string name)
4050
private static bool TryParseKnob(string name, out int value)
4151
=> int.TryParse(AppContext.GetData(name) as string, out value);
4252

53+
private static bool IsEnabled(string envName, string knobName)
54+
=> IsEnvVarEnabled(envName) || IsKnobEnabled(knobName);
55+
56+
private static bool IsDisabled(string envName, string knobName)
57+
=> IsEnvVarDisabled(envName) || IsKnobDisabled(knobName);
58+
4359
/// <summary>
4460
/// Is tiered JIT enabled?
4561
/// </summary>
4662
public static readonly bool IsTiered =
4763
IsNetCore
4864
// JITMinOpts disables tiered compilation (all methods are effectively tier0 instead of tier1).
49-
&& !IsEnvVarEnabled(MinOptsEnv)
65+
&& !IsEnvVarEnabled(EnvMinOpts)
5066
&& ((CoreRuntime.TryGetVersion(out var version) && version.Major >= 3)
5167
// Enabled by default in netcoreapp3.0+, check if it's disabled.
52-
? !IsEnvVarDisabled(TieredCompilationEnv) && !IsKnobDisabled("System.Runtime.TieredCompilation")
68+
? !IsDisabled(EnvTieredCompilation, KnobTieredCompilation)
5369
// Disabled by default in netcoreapp2.X, check if it's enabled.
54-
: IsEnvVarEnabled(TieredCompilationEnv) || IsKnobEnabled("System.Runtime.TieredCompilation"));
70+
: IsEnabled(EnvTieredCompilation, KnobTieredCompilation));
5571

5672
/// <summary>
57-
/// Is tiered JIT enabled with dynamic profile-guided optimization (tier0 instrumented)?
73+
/// The maximum numbers of jit tiers that a method may be promoted through. This is the maximum number of jit tiers - 1.
5874
/// </summary>
59-
public static readonly bool IsDPGO =
60-
IsTiered
61-
// Added experimentally in .Net 6
62-
&& Environment.Version.Major >= 6
63-
// Disabled if QuickJit is disabled in .Net 7+.
64-
&& (Environment.Version.Major < 7 || (!IsEnvVarDisabled("TC_QuickJit") && !IsKnobDisabled("System.Runtime.TieredCompilation.QuickJit")))
65-
&& (Environment.Version.Major >= 8
66-
// Enabled by default in .Net 8, check if it's disabled
67-
? !IsEnvVarDisabled(DynamicPGOEnv) && !IsKnobDisabled("System.Runtime.TieredPGO")
68-
// Disabled by default in earlier versions, check if it's enabled.
69-
: IsEnvVarEnabled(DynamicPGOEnv) || IsKnobEnabled("System.Runtime.TieredPGO"));
75+
public static readonly int MaxTierPromotions = GetMaxTierPromotions();
7076

77+
private static int GetMaxTierPromotions()
78+
{
79+
if (!IsTiered)
80+
{
81+
return 0;
82+
}
83+
// Tier1
84+
int maxPromotions = 1;
85+
if (GetIsDPGO())
86+
{
87+
// Tier0 instrumented
88+
++maxPromotions;
89+
}
90+
if (GetIsOSR())
91+
{
92+
// On-stack-replacement *shouldn't* interfere with promotion velocity, but there is a bug where OSR may cause a method to be tier0 instrumented twice.
93+
// https://github.com/dotnet/runtime/issues/117787#issuecomment-3090771091
94+
++maxPromotions;
95+
}
96+
return maxPromotions;
97+
98+
static bool GetIsDPGO() =>
99+
// Added experimentally in .Net 6.
100+
Environment.Version.Major >= 6
101+
// Disabled if QuickJit is disabled in .Net 7+.
102+
&& (Environment.Version.Major < 7 || !IsDisabled(EnvQuickJit, KnobQuickJit))
103+
&& (Environment.Version.Major >= 8
104+
// Enabled by default in .Net 8, check if it's disabled.
105+
? !IsDisabled(EnvPGO, KnobPGO)
106+
// Disabled by default in earlier versions, check if it's enabled.
107+
: IsEnabled(EnvPGO, KnobPGO));
108+
109+
static bool GetIsOSR() =>
110+
// Added experimentally in .Net 5.
111+
Environment.Version.Major >= 5
112+
&& (Environment.Version.Major >= 7
113+
// Enabled by default in .Net 7, check if it's disabled.
114+
? !IsEnvVarDisabled(EnvOSR)
115+
// Disabled by default in earlier versions, check if it's enabled.
116+
: IsEnvVarEnabled(EnvOSR));
117+
}
71118

72119
/// <summary>
73120
/// The number of times a method must be called before it will be eligible for the next JIT tier.
@@ -81,16 +128,16 @@ private static int GetTieredCallCountThreshold()
81128
return 0;
82129
}
83130
// AggressiveTiering was added in .Net 5.
84-
if (Environment.Version.Major >= 5 && IsEnvVarEnabled(AggressiveTieringEnv))
131+
if (Environment.Version.Major >= 5 && IsEnvVarEnabled(EnvAggressiveTiering))
85132
{
86133
return 1;
87134
}
88-
if (TryParseEnvVar(CallCountThresholdEnv, out int callCountThreshold))
135+
if (TryParseEnvVar(EnvCallCountThreshold, out int callCountThreshold))
89136
{
90137
return callCountThreshold;
91138
}
92139
// CallCountThreshold was added as a knob in .Net 8.
93-
if (Environment.Version.Major >= 8 && TryParseKnob("System.Runtime.TieredCompilation.CallCountThreshold", out callCountThreshold))
140+
if (Environment.Version.Major >= 8 && TryParseKnob(KnobCallCountThreshold, out callCountThreshold))
94141
{
95142
return callCountThreshold;
96143
}
@@ -110,16 +157,16 @@ private static TimeSpan GetTieredDelay()
110157
return TimeSpan.Zero;
111158
}
112159
// AggressiveTiering was added in .Net 5.
113-
if (Environment.Version.Major >= 5 && IsEnvVarEnabled(AggressiveTieringEnv))
160+
if (Environment.Version.Major >= 5 && IsEnvVarEnabled(EnvAggressiveTiering))
114161
{
115162
return TimeSpan.Zero;
116163
}
117-
if (TryParseEnvVar(CallCountingDelayMsEnv, out int callCountDelay))
164+
if (TryParseEnvVar(EnvCallCountingDelayMs, out int callCountDelay))
118165
{
119166
return TimeSpan.FromMilliseconds(callCountDelay);
120167
}
121168
// CallCountingDelayMs was added as a knob in .Net 8.
122-
if (Environment.Version.Major >= 8 && TryParseKnob("System.Runtime.TieredCompilation.CallCountingDelayMs", out callCountDelay))
169+
if (Environment.Version.Major >= 8 && TryParseKnob(KnobCallCountingDelayMs, out callCountDelay))
123170
{
124171
return TimeSpan.FromMilliseconds(callCountDelay);
125172
}

0 commit comments

Comments
 (0)