Skip to content

Troubleshooting

This page maps symptoms you may see in benchmark output to their likely causes and the specific NBenchmark configuration that addresses them.

Measurement variability

SymptomLikely causeConfiguration fix
Large Error (wide CI)Genuinely variable timings (auto-sampling already hit its sample ceiling or time cap)Demand a tighter target: .WithAutoTune(AutoTunePreset.Thorough) or --ci-target 0.01. Raise --max-samples / --max-tuning-time if the loop is stopping on a cap - see Configuration: AutoTune
Large Error (wide CI)OS scheduling / context-switch noiseSwitch outlier mode to .WithOutlierMode(OutlierMode.IqrFence) - see Configuration
Large Error (wide CI)Thermal throttling on laptopsPin a longer warmup with .WithWarmup(50) to let the CPU stabilise. Run plugged in. - see Configuration
Same benchmark, a different median each run — and every run reports a tight ErrorWarmup ended before the JIT finished tiering the body up, so the run measured pre-tier-1 (unoptimized) code. This is not noise: each run is internally consistent, which is why the error margin looks trustworthy. A body can read several times slow this wayCheck autoTune.warmupTimeFloorMet in the JSON (or warmup cut short on the console summary) and autoTune.splitHalfDrift. Raise --min-warmup-time (default 250 ms), and --max-warmup if the ceiling is what cut warmup short. For a nanosecond body with --ops-per-sample 1, raise the ops-per-sample so each sample spans more work - see Measurement: Warmup
Result reports a driftUnresolved stopThe measured timings kept moving while they were being collected, so the interval describes a moving target. Usually a JIT tier-up or dynamic-PGO re-optimization landing inside measurement; otherwise a thermal ramp, a filling cache, or a growing data structureRaise --min-warmup-time so the transition lands during warmup instead. If the body is genuinely non-stationary, that is the finding - use --launch-count 5 to measure across-launch spread, which is the honest signal - see Measurement
Sample count varies between runsAuto-sampling working as designed - each run collects exactly enough samples to hit the CI targetExpected. Pin .WithIterations(n) / --iterations n for a fixed, reproducible sample count (e.g. in CI) - see Configuration: Iterations
High StdDevGC pressure or allocation noiseEnable allocation tracking with .WithAllocations() to diagnose - see Configuration. Under the default Realistic profile, natural GC pauses are included in the timing; switch to the Independent profile (--profile independent) to force per-iteration GC and isolate iterations from GC noise - see Measurement Profiles

Quick reference: Outlier modes

ModeWhen to use
IqrFence (default)General-purpose. The IQR-based fence adapts to your data's spread, trimming spikes from OS scheduling interrupts without discarding clean samples.
RemoveTop5PercentWhen you want a fixed quota - always removes the slowest 5% of iterations.
RemoveTopAndBottom5PercentWhen very fast outliers (e.g. cache hits after warmup) also skew results.
NoneWhen every sample matters (latency-tail analysis).

Zero or unexpected results

SymptomLikely causeConfiguration fix
Result shows 0 nsDead code elimination - the compiler removed your benchmark body because it has no observable side effectsUse the Func<T> overload that returns a value, or add a side effect. See FAQ: 0 ns
All results zeroedDry-run mode active (--dry-run, Iterations=0, WarmupIterations=0)Remove --dry-run flag or set Iterations > 0
MarginOfError is ±0 nsOnly one sample (n < 2, from a pinned Iterations = 1) or all measurements identical (timer resolution coarser than the benchmark duration)Unpin Iterations to use auto mode (collects at least AutoTune.MinSamples), or pin a larger count. For a fast body, auto ops-per-sample calibration amortises a coarse timer - note it is skipped when setup/teardown is set
Sig column is blankToo few samples for the Mann-Whitney U test (requires ≥2 per group), or the Kruskal-Wallis omnibus was not significant (three-plus benchmarks compared, no post-hoc ran)Increase iterations or combine more runs - see FAQ: significance

Discovery and setup errors

SymptomLikely causeConfiguration fix
[Benchmark] method not discoveredMethod is static, class is abstract, or assembly not registeredUse --list to verify what the host finds - see Harness mode: listing. Check the class is public, not abstract, and the method is an instance method
"Could not instantiate MyClass"No public parameterless constructorAdd one, use [BenchmarkSetup], or add NBenchmark.DependencyInjection - see FAQ: instantiation
Benchmarks run in different order each timeRandom order is the default (prevents systematic bias)Use --order declaration or .WithRunOrder(RunOrder.Declaration) for source order - see Configuration

Still stuck?

Released under the MIT License.