Website review
This review may include AI-generated content and is not individually verified. Domain, certificate and other facts come from the observed data on this page.
What is nanobench?
nanobench is a single-header C++ microbenchmarking library. You include it in your project, write a small harness around the code you want to measure, and it runs that code repeatedly while timing each round and reporting statistics such as time per unit, error, and optionally CPU counters like instructions, cycles, and branch misses.
What it is for
It is aimed at C++ developers who want quick, repeatable measurements of small code paths — a function, an algorithm, a container operation — without pulling in a large framework. The page evidence shows the intended workflow: install by direct inclusion or CMake integration, then use ankerl::nanobench::Bench with run(), title(), name(), and unit() to describe each benchmark, and output() or render() to format results.
Things it does beyond plain timing
- Comparison mode:
compare()andCompareResultlet you check whether one implementation is meaningfully faster than another rather than eyeballing two numbers. - Stability handling: the API exposes
warmup(),epochs(),minEpochTime(), and related knobs, and the documentation discusses unstable results and serial correlation — a sign the library expects you to think about measurement noise, not just call a timer. - Complexity annotations:
complexityN()andcomplexityBigO()support fitting results to a growth curve. - Output formats: table columns, CSV, JSON, HTML box plots, and pyperf-compatible output, plus Mustache-like templates for custom rendering.
- A bundled fast PRNG (
Rng) for benchmarks that need random input.
Who should reach for it
A concrete case: you are choosing between two implementations of a hot function and want a defensible answer. You write one Bench with two run() calls under the same title, use compare(), and let the reported error tell you whether the difference is real. If you only need a rough one-off number, a stopwatch-style loop is less setup; nanobench pays off when you need statistics, comparisons, or machine-readable output.
As a next step, read the "Designing the experiment" and "Reducing a round to one number" sections before trusting any result — the library gives you the tools, but the measurement discipline is yours. The project lives at nanobench.
How do I integrate nanobench into my project with CMake or direct inclusion?
nanobench is a single-header C++ microbenchmarking library, so you can either drop the header directly into your project or wire it in through CMake. Both paths use the same #include and the same ankerl::nanobench::Bench API; the difference is only in how the header reaches your compiler.
Direct inclusion
Copy nanobench.h (and its companion nanobench.cpp, if your version ships one) into your source tree, then include it by relative path:
#include "nanobench.h"
int main() {
ankerl::nanobench::Bench b;
b.title("my benchmark").unit("op");
b.run("work", [] { /* code under test */ });
b.render(ankerl::nanobench::templates::markdown());
}
This is the quickest route for a single experiment or a throwaway harness. The trade-off is that the header lives in your repo and you update it by hand.
CMake integration
Add the nanobench directory as a subdirectory and link the target the library exports (the documentation lists CMake Integration as a first-class option):
add_subdirectory(third_party/nanobench)
target_link_libraries(my_bench PRIVATE nanobench)
Then #include <nanobench.h> in your code. Linking the target keeps include paths and any compile definitions consistent across your targets, which matters once more than one executable or test binary uses the library.
Which to choose
- One benchmark file, one machine: direct inclusion is less setup.
- Multiple targets, CI, or shared team repo: use the CMake target so version and flags stay in one place.
- Vendored dependency policy: if you already vendor headers, direct inclusion fits; if you pin dependencies via CMake, prefer the subdirectory route.
A practical next step
Start with direct inclusion to confirm the API compiles and produces output, then convert to add_subdirectory + target_link_libraries when you commit the benchmark. Keep the benchmark in its own executable rather than mixing it into your production binary, so optimization flags and runtime cost stay separate. The official docs at nanobench cover the full Bench reference and the output templates (csv, json, htmlBoxplot, pyperf) you will likely want once the harness runs.
How do I interpret nanobench's output columns and comparison results?
nanobench's output is a table of measurement columns per benchmark, and its comparison results tell you whether the difference between two variants is statistically meaningful. Read the columns first to understand what was measured, then read the comparison to decide whether the difference matters.
Reading the output columns
Each row is one run() (or one entry in a compare()). The core columns:
relative— the row's time relative to the fastest row in the same run, so the quickest benchmark is1.00and slower ones are multiples of it. This is the fastest way to see the shape of your results.timePerUnit— elapsed time divided bybatch()×unit(). If you didn't set a unit, treat it as time per iteration.unitPerSecond— the reciprocal, useful when you think in throughput (items/sec, ops/sec).error— nanobench's estimated uncertainty for that row. A largeerrormeans the number is noisy; don't draw conclusions from it until you understand why.instructions,cycles,ipc,branches,branchMisses— hardware counters, only present when you enableperformanceCounters().ipcandbranchMissesare the most diagnostic: low IPC suggests stalls (cache misses, dependencies); high branch misses suggest unpredictable control flow.total— total time for the whole run, including untimed setup excluded viasetup().
Columns are configurable: hideColumn()/showColumn()/isColumnVisible() control which appear, and contextColumn() turns a context() value into its own column — handy when you sweep a parameter (e.g. input size) and want one table with a column per value.
Reading comparison results
compare() returns a CompareResult with size(), operator[], rounds(), comparisons(), isSignificant() and fastest(). The practical reading:
- Check
isSignificant()before believing any ordering. If the difference isn't significant, treat the two variants as tied and choose on other grounds (readability, maintainability). - Use
fastest()to get the winning entry, but only after step 1. - Look at
rounds()/comparisons()to see how much evidence backed the verdict — more rounds with a stable result is stronger.
A concrete scenario
You refactor a hot loop and want to know if it's actually faster. Run both versions with compare(), set epochs() high enough that error is small relative to the effect you expect, and enable performanceCounters() if available. If isSignificant() is false and error is comparable to the gap, the refactor is a wash performance-wise — keep it only if it's clearer. If it's significant, check ipc and branchMisses to see why: a jump in IPC with fewer branch misses confirms the loop got more predictable, which is a more durable win than a lucky measurement.
Decision criteria
- Noisy
error? Increaseepochs()orminEpochTime(); check for unstable inputs before trusting anything. - Tiny but significant difference? Ask whether it's above your noise floor in real use; sub-percent wins rarely survive integration.
- Comparing across machines or compilers? Re-run the comparison on each;
relativeis only meaningful within one run.
For a broader comparison against other microbenchmark harnesses (Google Benchmark, nonius, Catch2's benchmark support), see GitHub repositories for each project's own results.
When should I use nanobench instead of Google Benchmark or Catch2?
Use nanobench when you want a single-header C++ microbenchmarking library that is quick to drop into a project and produces human-readable tables plus exportable results (CSV, JSON, HTML box plots). Its documentation describes a Mustache-like templating system, per-benchmark context columns, comparison of alternatives, and complexity estimation. That makes it a good fit for developers who want low-friction benchmarking inside an existing build, especially for small, focused experiments and quick comparisons.
Where each tool tends to fit
| Situation | nanobench | Google Benchmark | Catch2 |
|---|---|---|---|
| Adding benchmarks with minimal setup | Direct inclusion or CMake; single header emphasized in its docs | Typically a library dependency with its own build integration | Primarily a test framework; benchmarking is an add-on via its benchmark support |
| Comparing several implementations side by side | Built-in comparison support and comparison output | Supports comparisons, often via manual setup or fixtures | Less central; you usually structure it yourself |
| Reporting and export | CSV, JSON, HTML box plots, pyperf output | JSON and console output; rich ecosystem | Test-style output; not benchmark-report oriented |
| Estimating algorithmic complexity | Complexity helpers (complexityN, complexityBigO) |
Possible but not the primary focus | Not a typical use |
| Using one framework for tests and benchmarks | Benchmarks only | Benchmarks only | Tests and simple benchmarks together |
Concrete scenario
Suppose you are choosing between two string-splitting routines in a C++ service. With nanobench you can write one Bench object, run both functions with run(), and use compare() to see which is faster while the table shows time per unit and error. If you also want to see how cost scales with input size, the complexity helpers give you a Big-O estimate. If your team already runs Google Benchmark in CI and consumes its JSON, staying there avoids splitting your measurement tooling. If you mainly need assertions and a rough timing check, Catch2 keeps tests and benchmarks in one place.
Decision criterion
Pick nanobench for lightweight, self-contained microbenchmarks where readable tables and flexible export matter. Pick Google Benchmark when you need a mature benchmarking ecosystem and established CI reporting. Pick Catch2 when benchmarking is secondary to unit testing and you value having both in one framework. A useful next step is to write the same small benchmark in two candidates and compare the output format and setup effort against your team's reporting needs.
See nanobench, Google Benchmark and Catch2 for their official documentation.
How do I export nanobench results to CSV, JSON, or HTML box plots for reporting?
Use nanobench's built-in render() templates to export results: nanobench::templates::csv, nanobench::templates::json, and nanobench::templates::htmlBoxplot turn a Bench object's results into text you can write to a file. CSV and JSON are best for archiving or feeding dashboards; the HTML box plot is best for a report where a reader needs to see spread, not just a single number.
Typical flow
- Declare a
Bench, run one or more cases withrun(), and keep the object alive. - Choose a template and call
render()against the results, writing the returned text to a.csv,.json, or.htmlfile. - For comparisons, use
compare()so the output carries the comparison outcome (isSignificant(),fastest()) rather than a flat list of timings.
Choosing the format
| Format | Best for | Trade-off |
|---|---|---|
| CSV | Spreadsheet review, long-term archiving, custom plots | Loses the nested structure of comparison and context data |
| JSON | Scripts, CI artifacts, dashboards, downstream analysis | Needs a parser; not readable at a glance |
| HTML box plot | Human-facing reports and review meetings | Static file; not a substitute for raw data |
Practical example
A common setup: a CI job runs a benchmark, writes JSON as the artifact, and a small script extracts timePerUnit and error per case. For a pull-request summary, render the same results as an HTML box plot and attach it. CSV is the pragmatic middle ground when you want something a colleague can open without tooling.
Decision criteria
- Audience is a person skimming a report? HTML box plot.
- Audience is a script or dashboard? JSON.
- Audience is a spreadsheet user or a one-off comparison? CSV.
Two things shape how useful the export is. First, stability: unstable timings produce wide box plots and noisy error columns, so tune epochs and interval settings before exporting. Second, context: use context() and contextColumn() to attach parameters such as input size or compiler flags, otherwise the exported rows lose the information that makes them interpretable later.
How do I configure nanobench's epochs, warmup, and timing parameters for stable benchmarks?
Configure nanobench's timing controls through the ankerl::nanobench::Bench object before you call run(): use warmup(), epochs(), minEpochTime(), maxEpochTime(), minEpochIterations(), epochIterations(), clockResolutionMultiple() and targetIntervalWidth() to shape how each benchmark round is measured. The library's own documentation frames this as "designing the experiment" — the parameters exist so you can trade measurement time against stability, rather than there being one correct preset.
What each knob does in practice
| Setting | Practical effect | When to adjust it |
|---|---|---|
warmup() |
Runs the benchmark unmeasured before timing starts | Code with caches, lazy initialization, or first-call overhead |
epochs() |
Number of measured rounds; more rounds give a tighter estimate | Noisy results, or when you need a confidence-worthy comparison |
minEpochTime() / maxEpochTime() |
Bounds how long a single epoch runs | Very fast code needs a floor; slow code needs a ceiling |
minEpochIterations() / epochIterations() |
Guarantees a minimum number of inner iterations per epoch | Sub-nanosecond operations where the clock resolution dominates |
clockResolutionMultiple() |
Ensures each epoch spans a multiple of the clock's resolution | Tiny benchmarks where timer granularity causes jitter |
targetIntervalWidth() |
Target spread of measurements within a round | Tightening stability at the cost of runtime |
A concrete starting scenario
Suppose you are benchmarking a hash function that takes a few nanoseconds per call. Left at defaults, individual epochs may be too short for the clock to resolve, producing wildly varying numbers. A sensible adjustment is to raise minEpochIterations() so each epoch does enough work to be measurable, set minEpochTime() to something like a few milliseconds, and increase epochs() so the reported statistics have enough samples. Add warmup() if the function touches memory that benefits from cache priming.
Decision criteria
- Results look unstable (wide error, warning about stability): first increase
epochs(), thenminEpochTime(). The documentation notes that warmup alone is not the answer to serial correlation between measurements. - Benchmark runs too long: lower
epochs()ormaxEpochTime(), accepting a wider interval. - Comparing two implementations: use
compare()and checkisSignificant()on theCompareResultrather than eyeballing means; significance depends on having enough epochs. - Very fast operations: prefer iteration-count controls over time-based ones, since time-based bounds may be shorter than the clock can resolve.
Useful next step
Run the same benchmark twice with different epochs() values and compare the reported error and the significance verdict. If the verdict flips, your settings are too loose for the effect size you are trying to detect. For deeper guidance on experiment design, the project's documentation at nanobench covers the reasoning behind each parameter, and Google Benchmark offers a contrasting API if you want to cross-check methodology.
User reviews (0)