Skip to content

Refactor engine JIT stage #2806

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/BenchmarkDotNet/Configs/ImmutableConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ internal ImmutableConfig(

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

internal bool HasDisassemblyDiagnoser() => diagnosers.OfType<DisassemblyDiagnoser>().Any();

public bool HasExtraStatsDiagnoser() => HasMemoryDiagnoser() || HasThreadingDiagnoser() || HasExceptionDiagnoser();

public IDiagnoser? GetCompositeDiagnoser(BenchmarkCase benchmarkCase, RunMode runMode)
Expand Down
210 changes: 92 additions & 118 deletions src/BenchmarkDotNet/Engines/Engine.cs

Large diffs are not rendered by default.

60 changes: 40 additions & 20 deletions src/BenchmarkDotNet/Engines/EngineActualStage.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using BenchmarkDotNet.Characteristics;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Mathematics;
using BenchmarkDotNet.Reports;
Expand All @@ -9,22 +8,31 @@

namespace BenchmarkDotNet.Engines
{
internal abstract class EngineActualStage(IterationMode iterationMode) : EngineStage(IterationStage.Actual, iterationMode)
internal abstract class EngineActualStage(IterationMode iterationMode, long invokeCount, int unrollFactor, EngineParameters parameters) : EngineStage(IterationStage.Actual, iterationMode, parameters)
{
internal const int MaxOverheadIterationCount = 20;

internal static EngineActualStage GetOverhead(IEngine engine)
=> new EngineActualStageAuto(engine.TargetJob, engine.Resolver, IterationMode.Overhead);
internal readonly long invokeCount = invokeCount;
internal readonly int unrollFactor = unrollFactor;

internal static EngineActualStage GetWorkload(IEngine engine, RunStrategy strategy)
internal static EngineActualStage GetOverhead(long invokeCount, int unrollFactor, EngineParameters parameters)
=> new EngineActualStageAuto(IterationMode.Overhead, invokeCount, unrollFactor, parameters);

internal static EngineActualStage GetWorkload(RunStrategy strategy, long invokeCount, int unrollFactor, EngineParameters parameters)
{
var targetJob = engine.TargetJob;
var targetJob = parameters.TargetJob;
int? iterationCount = targetJob.ResolveValueAsNullable(RunMode.IterationCountCharacteristic);
const int DefaultWorkloadCount = 10;
return iterationCount == null && strategy != RunStrategy.Monitoring
? new EngineActualStageAuto(targetJob, engine.Resolver, IterationMode.Workload)
: new EngineActualStageSpecific(iterationCount ?? DefaultWorkloadCount, IterationMode.Workload);
? new EngineActualStageAuto(IterationMode.Workload, invokeCount, unrollFactor, parameters)
: new EngineActualStageSpecific(iterationCount ?? DefaultWorkloadCount, IterationMode.Workload, invokeCount, unrollFactor, parameters);
}

protected IterationData GetIterationData()
=> new(Mode, Stage, ++iterationIndex, invokeCount, unrollFactor, parameters.IterationSetupAction, parameters.IterationCleanupAction,
Mode == IterationMode.Workload
? unrollFactor == 1 ? parameters.WorkloadActionNoUnroll : parameters.WorkloadActionUnroll
: unrollFactor == 1 ? parameters.OverheadActionNoUnroll : parameters.OverheadActionUnroll);
}

internal sealed class EngineActualStageAuto : EngineActualStage
Expand All @@ -37,22 +45,23 @@ internal sealed class EngineActualStageAuto : EngineActualStage
private readonly List<Measurement> measurementsForStatistics;
private int iterationCounter = 0;

public EngineActualStageAuto(Job targetJob, IResolver resolver, IterationMode iterationMode) : base(iterationMode)
public EngineActualStageAuto(IterationMode iterationMode, long invokeCount, int unrollFactor, EngineParameters parameters) : base(iterationMode, invokeCount, unrollFactor, parameters)
{
maxRelativeError = targetJob.ResolveValue(AccuracyMode.MaxRelativeErrorCharacteristic, resolver);
maxAbsoluteError = targetJob.ResolveValueAsNullable(AccuracyMode.MaxAbsoluteErrorCharacteristic);
outlierMode = targetJob.ResolveValue(AccuracyMode.OutlierModeCharacteristic, resolver);
minIterationCount = targetJob.ResolveValue(RunMode.MinIterationCountCharacteristic, resolver);
maxIterationCount = targetJob.ResolveValue(RunMode.MaxIterationCountCharacteristic, resolver);
maxRelativeError = parameters.TargetJob.ResolveValue(AccuracyMode.MaxRelativeErrorCharacteristic, parameters.Resolver);
maxAbsoluteError = parameters.TargetJob.ResolveValueAsNullable(AccuracyMode.MaxAbsoluteErrorCharacteristic);
outlierMode = parameters.TargetJob.ResolveValue(AccuracyMode.OutlierModeCharacteristic, parameters.Resolver);
minIterationCount = parameters.TargetJob.ResolveValue(RunMode.MinIterationCountCharacteristic, parameters.Resolver);
maxIterationCount = parameters.TargetJob.ResolveValue(RunMode.MaxIterationCountCharacteristic, parameters.Resolver);
measurementsForStatistics = GetMeasurementList();
}

internal override List<Measurement> GetMeasurementList() => new(maxIterationCount);

internal override bool GetShouldRunIteration(List<Measurement> measurements, ref long invokeCount)
internal override bool GetShouldRunIteration(List<Measurement> measurements, out IterationData iterationData)
{
if (measurements.Count == 0)
{
iterationData = GetIterationData();
return true;
}

Expand All @@ -72,25 +81,36 @@ internal override bool GetShouldRunIteration(List<Measurement> measurements, ref

if (iterationCounter >= minIterationCount && actualError < maxError)
{
iterationData = default;
return false;
}

if (iterationCounter >= maxIterationCount || isOverhead && iterationCounter >= MaxOverheadIterationCount)
{
iterationData = default;
return false;
}

iterationData = GetIterationData();
return true;
}
}

internal sealed class EngineActualStageSpecific(int maxIterationCount, IterationMode iterationMode) : EngineActualStage(iterationMode)
internal sealed class EngineActualStageSpecific(int maxIterationCount, IterationMode iterationMode, long invokeCount, int unrollFactor, EngineParameters parameters)
: EngineActualStage(iterationMode, invokeCount, unrollFactor, parameters)
{
private int iterationCount = 0;

internal override List<Measurement> GetMeasurementList() => new(maxIterationCount);

internal override bool GetShouldRunIteration(List<Measurement> measurements, ref long invokeCount)
=> ++iterationCount <= maxIterationCount;
internal override bool GetShouldRunIteration(List<Measurement> measurements, out IterationData iterationData)
{
if (iterationIndex < maxIterationCount)
{
iterationData = GetIterationData();
return true;
}

iterationData = default;
return false;
}
}
}
123 changes: 4 additions & 119 deletions src/BenchmarkDotNet/Engines/EngineFactory.cs
Original file line number Diff line number Diff line change
@@ -1,130 +1,15 @@
using System;
using BenchmarkDotNet.Jobs;
using Perfolizer.Horology;

namespace BenchmarkDotNet.Engines
{
public class EngineFactory : IEngineFactory
{
public IEngine CreateReadyToRun(EngineParameters engineParameters)
{
if (engineParameters.WorkloadActionNoUnroll == null)
throw new ArgumentNullException(nameof(engineParameters.WorkloadActionNoUnroll));
if (engineParameters.WorkloadActionUnroll == null)
throw new ArgumentNullException(nameof(engineParameters.WorkloadActionUnroll));
if (engineParameters.Dummy1Action == null)
throw new ArgumentNullException(nameof(engineParameters.Dummy1Action));
if (engineParameters.Dummy2Action == null)
throw new ArgumentNullException(nameof(engineParameters.Dummy2Action));
if (engineParameters.Dummy3Action == null)
throw new ArgumentNullException(nameof(engineParameters.Dummy3Action));
if (engineParameters.OverheadActionNoUnroll == null)
throw new ArgumentNullException(nameof(engineParameters.OverheadActionNoUnroll));
if (engineParameters.OverheadActionUnroll == null)
throw new ArgumentNullException(nameof(engineParameters.OverheadActionUnroll));
if (engineParameters.TargetJob == null)
throw new ArgumentNullException(nameof(engineParameters.TargetJob));

engineParameters.GlobalSetupAction?.Invoke(); // whatever the settings are, we MUST call global setup here, the global cleanup is part of Engine's Dispose

if (!engineParameters.NeedsJitting) // just create the engine, do NOT jit
return CreateMultiActionEngine(engineParameters);

int jitIndex = 0;

if (engineParameters.HasInvocationCount || engineParameters.HasUnrollFactor) // it's a job with explicit configuration, just create the engine and jit it
{
var warmedUpMultiActionEngine = CreateMultiActionEngine(engineParameters);

DeadCodeEliminationHelper.KeepAliveWithoutBoxing(Jit(warmedUpMultiActionEngine, ++jitIndex, invokeCount: engineParameters.UnrollFactor, unrollFactor: engineParameters.UnrollFactor));

return warmedUpMultiActionEngine;
}

var singleActionEngine = CreateSingleActionEngine(engineParameters);
var singleInvocationTime = Jit(singleActionEngine, ++jitIndex, invokeCount: 1, unrollFactor: 1);
double timesPerIteration = engineParameters.IterationTime / singleInvocationTime; // how many times can we run given benchmark per iteration

if ((timesPerIteration < 1.5) && (singleInvocationTime < TimeInterval.FromSeconds(10.0)))
{
// if the Jitting took more than IterationTime/1.5 but still less than 10s (a magic number based on observations of reported bugs)
// we call it one more time to see if Jitting itself has not dominated the first invocation
// if it did, it should NOT be a single invocation engine (see #837, #1337, #1338, and #1780)
singleInvocationTime = Jit(singleActionEngine, ++jitIndex, invokeCount: 1, unrollFactor: 1);
timesPerIteration = engineParameters.IterationTime / singleInvocationTime;
}

// executing once takes longer than iteration time => long running benchmark, needs no pilot and no overhead
// Or executing twice would put us well past the iteration time ==> needs no pilot and no overhead
if (timesPerIteration < 1.5)
return singleActionEngine;

int defaultUnrollFactor = Job.Default.ResolveValue(RunMode.UnrollFactorCharacteristic, EngineParameters.DefaultResolver);
int roundedUpTimesPerIteration = (int)Math.Ceiling(timesPerIteration);

if (roundedUpTimesPerIteration < defaultUnrollFactor) // if we run it defaultUnrollFactor times per iteration, it's going to take longer than IterationTime
{
var needsPilot = engineParameters.TargetJob
.WithUnrollFactor(1) // we don't want to use unroll factor!
.WithMinInvokeCount(2) // the minimum is 2 (not the default 4 which can be too much and not 1 which we already know is not enough)
.WithEvaluateOverhead(false); // it's something very time consuming, it overhead is too small compared to total time

return CreateEngine(engineParameters, needsPilot, engineParameters.OverheadActionNoUnroll, engineParameters.WorkloadActionNoUnroll);
}

var multiActionEngine = CreateMultiActionEngine(engineParameters);

DeadCodeEliminationHelper.KeepAliveWithoutBoxing(Jit(multiActionEngine, ++jitIndex, invokeCount: defaultUnrollFactor, unrollFactor: defaultUnrollFactor));

return multiActionEngine;
}

/// <returns>the time it took to run the benchmark</returns>
private static TimeInterval Jit(Engine engine, int jitIndex, int invokeCount, int unrollFactor)
{
engine.Dummy1Action.Invoke();

DeadCodeEliminationHelper.KeepAliveWithoutBoxing(engine.RunIteration(new IterationData(IterationMode.Overhead, IterationStage.Jitting, jitIndex, invokeCount, unrollFactor))); // don't forget to JIT idle
var engine = new Engine(engineParameters);

engine.Dummy2Action.Invoke();
// TODO: Move GlobalSetup/Cleanup to Engine.Run.
engine.Parameters.GlobalSetupAction.Invoke(); // whatever the settings are, we MUST call global setup here, the global cleanup is part of Engine's Dispose

var result = engine.RunIteration(new IterationData(IterationMode.Workload, IterationStage.Jitting, jitIndex, invokeCount, unrollFactor));

engine.Dummy3Action.Invoke();

engine.WriteLine();

return TimeInterval.FromNanoseconds(result.Nanoseconds);
return engine;
}

private static Engine CreateMultiActionEngine(EngineParameters engineParameters)
=> CreateEngine(engineParameters, engineParameters.TargetJob, engineParameters.OverheadActionUnroll, engineParameters.WorkloadActionUnroll);

private static Engine CreateSingleActionEngine(EngineParameters engineParameters)
=> CreateEngine(engineParameters,
engineParameters.TargetJob
.WithInvocationCount(1).WithUnrollFactor(1) // run the benchmark exactly once per iteration
.WithEvaluateOverhead(false), // it's something very time consuming, it overhead is too small compared to total time
// todo: consider if we should set the warmup count to 2
engineParameters.OverheadActionNoUnroll,
engineParameters.WorkloadActionNoUnroll);

private static Engine CreateEngine(EngineParameters engineParameters, Job job, Action<long> idle, Action<long> main)
=> new Engine(
engineParameters.Host,
EngineParameters.DefaultResolver,
engineParameters.Dummy1Action,
engineParameters.Dummy2Action,
engineParameters.Dummy3Action,
idle,
main,
job,
engineParameters.GlobalSetupAction,
engineParameters.GlobalCleanupAction,
engineParameters.IterationSetupAction,
engineParameters.IterationCleanupAction,
engineParameters.OperationsPerInvoke,
engineParameters.MeasureExtraStats,
engineParameters.BenchmarkName);
}
}
Loading
Loading