5 min readStart here

The reviewer that ships with the compiler

Learn how NBenchmark's built-in analyzers catch common benchmarking mistakes at build time.

You can write a benchmark that compiles perfectly, runs without errors, and produces a tight confidence interval, yet is fundamentally wrong.

The most common mistakes - throwaway bodies, static methods, or shared state - don't trigger exceptions. They just silently invalidate your results. NBenchmark solves this by shipping a set of Roslyn analyzers (NB0001-NB0014) that act as a continuous peer review for your performance tests.

The honesty family: Preventing silent lies

The most dangerous benchmarks are those that measure nothing. If the JIT compiler realizes a method's result isn't used, it may eliminate the body entirely. You'll see an incredibly fast median, but you're measuring the cost of a no-op.

NB0004 and NB0005 catch these "empty" benchmarks. NB0004 flags methods with no observable side effects, while NB0005 flags literally empty bodies.

Then there is the lambda trap. In Single mode, it is tempting to pass an Action lambda to Benchmark.Run. However, if that lambda doesn't return a value, the runner cannot easily sink the result to defeat dead-code elimination. NB0010 warns you about this.

The fix for all three is the same: return a value.

// ❌ NB0005: Empty body
[Benchmark]
public void DoNothing() { }

// ❌ NB0004: No observable side effects (result ignored)
[Benchmark]
public void SumIgnored() 
{
    var x = 1 + 1;
}

// ✅ Fixed: Return the value so NBenchmark can sink it
[Benchmark]
public int SumHonest() => 1 + 1;

The discovery family: Ensuring the engine can run

NBenchmark's Harness mode uses reflection to discover and instantiate your benchmark classes. If the class doesn't follow the required contract, the run will fail at runtime. The analyzers move these failures to build time.

NB0001 ensures your class has a public parameterless constructor. NB0002 prevents [Benchmark] from being placed on static methods, as the engine expects instance-based execution for state management.

If you use [Arguments] or [ArgumentsSource], NB0003 verifies that the number of arguments provided matches the method's parameter count (arity). Other guards include NB0006 (preventing multiple baselines in one class) and NB0007 (detecting duplicate lifecycle attributes).

public class MyBenchmarks
{
    // ❌ NB0002: Benchmarks cannot be static
    [Benchmark]
    public static void StaticMethod() { }

    // ❌ NB0003: Argument arity mismatch (expected 1, got 2)
    [Benchmark]
    [Arguments(10, 20)] 
    public void Process(int size) { }

    // ✅ Fixed
    [Benchmark]
    [Arguments(10)]
    public void ProcessFixed(int size) { }
}

The range family: Sanity checking the knobs

Configuring samples and warmup counts is usually automatic, but when you pin them manually, it is easy to provide a value that is either too low to be statistically significant or high enough to hang your CI pipeline.

NB0008 and NB0009 monitor these values. If you set Samples to 1 or a WarmupSamples count that exceeds reasonable bounds, the analyzer flags it.

The contamination family: Guarding state independence

Benchmark results are only valid if each sample is independent. If a benchmark modifies an instance field that is shared across samples, the second sample is measuring a different state than the first.

NB0011 and NB0013 target these "contaminated" benchmarks. They flag the use of mutable instance fields or scoped services when the class is marked with InstanceLifetime.PerClass. The full story on managing expensive state is covered in post 19.

Finally, NB0014 flags when a benchmark body captures local state. This is an Info severity notification rather than an error. Because NBenchmark runs benchmarks in isolated worker processes, any captured state must be serialized and transferred across a process boundary. If you need complex setup that cannot be easily transferred, you should use [BenchmarkPlan].

public class StateBenchmark
{
    private int _counter = 0;

    // ❌ NB0013: Mutable field modified under PerClass lifetime
    [Benchmark]
    public void Increment() => _counter++; 
}

Managing severities in CI

Analyzers use standard Roslyn severities:

  • Error: Blocks the build. These are non-negotiable honesty or discovery failures.
  • Warning: Notifies you of a likely mistake.
  • Info: Provides a hint about how the engine is handling your code.

If you encounter a false positive - or if you have a highly specific reason to bypass a guard - you can suppress the warning using #pragma warning disable or a .editorconfig file. In CI environments, it is recommended to treat warnings as errors to ensure no "silent lies" reach your main branch.

For more information on troubleshooting your benchmarks, see the Troubleshooting guide.