AutoBenchmark: Automatisches Multi-Intervall-Benchmarking in C++ mit std::chrono
Problem:
Ein Teil deines C++-Codes leidet unter Performance-Problemen. Du suchst eine leichtgewichtige Lösung, die es dir ermöglicht, einfach verschiedene Zeitpunkte aufzuzeichnen und die Ergebnisse adaptiv auszugeben (d.h. du willst nicht wissen, dass etwas 1102564643 Nanosekunden gelaufen ist, du willst nur wissen, dass es 1,102 Sekunden gedauert hat)
Lösung
Ich habe AutoBenchmark geschrieben, damit du die möglichst unkomplizierte C++11-Erfahrung für deine Micro-Benchmarking-Bedürfnisse haben kannst.
AutoBenchmark ermöglicht es dir, verschiedene Zeitpunkte aufzuzeichnen, jeweils mit einem Label. Der erste Zeitpunkt wird aufgezeichnet, wenn diese Instanz konstruiert wird. AutoBenchmark unterstützt eine beliebige Anzahl von Zeitpunkten.
Wenn eine Instanz dieser Klasse destruiert wird, gibt sie automatisch alle Benchmark-Ergebnisse aus, aber nur wenn eine konfigurierbare Zeitspanne seit ihrer Konstruktion vergangen ist — das ist extrem praktisch, besonders wenn du mehrere Exit-Punkte in deiner Funktion hast, die sonst ein mehrfaches Aufruf von Print() erfordern würden.
Es ermöglicht dir, das Benchmark zu ignorieren, wenn ein Performance-Ziel erreicht wird (z.B. wenn du eine for-Schleife hast, die nur für einige Datenpunkte langsam ist, kannst du AutoBenchmark so konfigurieren, dass es Infos nur für die langsamen Durchläufe ausgibt).
Das Standardverhalten (d.h. Konstruktor mit Standardparametern) deaktiviert die automatische Ausgabe — in diesem Fall kannst du Print() selbst aufrufen.
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;
/**
* Automatisches Benchmark: Ermöglicht es dir, verschiedene Zeitpunkte aufzuzeichnen,
* jeweils mit einem Label. Der erste Zeitpunkt wird aufgezeichnet, wenn diese Instanz
* konstruiert wird. Diese Klasse unterstützt eine beliebige Anzahl von Zeitpunkten.
*
* Wenn eine Instanz dieser Klasse destruiert wird, gibt sie automatisch
* alle Benchmark-Ergebnisse aus, aber nur wenn eine konfigurierbare Zeitspanne
* seit ihrer Konstruktion vergangen ist, was es dir ermöglicht, automatisch
*/
class AutoBenchmark {
public:
/**
* Initialisiert ein Benchmark, das seine Einträge automatisch ausgibt,
* bei Destruktion, wenn die insgesamt verbrauchte Zeit >= autoPrintThreshold
* ist zum Zeitpunkt der Destruktion. Nur die Zeit bis zum letzten Record()-ed
* Label wird ausgegeben.
* @param autoPrintThreshold: Wie viele Sekunden vergangen sein müssen,
* damit der Destruktor automatisch ausgibt. Standard ist niemals ausgeben.
* @param benchmarkLabel: Ein Label, das einmal vor allen Ergebnissen ausgegeben wird
* @param lineLabel: Ein Label (z.B. Einrückung), das vor jeder Ergebniszeile ausgegeben wird
*/
AutoBenchmark(double autoPrintThreshold=std::numeric_limits<double>::max(), const std::string& benchmarkLabel = "", const std::string& lineLabel = " ");
~AutoBenchmark();
/**
* Einen Datenpunkt aufzeichnen
*/
void Record(const std::string& label = "");
void Record(const char *label = "");
/**
* Alle Zeitdeltas ausgeben
*/
void Print();
/**
* Benchmark zurücksetzen, als wäre es eine neue Instanz.
*/
void Reset();
/**
* Gibt now() - ersten Zeitpunkt in Sekunden zurück.
*/
double TotalSeconds();
private:
vector<std::chrono::system_clock::time_point> times;
vector<std::string> labels;
double autoPrintThreshold;
std::string benchmarkLabel;
std::string lineLabel;
};Quellcode (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"); // Nur um Indizes gleich zu halten
}
AutoBenchmark::~AutoBenchmark() {
if(TotalSeconds() >= autoPrintThreshold) {
Print();
}
}
void AutoBenchmark::Record(const std::string &label) {
times.push_back(chrono::system_clock::now());
labels.emplace_back(label); // Nur um Indizes gleich zu halten
}
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++) {
// Zeitintervall für Größenvergleich berechnen
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];
// Ausgeben
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();
}Verwendungsbeispiel:
#include "AutoBenchmark.hpp"
void MySlowFunction() {
// Jeder Durchlauf, der >= 0,3 Sekunden dauert, wird automatisch ausgegeben
AutoBenchmark myBenchmark(0.3, "Results of running MySlowFunction():");
// .. Aufgabe 1 erledigen ...
myBenchmark.Record("Running task 1"); // wird ausgegeben als: Running task 1 took 1.2ms
// .. Aufgabe 2 erledigen ...
myBenchmark.Record("Running task 2");
// Schleifenbeispiel
for(size_t i = 0; ....) {
// ... Schleifeniterations-Aufgabe hier erledigen ...
myBenchmark.Record("Loop iteration " + std::to_string(i));
}
// myBenchmark wird hier destruiert, also wenn MySlowFunction() insgesamt
// mehr als 0,3s bis zur Rückkehr gedauert hat, wird das Ergebnis automatisch
// auf cout ausgegeben.
}Wenn MySlowFunction() insgesamt mehr als 0,3s gedauert hat, gibt AutoBenchmark die Ergebnisse bei der Destruktion aus — d.h. wenn MySlowFunction() zurückkehrt:
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