AutoBenchmark: Automatic multi-interval benchmarking in C++ using std::chrono
Problem:
Some part of your C++ code is suffering from performance issues. You are looking for a lightweight solution that allows you to easily record different time points and adaptively print the results (i.e. you don’t want to know something ran for 1102564643 nanoseconds, you just want to now that it took 1.102 seconds)
Solution
I wrote AutoBenchmark
so you can have the most hassle-free C++11 experience possible for your micro-benchmarking needs.
AutoBenchmark
allows you to record different points in time, each with a label. The first time point is recorded when this instance is constructed. AutoBenchmark supports an arbitrary number of time points.
When an instance of this class is destructed, it will automatically print all the benchmark results, but only if a configurable amount of time has passed since its construction - this is extremely handy especially if you have multiple exit points in your function that would otherwise require calling Print()
multiple times.
It allows you to ignore the benchmark when some performance goal is passed (e.g. if you have a for
loop that is slow only for some datapoints, you can configure AutoBenchmark
to print infos only for the slow runs).
The default behaviour (i.e. constructor with default parameters) is to disable automatic printing - in that case, you can call Print()
yourself.
Header (AutoBenchmark.hpp
):
/**
* AutoBenchmark v1.1
* Written by Uli Köhler
* https://techoverflow.net
*
* https://techoverflow.net/2018/03/31/autobenchmark-automatic-multi-interval-benchmarking-in-c-using-stdchrono/
*
* Published under CC0 1.0 Universal
*/
#pragma once
#include <chrono>
#include <string>
#include <limits>
#include <vector>
using namespace std;
/**
* Automatic benchmark: Allows you to record different points in time,
* each with a label. The first time point is recorded when this instance
* is constructed. This class supports an arbitrary number of time points.
*
* When an instance of this class is destructed, it will automatically
* print all the benchmark results, but only if a configurable amount
* of time has passed since its construction, allowing you to automatically
*/
class AutoBenchmark {
public:
/**
* Initialize a benchmark that automatically prints its records
* on destruction if the total time consumed is >= autoPrintThreshold
* at the time of destruction. Only the time up until the last Record()ed
* label is printed.
* @param autoPrintThreshold: How many seconds will need to have passed
* so that the destructor will automatically print. Default is to never print.
* @param benchmarkLabel: A label that will be printed once, before all the results
* @param lineLabel: A label (e.g. indent) that will be printed before each result line
*/
AutoBenchmark(double autoPrintThreshold=std::numeric_limits<double>::max(), const std::string& benchmarkLabel = "", const std::string& lineLabel = " ");
~AutoBenchmark();
/**
* Record a datapoint
*/
void Record(const std::string& label = "");
void Record(const char *label = "");
/**
* Print all time deltas
*/
void Print();
/**
* Reset the benchmark, as if it were a new instance.
*/
void Reset();
/**
* Return now() - first timepoint in seconds.
*/
double TotalSeconds();
private:
vector<std::chrono::system_clock::time_point> times;
vector<std::string> labels;
double autoPrintThreshold;
std::string benchmarkLabel;
std::string lineLabel;
};
Source (AutoBenchmark.cpp
):
#include "AutoBenchmark.hpp"
#include <iostream>
AutoBenchmark::AutoBenchmark(double autoPrintThreshold, const std::string& benchmarkLabel, const std::string& lineLabel)
: autoPrintThreshold(autoPrintThreshold), benchmarkLabel(benchmarkLabel), lineLabel(lineLabel) {
times.push_back(chrono::system_clock::now());
labels.emplace_back("Begin"); // Just to keep indices the same
}
AutoBenchmark::~AutoBenchmark() {
if(TotalSeconds() >= autoPrintThreshold) {
Print();
}
}
void AutoBenchmark::Record(const std::string &label) {
times.push_back(chrono::system_clock::now());
labels.emplace_back(label); // Just to keep indices the same
}
void AutoBenchmark::Record(const char *label) {
Record(string(label));
}
void AutoBenchmark::Print() {
if(benchmarkLabel.length()) {
cout << benchmarkLabel << '\n';
}
for (size_t i = 1; i < times.size(); i++) {
// Compute time interval for size comparison
chrono::duration<double, std::nano> ns = times[i] - times[i - 1];
chrono::duration<double, std::micro> us = times[i] - times[i - 1];
chrono::duration<double, std::milli> ms = times[i] - times[i - 1];
chrono::duration<double> s = times[i] - times[i - 1];
chrono::duration<double, std::ratio<60>> min = times[i] - times[i - 1];
chrono::duration<double, std::ratio<3600>> hrs = times[i] - times[i - 1];
// Print
if(ns.count() < 1000.0) {
cout << lineLabel << labels[i] << " took " << ns.count() << " ns\n";
} else if(us.count() < 1000.0) {
cout << lineLabel << labels[i] << " took " << us.count() << " μs\n";
} else if (ms.count() < 1000.0) {
cout << lineLabel << labels[i] << " took " << ms.count() << " ms\n";
} else if (s.count() < 60.0) {
cout << lineLabel << labels[i] << " took " << s.count() << " seconds\n";
} else if (min.count() < 1000.0) {
cout << lineLabel << labels[i] << " took " << min.count() << " minutes\n";
} else {
cout << lineLabel << labels[i] << " took " << hrs.count() << " hours\n";
}
}
cout << flush;
}
void AutoBenchmark::Reset() {
times.clear();
labels.clear();
times.push_back(chrono::system_clock::now());
labels.emplace_back("Begin");
}
double AutoBenchmark::TotalSeconds() {
chrono::duration s = chrono::system_clock::now() - times[0];
return s.count();
}
Usage example:
#include "AutoBenchmark.hpp"
void MySlowFunction() {
// Every run that takes >= 0.3 seconds will auto-print
AutoBenchmark myBenchmark(0.3, "Results of running MySlowFunction():");
// .. do task 1 ...
myBenchmark.Record("Running task 1"); // will print as: Running task 1 took 1.2ms
// .. do task 2 ...
myBenchmark.Record("Running task 2");
// Loop example
for(size_t i = 0; ....) {
// ... do loop iteration task here ...
myBenchmark.Record("Loop iteration " + std::to_string(i));
}
// myBenchmark will be destructed here, so if MySlowFunction() took
// more than 0.3s to run until it returned, the result will be printed
// to cout automatically.
}
If MySlowFunction() took more than 0.3s to run overall, AutoBenchmark will print the results when it is destructed - i.e. when MySlowFunction( ) returns:
Results of running MySlowFunction():
Running task 1 took 260.826 ms
Running task 2 took 36.148 μs
Loop iteration 0 took 2.5522 seconds
Loop iteration 1 took 664.059 ms
Loop iteration 2 took 22.2772 ms
Loop iteration 3 took 57.4024 ms
Loop iteration 4 took 16.9928 ms
Loop iteration 5 took 14.0497 ms
Loop iteration 6 took 62.5218 ms