Wie man C++-Code mit Google Benchmark benchmarkt
Everett Pompeii
Was ist Benchmarking?
Benchmarking ist die Praxis, die Leistung Ihres Codes zu testen, um zu sehen, wie schnell (Latenz) oder wie viel (Durchsatz) Arbeit er leisten kann. Dieser oft übersehene Schritt in der Softwareentwicklung ist entscheidend für die Erstellung und Wartung von schnellem und leistungsstarkem Code. Benchmarking liefert die notwendigen Metriken für Entwickler, um zu verstehen, wie gut ihr Code unter verschiedenen Arbeitslasten und Bedingungen funktioniert. Aus den gleichen Gründen, aus denen Sie Unit- und Integrationstests schreiben, um Funktionsregressionen zu verhindern, sollten Sie Benchmarks schreiben, um Leistungsregressionen zu verhindern. Leistungsfehler sind Fehler!
Schreibe FizzBuzz in C++
Um Benchmarks zu schreiben, benötigen wir einige Quellcodes zum Benchmarken. Zum Anfang werden wir ein sehr einfaches Programm schreiben, FizzBuzz.
Die Regeln für FizzBuzz lauten wie folgt:
Schreibe ein Programm, das die ganzen Zahlen von
1
bis100
(inklusiv) ausgibt:
- Für Vielfache von drei, drucke
Fizz
- Für Vielfache von fünf, drucke
Buzz
- Für Vielfache von drei und fünf, drucke
FizzBuzz
- Für alle anderen, drucke die Zahl
Es gibt viele Möglichkeiten, FizzBuzz zu schreiben. Also wählen wir meine Lieblingsmethode:
- Iterieren Sie von
1
bis100
, und erhöhen Sie nach jeder Iteration. - Berechnen Sie für jede Zahl den Modulus (Rest nach der Division).
- Wenn der Rest
0
ist, dann ist die Zahl ein Vielfaches des gegebenen Faktors:- Wenn der Rest bei
15
0
ist, dann drucken SieFizzBuzz
. - Wenn der Rest bei
3
0
ist, dann drucken SieFizz
. - Wenn der Rest bei
5
0
ist, dann drucken SieBuzz
.
- Wenn der Rest bei
- Ansonsten drucken Sie einfach die Zahl.
Schritt-für-Schritt-Anleitung
Um dieser Schritt-für-Schritt-Anleitung zu folgen, müssen Sie installieren Sie git
, installieren Sie cmake
, und installieren Sie die GNU Compiler Collection (GCC) g++
.
🐰 Der Quellcode für diesen Beitrag ist auf GitHub verfügbar.
Erstellen Sie eine C++-Datei mit dem Namen game.cpp
,
und fügen Sie den Inhalt der obigen FizzBuzz-Implementierung ein.
Verwenden Sie g++
, um eine ausführbare Datei mit dem Namen game
zu erstellen, und führen Sie sie dann aus.
Die Ausgabe sollte wie folgt aussehen:
🐰 Boom! Sie meistern das Coding-Interview!
Bevor Sie weitergehen, ist es wichtig, die Unterschiede zwischen Mikro-Benchmarking und Makro-Benchmarking zu diskutieren.
Micro-Benchmarking vs. Macro-Benchmarking
Es gibt zwei Hauptkategorien von Software-Benchmarks: Micro-Benchmarks und Macro-Benchmarks.
Micro-Benchmarks arbeiten auf einer Ebene ähnlich wie Unit-Tests.
Zum Beispiel wäre ein Benchmark für eine Funktion, die Fizz
, Buzz
oder FizzBuzz
für eine einzelne Zahl ermittelt, ein Micro-Benchmark.
Macro-Benchmarks arbeiten auf einer Ebene, die Integrationstests ähnelt.
Zum Beispiel wäre ein Benchmark für eine Funktion, die das gesamte Spiel von FizzBuzz spielt, von 1
bis 100
, ein Macro-Benchmark.
Im Allgemeinen ist es am besten, auf der niedrigstmöglichen Abstraktionsebene zu testen. Im Falle von Benchmarks macht dies sie sowohl leichter zu pflegen, und es hilft, die Menge an Rauschen in den Messungen zu reduzieren. Allerdings können genau wie End-to-End-Tests, die für eine Überprüfung der gesamten Systemzusammenstellung sehr hilfreich sein können, Macro-Benchmarks sehr nützlich sein, um sicherzustellen, dass die kritischen Pfade durch Ihre Software performant bleiben.
Benchmarking in C++
Die zwei beliebten Optionen für Benchmarking in C++ sind: Google Benchmark und Catch2.
Google Benchmark ist eine robuste und vielseitige Benchmarking-Bibliothek für C++, die es Entwicklern ermöglicht, die Leistung ihres Codes mit hoher Präzision zu messen. Ein wesentlicher Vorteil ist die einfache Integration in bestehende Projekte, insbesondere solche, die bereits GoogleTest verwenden. Google Benchmark bietet detaillierte Leistungsmetriken, einschließlich der Möglichkeit zur Messung von CPU-Zeit, Wall-Zeit und Speicherverbrauch. Es unterstützt ein breites Spektrum an Benchmarking-Szenarien, von einfachen Funktionsbenchmarks bis hin zu komplexen, parametrisierten Tests.
Catch2 ist ein modernes, header-only Testframework für C++, das den Prozess des Schreibens und Ausführens von Tests vereinfacht. Einer seiner Hauptvorteile ist die Benutzerfreundlichkeit, mit einer Syntax, die sowohl intuitiv als auch ausdrucksstark ist, was es Entwicklern ermöglicht, Tests schnell und klar zu schreiben. Catch2 unterstützt eine breite Palette von Testtypen, einschließlich Unit-Tests, Integrationstests, Behavior-Driven Development (BDD) Stiltests und grundlegende Mikro-Benchmarking-Funktionen.
Beide werden von Bencher unterstützt. Warum also Google Benchmark wählen? Google Benchmark integriert sich nahtlos mit GoogleTest, welches der De-facto-Standard für Unit-Tests im C++-Ökosystem ist. Ich würde vorschlagen, Google Benchmark zu verwenden, um die Latenz Ihres Codes zu messen, insbesondere wenn Sie bereits GoogleTest verwenden. Das heißt, Google Benchmark eignet sich hervorragend zum Messen der echten Zeit.
Refactorisieren von FizzBuzz
Um unsere FizzBuzz-Anwendung zu testen,
müssen wir unsere Logik von der main
-Funktion unseres Programms entkoppeln.
Benchmark-Frameworks können die main
-Funktion nicht benchmarken.
Um dies zu tun, müssen wir einige Änderungen vornehmen.
Lassen Sie uns unsere FizzBuzz-Logik in ein paar Funktionen umstrukturieren,
die sich in einer neuen Datei namens play_game.cpp
befinden:
fizz_buzz
: Nimmt eine ganze Zahln
entgegen und führt die eigentlicheFizz
,Buzz
,FizzBuzz
oder Zahlenlogik durch, indem das Ergebnis als String zurückgegeben wird.play_game
: Nimmt eine ganze Zahln
entgegen, ruftfizz_buzz
mit dieser Zahl auf und gibt das Ergebnis aus, wennshould_print
auftrue
gesetzt ist.
Erstellen wir nun eine Header-Datei mit dem Namen play_game.h
und fügen die Funktionsdeklaration von play_game
hinzu:
Dann aktualisieren Sie die main
-Funktion in game.cpp
, um die Definition der play_game
-Funktion aus der Header-Datei zu verwenden:
Die main
-Funktion unseres Programms iteriert durch die Zahlen 1
bis 100
inklusive und ruft für jede Zahl play_game
auf, wobei should_print
auf true
gesetzt ist.
Benchmarking FizzBuzz
Um unseren Code zu benchmarken, müssen wir zuerst Google Benchmark installieren.
Klonen Sie die Bibliothek:
Wechseln Sie in das neu geklonte Verzeichnis:
Verwenden Sie cmake
, um ein Verzeichnis für die Erstellung zu erstellen, in das das Build-Output platziert wird:
Verwenden Sie cmake
, um die Build-Systemdateien zu generieren und alle Abhängigkeiten herunterzuladen:
Endlich die Bibliothek bauen:
Zurück zum übergeordneten Verzeichnis gehen:
Erstellen wir nun eine neue Datei namens benchmark_game.cpp
:
- Importieren Sie die Funktionsdefinitionen aus
play_game.h
. - Importieren Sie den Header der Google
benchmark
-Bibliothek. - Erstellen Sie eine Funktion namens
BENCHMARK_game
, die eine Referenz zubenchmark::State
nimmt. - Iterieren Sie über das
benchmark::State
-Objekt. - Für jede Iteration von
1
bis100
(inklusive) iterieren.- Rufen Sie
play_game
mit der aktuellen Nummer undshould_print
auffalse
auf.
- Rufen Sie
- Übergeben Sie die Funktion
BENCHMARK_game
an denBENCHMARK
Runner. - Führen Sie den Benchmark mit
BENCHMARK_MAIN
aus.
Jetzt sind wir bereit, unseren Code zu benchmarken:
🐰 Let’s make the beet rocken! Wir haben unsere ersten Benchmark-Metriken!
Endlich können wir unsere müden Entwicklerköpfe ausruhen… Nur ein Scherz, unsere Benutzer wollen ein neues Feature!
Schreiben Sie FizzBuzzFibonacci in C++
Unsere Key Performance Indicators (KPIs) sind gesunken, also möchte unser Product Manager (PM), dass wir eine neue Funktion hinzufügen. Nach viel Brainstorming und vielen Benutzerinterviews wurde entschieden, dass das gute alte FizzBuzz nicht genug ist. Die Kinder von heute wollen ein neues Spiel, FizzBuzzFibonacci.
Die Regeln für FizzBuzzFibonacci lauten wie folgt:
Schreibe ein Programm, welches die Zahlen von
1
bis100
(inklusiv) ausgibt:
- Für Vielfache von drei, drucke
Fizz
- Für Vielfache von fünf, drucke
Buzz
- Für Vielfache von sowohl drei als auch fünf, drucke
FizzBuzz
- Für Zahlen, die Teil der Fibonacci-Sequenz sind, drucke nur
Fibonacci
- Für alle anderen, drucke die Zahl
Die Fibonacci-Sequenz ist eine Sequenz, bei der jede Zahl die Summe der beiden vorhergehenden Zahlen ist.
Zum Beispiel wäre, beginnend bei 0
und 1
, die nächste Zahl in der Fibonacci-Sequenz 1
.
Gefolgt von: 2
, 3
, 5
, 8
und so weiter.
Zahlen, die Teil der Fibonacci-Sequenz sind, werden als Fibonacci-Zahlen bezeichnet. Daher müssen wir eine Funktion schreiben, die Fibonacci-Zahlen erkennt.
Es gibt viele Arten, die Fibonacci-Sequenz zu schreiben und ebenso viele Möglichkeiten, eine Fibonacci-Zahl zu erkennen. Daher werden wir meine Lieblingsmethode wählen:
- Erstellen Sie eine Funktion namens
is_fibonacci_number
, die eine ganze Zahl entgegennimmt und einen booleschen Wert zurückgibt. - Iterieren Sie über alle Zahlen von
0
bis zu unserer gegebenen Zahln
einschließlich. - Initiieren Sie unsere Fibonacci-Sequenz beginnend mit
0
und1
als dievorherige
undaktuelle
Zahl. - Iterieren Sie, während die
aktuelle
Zahl kleiner als die aktuelle Iterationi
ist. - Addieren Sie die
vorherige
undaktuelle
Zahl, um dienächste
Zahl zu erhalten. - Aktualisieren Sie die
vorherige
Zahl zuraktuellen
Zahl. - Aktualisieren Sie die
aktuelle
Zahl zurnächsten
Zahl. - Sobald
aktuell
größer oder gleich der gegebenen Zahln
ist, verlassen wir die Schleife. - Prüfen Sie, ob die
aktuelle
Zahl gleich der gegebenen Zahln
ist, und wenn ja, geben Sietrue
zurück. - Andernfalls geben Sie
false
zurück.
Jetzt müssen wir unsere fizz_buzz
Funktion aktualisieren:
- Benennen Sie die
fizz_buzz
Funktion infizz_buzz_fibonacci
um, um sie aussagekräftiger zu machen. - Rufen Sie unsere
is_fibonacci_number
Hilfsfunktion auf. - Wenn das Ergebnis von
is_fibonacci_number
true
ist, geben SieFibonacci
zurück. - Wenn das Ergebnis von
is_fibonacci_number
false
ist, führen Sie die gleicheFizz
,Buzz
,FizzBuzz
oder Zahlenlogik aus und geben das Ergebnis zurück.
Da wir fizz_buzz
in fizz_buzz_fibonacci
umbenannt haben, müssen wir auch unsere play_game
Funktion aktualisieren:
Sowohl unsere main
Funktion als auch die BENCHMARK_game
Funktion können genauso bleiben.
Benchmarking FizzBuzzFibonacci
Nun können wir unseren Benchmark erneut ausführen:
Wenn wir durch unsere Terminal-Historie zurückscrollen, können wir einen augenscheinlichen Vergleich zwischen der Leistung unserer FizzBuzz- und FizzBuzzFibonacci-Spiele anstellen: 1698 ns
vs 56190 ns
. Deine Zahlen werden ein wenig anders sein als meine. Der Unterschied zwischen den beiden Spielen liegt jedoch wahrscheinlich im Bereich von 50x. Das scheint mir gut zu sein! Besonders für das Hinzufügen eines Features, das so schick klingt wie Fibonacci zu unserem Spiel. Die Kinder werden es lieben!
FizzBuzzFibonacci in C++ erweitern
Unser Spiel ist ein Hit! Die Kinder lieben es wirklich, FizzBuzzFibonacci zu spielen.
Tatsächlich haben die Führungskräfte signalisiert, dass sie eine Fortsetzung wollen.
Aber das ist die moderne Welt, wir brauchen Annual Recurring Revenue (ARR) und nicht nur einmalige Käufe!
Die neue Vision für unser Spiel ist, dass es offen ist, keine Lebensbeschränkung mehr zwischen 1
und 100
(auch wenn sie inklusive sind).
Nein, wir sind auf neuen Wegen unterwegs!
Die Regeln für Open World FizzBuzzFibonacci lauten wie folgt:
Schreiben Sie ein Programm, das jede positive ganze Zahl akzeptiert und ausgibt:
- Für Vielfache von drei, drucken Sie
Fizz
- Für Vielfache von fünf, drucken Sie
Buzz
- Für Vielfache von drei und fünf, drucken Sie
FizzBuzz
- Für Zahlen, die Teil der Fibonacci-Sequenz sind, drucken Sie nur
Fibonacci
- Für alle anderen drucken Sie die Zahl
Damit unser Spiel für jede Zahl funktioniert, müssen wir ein Kommandozeilenargument akzeptieren.
Aktualisieren Sie die main
-Funktion, damit sie folgendermaßen aussieht:
- Aktualisieren Sie die
main
-Funktion, umargc
undargv
zu akzeptieren. - Holen Sie das erste Argument, das unserem Spiel übergeben wird, und überprüfen Sie, ob es sich um eine Ziffer handelt.
- Wenn ja, parsen Sie das erste Argument als Ganzzahl,
i
. - Spielen Sie unser Spiel mit der neu geparsten Ganzzahl
i
.
- Wenn ja, parsen Sie das erste Argument als Ganzzahl,
- Wenn das Parsen fehlschlägt oder kein Argument übergeben wird, wird standardmäßig eine gültige Eingabeaufforderung angezeigt.
Jetzt können wir unser Spiel mit jeder beliebigen Zahl spielen!
Kompilieren Sie unsere game
-Ausführungsdatei neu und führen Sie dann
die Ausführungsdatei gefolgt von einer Ganzzahl aus, um unser Spiel zu spielen:
Und wenn wir eine ungültige Zahl weglassen oder angeben:
Wow, das war ein gründliches Testen! CI besteht. Unsere Chefs sind begeistert. Lassen Sie es uns veröffentlichen! 🚀
Das Ende
🐰 … das Ende Ihrer Karriere vielleicht?
Nur ein Scherz! Alles steht in Flammen! 🔥
Nun, anfangs schien alles gut zu laufen. Und dann um 02:07 Uhr am Samstag löste mein Pager aus:
📟 Dein Spiel steht in Flammen! 🔥
Nachdem ich aus dem Bett gehetzt war, versuchte ich herauszufinden, was los war. Ich versuchte, die Logs durchzusuchen, aber das war schwierig, weil alles ständig abstürzte. Schließlich fand ich das Problem. Die Kinder! Sie liebten unser Spiel so sehr, dass sie es bis zu einer Million hochspielten! In einem Geistesblitz fügte ich zwei neue Benchmarks hinzu:
- Ein Mikro-Benchmark
BENCHMARK_game_100
für das Spiel mit der Zahl hundert (100
) - Ein Mikro-Benchmark
BENCHMARK_game_1_000_000
für das Spiel mit der Zahl eine Million (1_000_000
)
Als ich es ausführte, erhielt ich Folgendes:
Warte… warte…
Was! 1,249 ns
x 10,000
sollten 12,490,000 ns
sein, nicht 110,879,642 ns
🤯
Obwohl ich meinen Fibonacci-Sequenz-Code funktional korrekt hatte, muss irgendwo ein Performance-Fehler drin sein.
Fix FizzBuzzFibonacci in C++
Werfen wir einen weiteren Blick auf die Funktion is_fibonacci_number
:
Jetzt, da ich über die Leistung nachdenke, fällt mir auf, dass ich eine unnötige, zusätzliche Schleife habe.
Wir können die Schleife for (int i = 0; i <= n; ++i)
vollständig entfernen und
einfach den Wert current
mit der gegebenen Zahl (n
) vergleichen 🤦
- Aktualisieren Sie unsere Funktion
is_fibonacci_number
. - Initialisieren Sie unsere Fibonacci-Sequenz, beginnend mit
0
und1
als den jeweiligenprevious
undcurrent
Zahlen. - Iterieren Sie, während die
current
-Zahl kleiner ist als die gegebene Zahln
. - Fügen Sie die
previous
- undcurrent
-Zahl hinzu, um dienext
-Zahl zu erhalten. - Aktualisieren Sie die
previous
-Zahl auf diecurrent
-Zahl. - Aktualisieren Sie die
current
-Zahl auf dienext
-Zahl. - Sobald
current
größer oder gleich der gegebenen Zahln
ist, verlassen wir die Schleife. - Überprüfen Sie, ob die
current
-Zahl gleich der gegebenen Zahln
ist und geben Sie dieses Ergebnis zurück.
Jetzt lassen Sie uns diese Benchmarks erneut ausführen und sehen, wie wir abgeschnitten haben:
Oh, wow! Unser BENCHMARK_game
Benchmark ist zurück, ungefähr dort, wo er bei dem ursprünglichen FizzBuzz war.
Ich wünschte, ich könnte mich genau an das Ergebnis erinnern. Es ist allerdings schon drei Wochen her.
Mein Terminalverlauf reicht nicht so weit zurück, und Google Benchmark speichert seine Ergebnisse nicht.
Aber ich denke, es ist nah dran!
Der BENCHMARK_game_100
Benchmark ist fast um das 50-fache gesunken auf 34.4 ns
.
Und der BENCHMARK_game_1_000_000
Benchmark ist mehr als 1.500.000-fach gesunken! Von 110,879,642 ns
auf 61.6 ns
!
🐰 Hey, wenigstens haben wir diesen Performance-Fehler erwischt, bevor er in die Produktion gelangte… oh, richtig. Schon gut…
Leistungsverschlechterungen in CI auffangen
Die Geschäftsführung war nicht glücklich über die Flut von negativen Bewertungen, die unser Spiel aufgrund meines kleinen Performance-Bugs erhalten hat. Sie sagten mir, dass so etwas nicht wieder passieren darf, und als ich fragte wie, sagten sie mir einfach, dass ich es einfach nicht noch einmal tun soll. Wie soll ich das schaffen‽
Zum Glück habe ich dieses großartige Open-Source-Tool namens Bencher gefunden. Es gibt einen sehr großzügigen kostenlosen Tier, sodass ich Bencher Cloud einfach für meine persönlichen Projekte verwenden kann. Und bei der Arbeit, wo alles in unserer privaten Cloud sein muss, habe ich angefangen, Bencher Self-Hosted zu verwenden.
Bencher hat eingebaute Adapter, also ist es einfach, es in CI zu integrieren. Nachdem ich der Quick Start Anleitung gefolgt bin, kann ich meine Benchmarks ausführen und mit Bencher verfolgen.
Mit diesem praktischen Zeitreise-Gerät, das mir ein netter Hase gegeben hat, konnte ich zurück in die Vergangenheit reisen und nachspielen, was passiert wäre, wenn wir von Anfang an Bencher verwendet hätten. Sie können sehen, wo wir die fehlerhafte FizzBuzzFibonacci Implementierung zum ersten Mal gepusht haben. Ich habe sofort Fehler in CI als Kommentar zu meinem Pull Request bekommen. Noch am gleichen Tag habe ich den Performance-Fehler behoben, indem ich diese unnötige, zusätzliche Schleife entfernt habe. Keine Brände. Nur zufriedene Benutzer.
Bencher: Kontinuierliches Benchmarking
Bencher ist eine Suite von kontinuierlichen Benchmarking-Tools. Hatten Sie jemals eine Performance Regression, die Ihre Nutzer beeinflusste? Bencher hätte das verhindern können. Bencher ermöglicht es Ihnen, Leistungsregressionen vorher zu erkennen und zu verhindern, bevor sie in die Produktion gelangen.
- Ausführen: Führen Sie Ihre Benchmarks lokal oder in CI mit Ihren bevorzugten Benchmarking-Tools aus. Das
bencher
CLI umfasst einfach Ihr vorhandenes Benchmark-Harness und speichert die Ergebnisse. - Verfolgen: Verfolgen Sie die Ergebnisse Ihrer Benchmarks im Laufe der Zeit. Überwachen, abfragen und grafisch darstellen der Ergebnisse mit der Bencher Web Konsole auf Basis des Quellzweigs, Testbetts und Maßnahme.
- Auffangen: Fangen Sie Leistungsregressionen in CI ab. Bencher verwendet modernste, anpassbare Analysen, um Leistungsregressionen zu erkennen, bevor sie in die Produktion gelangen.
Aus denselben Gründen, warum Unit Tests in CI laufen, um Feature Regressionen zu verhindern, sollten Benchmarks in CI mit Bencher ausgeführt werden, um Leistungsregressionen zu verhindern. Performance-Bugs sind Fehler!
Beginnen Sie damit, Leistungsregressionen in CI aufzufangen - probieren Sie Bencher Cloud kostenlos aus.