4 min readRunning it for real

Cutting the noise at its source

Reducing environmental interference using CPU affinity, process priority, and thread control for maximum measurement precision.

If you've ever run the same benchmark twice and gotten two different results, you've experienced the "noisy neighbor" problem.

Your code isn't running in a vacuum. It's running on a machine where the OS is constantly stealing cycles to handle network interrupts, background telemetry, and a dozen open browser tabs. While NBenchmark's outlier machinery treats the symptoms of this noise by trimming spikes, the most precise benchmarks start by preventing the noise from reaching the sample stream in the first place.

The shared-runner confession

The first rule of performance engineering is that your laptop is not a measurement instrument.

Laptops are designed for bursty, interactive workloads, not steady-state measurement. They have aggressive thermal throttling, variable clock speeds, and background processes that wake up without warning. This is exacerbated in CI environments, where "shared runners" mean your benchmark is competing for L3 cache with three other unrelated build jobs on the same physical core.

If you want a number you can trust, you have to take control of the environment.

CPU Affinity: Pinning your work

The OS scheduler frequently moves threads between different CPU cores to balance heat and load. This "core hopping" is a disaster for benchmarks because it flushes the L1 and L2 caches, creating artificial spikes in your timing data.

CPU affinity allows you to pin the worker process to a specific core. When NBenchmark uses affinity, it ensures that the measurement happens on one dedicated core, keeping the caches hot and the timing stable.

You can enable this via the --cpu-affinity CLI flag or the WithHardwareAffinity method. On a machine with 16 cores, pinning your benchmark to core 7 removes the variance caused by the scheduler shifting your workload across the die.

Process Priority and Thread Control

Even with a pinned core, a high-priority system process can still preempt your benchmark. To minimize this, NBenchmark can elevate the worker process's priority.

Using the --priority flag (or WithProcessPriority), the engine tells the OS that the worker process is a high-priority task. This reduces the likelihood of the OS pausing your measurement to handle a background update or a telemetry ping.

Alongside priority, NBenchmark employs Thread Control. By default, the engine optimizes how threads are placed and managed to ensure that the timed window is as undisturbed as possible. While you can disable this via --no-thread-control, it is almost always beneficial to leave it on.

Note for macOS users: Apple's kernel restricts many of these operations. While NBenchmark attempts to apply these optimizations, the macOS platform may ignore affinity and priority requests, meaning you will naturally see more variance on Mac hardware than on Linux or Windows.

Knowing when the machine is beyond saving

There are times when no amount of pinning or priority can save a run. If you are running on a severely overloaded machine, the noise floor becomes so high that the results are meaningless.

NBenchmark includes a host-quality check via the --host-quality-warnings flag (or WithHostQualityWarnings). This feature monitors the system for extreme interference during the run. If it detects that the host is too noisy to provide an honest number, it will surface a warning in your report.

When you see a host-quality warning, don't try to "fix" the benchmark code. Instead, look at the machine:

  • Close your browser and Slack.
  • Move the run to a dedicated performance machine.
  • In CI, switch from a shared runner to a dedicated, "bare metal" instance.

Reproducibility and the seed

Noise control isn't just about the OS; it's also about the order of execution. If you have ten benchmarks and the first one warms up the CPU's turbo boost, the tenth one will look faster simply because it ran last.

To combat this, NBenchmark uses --order random by default. By shuffling the order of benchmarks in every run, the engine ensures that any systemic bias (like thermal throttling or cache warming) is distributed across all candidates rather than favoring the end of the list.

If you need to reproduce a specific "weird" run for debugging, you can use the --seed flag. Providing a seed ensures that the "random" order is identical across runs, allowing you to isolate whether a spike is caused by the code or by its position in the suite.

The "in-process" trap

As a final word of caution: none of these environmental controls work if you use --in-process.

In-process measuring runs your code in the same process as the harness. This means you share the heap, the JIT state, and the priority of the main application. As we'll see in post 18, this can lead to results that are off by a factor of 20x or more. In-process runs are excellent for smoke tests, but they are the opposite of noise control.


Go deeper: You've now learned how to control the environment and the hardware. But what happens when you've done everything right and the numbers still look impossible?

When the numbers look wrong →