Benchmark von Rust-Code mit libtest bench
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!
Schreiben Sie FizzBuzz in Rust
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:
- Erstellen Sie eine
main
Funktion - Iteriere von
1
bis100
einschließlich. - Berechne für jede Zahl den Modulus (Rest nach der Division) sowohl für
3
als auch für5
. - Musterabgleich auf die beiden Reste.
Wenn der Rest
0
ist, dann ist die Zahl durch den gegebenen Faktor teilbar. - Wenn der Rest für
3
und5
beide0
ist, dann druckeFizzBuzz
. - Wenn der Rest nur für
3
0
ist, dann druckeFizz
. - Wenn der Rest nur für
5
0
ist, dann druckeBuzz
. - Andernfalls drucke einfach die Zahl.
Schritt für Schritt folgen
Um dieser ausführlichen Anleitung zu folgen, müssen Sie zuerst Rust installieren.
🐰 Der Quellcode für diesen Beitrag ist auf GitHub verfügbar
Nachdem Sie Rust installiert haben, können Sie ein Terminalfenster öffnen und eingeben: cargo init game
Navigieren Sie dann in das neu erstellte Verzeichnis game
.
Sie sollten ein Verzeichnis namens src
mit einer Datei namens main.rs
sehen:
Ersetzen Sie dessen Inhalt mit der oben genannten FizzBuzz-Implementierung. Führen Sie dann cargo run
aus.
Die Ausgabe sollte so aussehen:
🐰 Boom! Sie meistern das Coding-Interview!
Eine neue Datei Cargo.lock
sollte erzeugt worden sein:
Bevor wir weitermachen, ist es wichtig, die Unterschiede zwischen Mikro-Benchmarking und Makro-Benchmarking zu besprechen.
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 Rust
Die drei beliebten Optionen für Benchmarking in Rust sind: libtest bench, Criterion, und Iai.
libtest ist Rusts eingebaute Einheitentest- und Benchmarking-Bibliothek.
Obwohl es Teil der Rust-Standardbibliothek ist, wird libtest bench immer noch als instabil betrachtet,
weshalb es nur in den nightly
Compiler-Releases verfügbar ist.
Um mit dem stabilen Rust-Compiler zu arbeiten,
muss ein separates Benchmarking-Harness
verwendet werden.
Keines von beiden wird jedoch aktiv weiterentwickelt.
Das beliebteste Benchmarking-Harness innerhalb des Rust-Ökosystems ist Criterion.
Es funktioniert sowohl mit stabilen als auch mit nightly
Rust-Compiler-Releases
und ist zum De-facto-Standard innerhalb der Rust-Community geworden.
Criterion ist auch viel funktionsreicher im Vergleich zum libtest bench.
Eine experimentelle Alternative zu Criterion ist Iai, vom selben Ersteller wie Criterion. Es verwendet jedoch Befehlszählungen anstelle der Echtzeitmessung: CPU-Befehle, L1-Zugriffe, L2-Zugriffe und RAM-Zugriffe. Dies ermöglicht einmalige Benchmarks, da diese Metriken zwischen den Durchläufen nahezu identisch bleiben sollten.
Alle drei werden von Bencher unterstützt. Warum also libtest bench wählen?
Es könnte eine gute Idee sein, wenn Sie versuchen, die externen Abhängigkeiten Ihres Projekts zu begrenzen
und Ihr Projekt verwendet bereits die Nightly
-Toolchain.
Abgesehen davon würde ich entweder Criterion oder Iai je nach Anwendungsfall empfehlen.
Installation von Rust nightly
All das gesagt, wir werden libtest bench verwenden, also stellen wir unsere Rust-Toolchain auf nightly
.
Erstellen Sie eine Datei rust-toolchain.toml
im Wurzelverzeichnis Ihres game
-Projekts, neben Cargo.toml
.
Ihre Verzeichnisstruktur sollte jetzt so aussehen:
Wenn das erledigt ist, führen Sie cargo run
erneut aus.
Es sollte eine Minute dauern, bis die neue, nächtliche Toolchain installiert ist,
bevor sie erneut ausgeführt wird und Ihnen die gleiche Ausgabe wie zuvor liefert.
FizzBuzz neu strukturieren
Um unsere FizzBuzz-Anwendung zu testen, müssen wir unsere Logik von der main
Funktion des Programms entkoppeln.
Benchmark-Harnesses können die main
Funktion nicht benchmarken.
Aktualisieren Sie Ihren Code, sodass er nun so aussieht:
Wir haben nun unseren Code in drei verschiedene Funktionen aufgeteilt:
main
: Der Haupt-Einstiegspunkt in unser Programm, der die Zahlen1
bis100
inklusive durchläuft und für jede Zahlplay_game
aufruft.play_game
: Nimmt eine vorzeichenlose Ganzzahln
entgegen, ruftfizz_buzz
mit dieser Zahl auf, und gibt das Ergebnis aus.fizz_buzz
: Nimmt eine vorzeichenlose Ganzzahln
entgegen und führt die tatsächlicheFizz
,Buzz
,FizzBuzz
oder Numerik-Logik aus, die das Ergebnis als Zeichenkette zurückgibt.
Benchmarking FizzBuzz
Um die instabile libtest crate zu verwenden, müssen wir das test
-Feature für unseren Code aktivieren und die test
crate importieren. Fügen Sie das oben auf main.rs
ein:
Jetzt sind wir bereit, unseren ersten Benchmak hinzuzufügen!
Fügen Sie das ganz unten auf main.rs
ein:
- Erstellen Sie ein Modul mit dem Namen
benchmarks
und stellen Sie die Compiler-Konfiguration auf dentest
Modus. - Importieren Sie den Benchmark-Runner
Bencher
. (🐰 Cooler Name!) - Importieren Sie unsere Funktion
play_game
. - Erstellen Sie einen Benchmark namens
bench_play_game
, der eine veränderbare Referenz aufBencher
erhält. - Stellen Sie das
#[bench]
Attribut ein, um anzugeben, dassbench_play_game
ein Benchmark ist. - Nutzen Sie die
Bencher
-Instanz (b
), um unseren Makro-Benchmark mehrmals auszuführen. - Führen Sie unseren Makro-Benchmark innerhalb einer “Black Box” aus, damit der Compiler unseren Code nicht optimiert.
- Iterieren Sie von
1
bis100
einschließlich. - Rufen Sie für jede Zahl
play_game
auf.
Jetzt sind wir bereit, unseren Code zu benchmarken, führen Sie cargo bench
aus:
🐰 Lassen Sie uns die Rübe drehen! 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 Rust
Unsere Key Performance Indicators (KPIs) sind runter, daher möchte unser Produkt Manager (PM), dass wir ein neues Feature hinzufügen. Nach viel Brainstorming und vielen Nutzerinterviews wurde entschieden, dass das gute alte FizzBuzz nicht ausreicht. Die Kids heutzutage 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 nicht signierte ganze Zahl nimmt und einen boolschen Wert zurückgibt. - Iterieren Sie über alle Zahlen von
0
bis zur gegebenen Zahln
einschließlich. - Initialisieren Sie unsere Fibonacci-Sequenz mit
0
und1
als denprevious
- undcurrent
-Zahlen. - Iterieren Sie solange die
current
Zahl kleiner ist als die aktuelle Iterationi
. - Addieren Sie die
previous
undcurrent
Zahlen, um dienext
Zahl zu bekommen. - 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, beenden wir die Schleife. - Überprüfen Sie, ob die
current
Zahl gleich der gegebenen Zahln
ist und geben Sie in diesem Falltrue
zurück. - Andernfalls, geben Sie
false
zurück.
Nun 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 Hilfsfunktion
is_fibonacci_number
auf. - Wenn das Ergebnis von
is_fibonacci_number
true
ist, dann geben SieFibonacci
zurück. - Wenn das Ergebnis von
is_fibonacci_number
false
ist, dann führen Sie die gleicheFizz
,Buzz
,FizzBuzz
, oder Zahl Logik aus und geben Sie das Ergebnis zurück.
Da wir fizz_buzz
in fizz_buzz_fibonacci
umbenannt haben, müssen wir auch unsere play_game
Funktion aktualisieren:
Unsere Funktionen main
und bench_play_game
können genau gleich bleiben.
Benchmark von FizzBuzzFibonacci
Jetzt können wir unseren Benchmark erneut ausführen:
Wenn wir in unserer Terminal-Historie zurückscrollen,
können wir einen Augenvergleich zwischen der Leistung unseres FizzBuzz und FizzBuzzFibonacci-Spiels machen: 4.879 ns
vs 22.167 ns
.
Ihre Zahlen werden etwas von meinen abweichen.
Jedoch ist der Unterschied zwischen den beiden Spielen wahrscheinlich im Bereich von 5x.
Das scheint mir gut zu sein! Besonders für das Hinzufügen eines so schick klingenden Features wie Fibonacci zu unserem Spiel.
Den Kindern wird es gefallen!
Erweiterung von FizzBuzzFibonacci in Rust
Unser Spiel ist ein Hit! Die Kinder lieben es wirklich, FizzBuzzFibonacci zu spielen.
So sehr, dass von den Geschäftsführern die Nachricht kam, dass sie eine Fortsetzung wollen.
Aber das ist die moderne Welt, wir brauchen jährlich wiederkehrende Einnahmen (ARR) anstatt einmaliger Käufe!
Die neue Vision für unser Spiel ist, dass es offen endet, kein Leben mehr zwischen der Begrenzung von 1
und 100
(auch wenn es inklusive ist).
Nein, wir sind auf zu neuen Fronten!
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
Um unser Spiel für jede Zahl funktionieren zu lassen, werden wir ein Kommandozeilenargument akzeptieren müssen.
Aktualisieren Sie die main
Funktion, um so auszusehen:
- Sammeln Sie alle Argumente (
args
), die unserer Spiel von der Kommandozeile aus übergeben werden. - Nehmen Sie das erste Argument, das zu unserem Spiel übergeben wird, und analysieren Sie es als eine vorzeichenlose Ganzzahl
i
. - Wenn das Parsen fehlschlägt oder kein Argument übergeben wird, fahren Sie standardmäßig mit unserem Spiel mit
15
als Eingang fort. - Spielen Sie schließlich unser Spiel mit der neu analysierten vorzeichenlosen Ganzzahl
i
.
Jetzt können wir unser Spiel mit jeder Zahl spielen!
Verwenden Sie cargo run
gefolgt von --
um Argumente an unser Spiel zu übergeben:
Und wenn wir eine Zahl weglassen oder eine ungültige Zahl angeben:
Wow, das war ein gründlicher Test! CI besteht. Unsere Chefs sind begeistert. Lasst uns es ausliefern! 🚀
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
bench_play_game_100
für das Spielen des Spiels mit der Zahl hundert (100
) - Ein Mikro-Benchmark
bench_play_game_1_000_000
für das Spielen des Spiels mit der Zahl eine Million (1_000_000
)
Als ich es ausgeführt habe, habe ich das bekommen:
Warten Sie darauf… warten Sie darauf…
Was! 439 ns
x 1.000
sollte 439.000 ns
und nicht 9.586.977 ns
sein 🤯
Obwohl ich meinen Fibonacci-Sequenzcode funktional richtig bekommen habe, muss ich irgendwo einen Leistungsfehler haben.
FizzBuzzFibonacci in Rust korrigieren
Lassen Sie uns noch einmal einen Blick auf diese is_fibonacci_number
Funktion werfen:
Jetzt, wo ich über die Leistung nachdenke, realisiere ich, dass ich eine unnötige, zusätzliche Schleife habe.
Wir können die for i in 0..=n {}
Schleife komplett loswerden und
vergleicht einfach den current
Wert mit der gegebenen Zahl (n
) 🤦
- Aktualisieren Sie Ihre
is_fibonacci_number
Funktion. - Initialisieren Sie unsere Fibonacci-Sequenz, die mit
0
und1
alsprevious
undcurrent
Zahlen beginnt. - Iterieren Sie, solange die
current
Zahl kleiner ist als die gegebene Zahln
. - Addieren Sie die
previous
undcurrent
Zahl, um dienext
Zahl zu erhalten. - Aktualisieren Sie die
previous
Zahl zurcurrent
Zahl. - Aktualisieren Sie die
current
Zahl zurnext
Zahl. - Sobald
current
größer oder gleich der gegebenen Zahln
ist, werden wir die Schleife verlassen. - Ü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 durchführen und sehen, wie wir abgeschnitten haben:
Oh, wow! Unsere bench_play_game
-Benchmark ist wieder auf etwa demselben Niveau wie die ursprüngliche FizzBuzz.
Ich wünschte, ich könnte mich genau an diese Punktzahl erinnern. Es sind aber schon drei Wochen vergangen.
Die Historie meines Terminals geht nicht so weit zurück.
Aber ich denke, es ist eng!
Die bench_play_game_100
-Benchmark ist fast 10x abgesunken, von 439 ns
auf 46 ns
.
Und das bench_play_game_1_000_000
-Benchmark ist um mehr als 10.000x gesunken! Von 9.586.977 ns
auf 53 ns
!
🐰 Hey, wenigstens haben wir diesen Performance-Bug erkannt, bevor er in die Produktion gelangt ist… ach, stimmt. Nein…
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.