Skip to content

Commit 178b6ad

Browse files
authored
Don't run the benchmark once per iteration if only the first ivocation lasts longer than IterationTime (#1573)
* add the test * if the Jitting took more than IterationTime but still less than 1s (a magic number based on observations of the reported bug) we call it one more time to see if Jitting itself has not dominated the first invocation if it did, it shoud NOT be a single invocation engine (fixes #837, fixes #1337 and fixes #1338)
1 parent c209b12 commit 178b6ad

File tree

3 files changed

+56
-5
lines changed

3 files changed

+56
-5
lines changed

src/BenchmarkDotNet/Engines/EngineFactory.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ public IEngine CreateReadyToRun(EngineParameters engineParameters)
4444
var singleActionEngine = CreateSingleActionEngine(engineParameters);
4545
var singleInvocationTime = Jit(singleActionEngine, ++jitIndex, invokeCount: 1, unrollFactor: 1);
4646

47+
if (singleInvocationTime > engineParameters.IterationTime && singleInvocationTime < TimeInterval.FromSeconds(1.0))
48+
{
49+
// if the Jitting took more than IterationTime but still less than 1s (a magic number based on observations of the reported bug)
50+
// we call it one more time to see if Jitting itself has not dominated the first invocation
51+
// if it did, it shoud NOT be a single invocation engine (see #837, #1337 and #1338)
52+
singleInvocationTime = Jit(singleActionEngine, ++jitIndex, invokeCount: 1, unrollFactor: 1);
53+
}
54+
4755
if (singleInvocationTime > engineParameters.IterationTime)
4856
return singleActionEngine; // executing once takes longer than iteration time => long running benchmark, needs no pilot and no overhead
4957

src/BenchmarkDotNet/Engines/EngineResolver.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class EngineResolver : Resolver
1010
{
1111
internal const int DefaultMinWorkloadIterationCount = 15;
1212
internal const int DefaultMaxWorkloadIterationCount = 100;
13+
internal const int DefaultIterationTime = 500;
1314

1415
internal const int ForceAutoWarmup = -1;
1516
internal const int DefaultMinWarmupIterationCount = 6;
@@ -20,7 +21,7 @@ public class EngineResolver : Resolver
2021
private EngineResolver()
2122
{
2223
Register(RunMode.RunStrategyCharacteristic, () => RunStrategy.Throughput);
23-
Register(RunMode.IterationTimeCharacteristic, () => TimeInterval.Millisecond * 500);
24+
Register(RunMode.IterationTimeCharacteristic, () => TimeInterval.Millisecond * DefaultIterationTime);
2425

2526
Register(RunMode.MinIterationCountCharacteristic, () => DefaultMinWorkloadIterationCount);
2627
Register(RunMode.MaxIterationCountCharacteristic, () => DefaultMaxWorkloadIterationCount);

tests/BenchmarkDotNet.Tests/Engine/EngineFactoryTests.cs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using BenchmarkDotNet.Jobs;
88
using BenchmarkDotNet.Running;
99
using JetBrains.Annotations;
10+
using Perfolizer.Horology;
1011
using Xunit;
1112

1213
namespace BenchmarkDotNet.Tests.Engine
@@ -33,6 +34,14 @@ private void VeryTimeConsumingSingle(long _)
3334
Thread.Sleep(IterationTime);
3435
}
3536

37+
private void TimeConsumingOnlyForTheFirstCall(long _)
38+
{
39+
if (timesBenchmarkCalled++ == 0)
40+
{
41+
Thread.Sleep(IterationTime);
42+
}
43+
}
44+
3645
private void InstantNoUnroll(long invocationCount) => timesBenchmarkCalled += (int) invocationCount;
3746
private void InstantUnroll(long _) => timesBenchmarkCalled += 16;
3847

@@ -78,10 +87,10 @@ public void ForDefaultSettingsVeryTimeConsumingBenchmarksAreExecutedOncePerItera
7887
var engine = new EngineFactory().CreateReadyToRun(engineParameters);
7988

8089
Assert.Equal(1, timesGlobalSetupCalled);
81-
Assert.Equal(1, timesIterationSetupCalled); // 1x for Target
82-
Assert.Equal(1, timesBenchmarkCalled);
83-
Assert.Equal(1, timesOverheadCalled);
84-
Assert.Equal(1, timesIterationCleanupCalled); // 1x for Target
90+
Assert.Equal(2, timesIterationSetupCalled); // 2x for Target
91+
Assert.Equal(2, timesBenchmarkCalled);
92+
Assert.Equal(2, timesOverheadCalled);
93+
Assert.Equal(2, timesIterationCleanupCalled); // 2x for Target
8594
Assert.Equal(0, timesGlobalCleanupCalled); // cleanup is called as part of dispose
8695

8796
Assert.Equal(1, engine.TargetJob.Run.InvocationCount); // call the benchmark once per iteration
@@ -95,6 +104,39 @@ public void ForDefaultSettingsVeryTimeConsumingBenchmarksAreExecutedOncePerItera
95104
Assert.Equal(1, timesGlobalCleanupCalled);
96105
}
97106

107+
[Theory]
108+
[InlineData(120)] // 120 ms as in the bug report
109+
[InlineData(250)] // 250 ms as configured in dotnet/performance repo
110+
[InlineData(EngineResolver.DefaultIterationTime)] // 500 ms - the default BDN setting
111+
public void BenchmarksThatRunLongerThanIterationTimeOnlyDuringFirstInvocationAreNotInvokedOncePerIteration(int iterationTime)
112+
{
113+
var engineParameters = CreateEngineParameters(
114+
mainNoUnroll: TimeConsumingOnlyForTheFirstCall,
115+
mainUnroll: InstantUnroll,
116+
job: Job.Default.WithIterationTime(TimeInterval.FromMilliseconds(iterationTime)));
117+
118+
var engine = new EngineFactory().CreateReadyToRun(engineParameters);
119+
120+
Assert.Equal(1, timesGlobalSetupCalled);
121+
// the factory should call the benchmark:
122+
// 1st time with unroll factor to JIT the code
123+
// one more to check that the Jitting has not dominated the reported time
124+
// and one more time to JIT the 16 unroll factor case as it turned out that Jitting has dominated the time
125+
Assert.Equal(1 + 1 + 1, timesIterationSetupCalled);
126+
Assert.Equal(1 + 1 + 16, timesBenchmarkCalled);
127+
Assert.Equal(1 + 1 + 16, timesOverheadCalled);
128+
Assert.Equal(1 + 1 + 1, timesIterationCleanupCalled); // 2x for Target
129+
Assert.Equal(0, timesGlobalCleanupCalled); // cleanup is called as part of dispose
130+
131+
Assert.False(engine.TargetJob.Run.HasValue(RunMode.InvocationCountCharacteristic)); // we need pilot stage
132+
133+
Assert.False(engine.TargetJob.Run.HasValue(AccuracyMode.EvaluateOverheadCharacteristic));
134+
135+
engine.Dispose(); // cleanup is called as part of dispose
136+
137+
Assert.Equal(1, timesGlobalCleanupCalled);
138+
}
139+
98140
[Fact]
99141
public void ForJobsWithExplicitUnrollFactorTheGlobalSetupIsCalledAndMultiActionCodeGetsJitted()
100142
=> AssertGlobalSetupWasCalledAndMultiActionGotJitted(Job.Default.WithUnrollFactor(16));

0 commit comments

Comments
 (0)