Wie man Rust-Code mit Gungraun 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!
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
1bis100(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:
fn main() { for i in 1..=100 { match (i % 3, i % 5) { (0, 0) => println!("FizzBuzz"), (0, _) => println!("Fizz"), (_, 0) => println!("Buzz"), (_, _) => println!("{i}"), } }}- Erstellen Sie eine
mainFunktion - Iteriere von
1bis100einschließlich. - Berechne für jede Zahl den Modulus (Rest nach der Division) sowohl für
3als auch für5. - Musterabgleich auf die beiden Reste.
Wenn der Rest
0ist, dann ist die Zahl durch den gegebenen Faktor teilbar. - Wenn der Rest für
3und5beide0ist, dann druckeFizzBuzz. - Wenn der Rest nur für
30ist, dann druckeFizz. - Wenn der Rest nur für
50ist, 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.
game├── Cargo.toml└── src └── main.rsSie sollten ein Verzeichnis namens src mit einer Datei namens main.rs sehen:
fn main() { println!("Hello, world!");}Ersetzen Sie dessen Inhalt mit der oben genannten FizzBuzz-Implementierung. Führen Sie dann cargo run aus.
Die Ausgabe sollte so aussehen:
$ cargo run Compiling playground v0.0.1 (/home/bencher) Finished dev [unoptimized + debuginfo] target(s) in 0.44s Running `target/debug/game`
12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzz...9798FizzBuzz🐰 Boom! Sie meistern das Coding-Interview!
Eine neue Datei Cargo.lock sollte erzeugt worden sein:
game├── Cargo.lock├── Cargo.toml└── src └── main.rsBevor 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 vier werden von Bencher unterstützt. Warum also Gungraun (den umbenannten Nachfolger von Iai-Callgrind) wählen? Gungraun verwendet Anweisungszähler anstelle von Wanduhrzeit. Das macht es ideal für kontinuierliches Benchmarking, also Benchmarking in CI. Ich würde vorschlagen, Gungraun für kontinuierliches Benchmarking zu verwenden, besonders wenn Sie Shared Runner verwenden. Gungraun wird aktiv gepflegt und hat umfassende Online-Dokumentation, was es zu einer zuverlässigen Wahl für langfristige Projekte macht. Es ist wichtig zu verstehen, dass Gungraun nur einen Proxy für das misst, was Sie wirklich interessiert. Verdoppelt sich die Latenz Ihrer Anwendung, wenn man von 1.000 Anweisungen auf 2.000 Anweisungen geht? Vielleicht ja, vielleicht nein. Aus diesem Grund kann es nützlich sein, auch wanduhrzeitbasierte Benchmarks parallel zu anweisungszählerbasierenden Benchmarks auszuführen.
Valgrind installieren
Gungraun verwendet ein Tool namens Valgrind um Anweisungszähler zu sammeln. Valgrind unterstützt Linux, Solaris, FreeBSD und macOS. Die macOS-Unterstützung ist jedoch auf x86_64-Prozessoren beschränkt, da arm64 (M1, M2, etc.) Prozessoren noch nicht unterstützt werden.
Auf Debian ausführen: sudo apt-get install valgrind
Auf macOS (nur x86_64/Intel-Chip): brew install valgrind
FizzBuzz umstrukturieren
Um unsere FizzBuzz-Anwendung zu testen, entkoppeln wir unsere Logik von der main-Funktion unseres Programms.
Im Gegensatz zu anderen Benchmark-Harnesses kann Gungraun die Benchmark-Binary
und die main-Funktion benchmarken, aber das ist rein Makro-Benchmarking. Wir wollen beides, Makro und Mikro.
Dafür müssen wir einige Änderungen vornehmen.
Erstellen Sie unter src eine neue Datei namens lib.rs:
game├── Cargo.lock├── Cargo.toml└── src └── lib.rs └── main.rsFügen Sie den folgenden Code zu lib.rs hinzu:
pub fn play_game(n: u32, print: bool) { let result = fizz_buzz(n); if print { println!("{result}"); }}
pub fn fizz_buzz(n: u32) -> String { match (n % 3, n % 5) { (0, 0) => "FizzBuzz".to_string(), (0, _) => "Fizz".to_string(), (_, 0) => "Buzz".to_string(), (_, _) => n.to_string(), }}play_game: Nimmt eine vorzeichenlose Ganzzahlnan, ruftfizz_buzzmit dieser Zahl auf, und wennprinttrueist, wird das Ergebnis ausgegeben.fizz_buzz: Nimmt eine vorzeichenlose Ganzzahlnan und führt die eigentlicheFizz,Buzz,FizzBuzzoder Zahlenlogik aus und gibt das Ergebnis als String zurück.
Dann sieht die aktualisierte main.rs so aus:
use game::play_game;
fn main() { for i in 1..=100 { play_game(i, true); }}game::play_game: Importieren Sieplay_gameaus demgameCrate, das wir gerade mitlib.rserstellt haben.main: Der Haupteinstiegspunkt in unser Programm, der die Zahlen von1bis100inklusive durchläuft und für jede Zahlplay_gameaufruft, mitprintauftruegesetzt.
Benchmarking von FizzBuzz
Um unseren Code zu benchmarken, müssen wir ein benches-Verzeichnis erstellen und
eine Datei für unsere Benchmarks hinzufügen, play_game.rs.
Beachten Sie, dass wir von der empfohlenen Methode
zur Strukturierung von Benchmarks der Einfachheit halber abweichen.
Für Ihr Projekt sollten Sie den Empfehlungen folgen:
game├── Cargo.lock├── Cargo.toml└── benches └── play_game.rs└── src └── lib.rs └── main.rsFügen Sie in play_game.rs den folgenden Code hinzu:
use gungraun::prelude::*;use std::hint::black_box;use game::play_game;
#[library_benchmark]fn bench_play_game() { for i in 1..=100 { play_game(black_box(i), black_box(false)) }}
library_benchmark_group!( name = bench_play_game_group, benchmarks = [bench_play_game]);
main!(library_benchmark_groups = bench_play_game_group);- Importieren Sie das
gungraun::prelude-Modul, das die notwendigen Makros bereitstellt. - Verwenden Sie
std::hint::black_box, um zu verhindern, dass der Compiler unseren Benchmark wegoptimiert. - Importieren Sie die Funktion
play_gameaus unseremgame-Crate. - Erstellen Sie eine Library-Benchmark-Funktion namens
bench_play_gamemit dem Attribut#[library_benchmark]. - Iterieren Sie von
1bis100und rufen Sieplay_gamemitprintauffalsegesetzt auf. - Erstellen Sie eine Library-Benchmark-Gruppe namens
bench_play_game_group, die unserenbench_play_game-Benchmark enthält. - Verwenden Sie das
main!-Makro, um die Benchmark-Gruppe auszuführen.
Jetzt müssen wir das game-Crate konfigurieren, um unsere Benchmarks auszuführen.
Fügen Sie das Folgende am Ende Ihrer Cargo.toml-Datei hinzu:
[dev-dependencies]gungraun = "0.18.0"
[[bench]]name = "play_game"harness = false
[profile.bench]debug = truegungraun: Fügen Siegungraunals Entwicklungsabhängigkeit hinzu, da wir es nur für Performance-Tests verwenden.bench: Registrieren Sieplay_gameals Benchmark und setzen Sieharnessauffalse, da wir Gungraun als unser Benchmarking-Harness verwenden werden.debug = true: Aktivieren Sie Debug-Informationen in Benchmark-Builds, was für Gungraun erforderlich ist, um detaillierte Ausgaben zu liefern.
Jetzt sind wir bereit, unseren Code zu benchmarken, führen Sie cargo bench aus:
$ cargo bench Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)play_game::bench_play_game_group::bench_play_game_100 Instructions: 17902|N/A (*********) L1 Hits: 24984|N/A (*********) LL Hits: 1|N/A (*********) RAM Hits: 20|N/A (*********) Total read+write: 25005|N/A (*********) Estimated Cycles: 25689|N/A (*********)
Gungraun result: Ok. 1 without regressions; 0 regressed; 0 filtered; 1 benchmarks finished in 0.15258s🐰 Lasst uns loslegen! Wir haben unsere ersten Benchmark-Metriken!
Endlich können wir unsere müden Entwicklerköpfe zur Ruhe legen… Nur ein Scherz, unsere Nutzer wollen eine neue Funktion!
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
1bis100(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:
fn is_fibonacci_number(n: u32) -> bool { for i in 0..=n { let (mut previous, mut current) = (0, 1); while current < i { let next = previous + current; previous = current; current = next; } if current == n { return true; } } false}- Erstellen Sie eine Funktion namens
is_fibonacci_numberdie eine nicht signierte ganze Zahl nimmt und einen boolschen Wert zurückgibt. - Iterieren Sie über alle Zahlen von
0bis zur gegebenen Zahlneinschließlich. - Initialisieren Sie unsere Fibonacci-Sequenz mit
0und1als denprevious- undcurrent-Zahlen. - Iterieren Sie solange die
currentZahl kleiner ist als die aktuelle Iterationi. - Addieren Sie die
previousundcurrentZahlen, um dienextZahl zu bekommen. - Aktualisieren Sie die
previousZahl auf diecurrentZahl. - Aktualisieren Sie die
currentZahl auf dienextZahl. - Sobald
currentgrößer oder gleich der gegebenen Zahlnist, beenden wir die Schleife. - Überprüfen Sie, ob die
currentZahl gleich der gegebenen Zahlnist und geben Sie in diesem Falltruezurück. - Andernfalls, geben Sie
falsezurück.
Nun müssen wir unsere fizz_buzz Funktion aktualisieren:
pub fn fizz_buzz_fibonacci(n: u32) -> String { if is_fibonacci_number(n) { "Fibonacci".to_string() } else { match (n % 3, n % 5) { (0, 0) => "FizzBuzz".to_string(), (0, _) => "Fizz".to_string(), (_, 0) => "Buzz".to_string(), (_, _) => n.to_string(), } }}- Benennen Sie die
fizz_buzzFunktion infizz_buzz_fibonaccium, um sie aussagekräftiger zu machen. - Rufen Sie unsere Hilfsfunktion
is_fibonacci_numberauf. - Wenn das Ergebnis von
is_fibonacci_numbertrueist, dann geben SieFibonaccizurück. - Wenn das Ergebnis von
is_fibonacci_numberfalseist, 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:
pub fn play_game(n: u32, print: bool) { let result = fizz_buzz_fibonacci(n); if print { println!("{result}"); }}Unsere Funktionen main und bench_play_game können genau gleich bleiben.
Benchmarking von FizzBuzzFibonacci
Jetzt können wir unseren Benchmark erneut ausführen:
$ cargo bench Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)play_game::bench_play_game_group::bench_play_game_100 Instructions: 331835|17902 (+1753.62%) [+18.5362x] L1 Hits: 338828|24984 (+1256.18%) [+13.5618x] LL Hits: 2|1 (+100.000%) [+2.00000x] RAM Hits: 22|20 (+10.0000%) [+1.10000x] Total read+write: 338852|25005 (+1255.14%) [+13.5514x] Estimated Cycles: 339608|25689 (+1222.00%) [+13.2200x]
Gungraun result: Ok. 1 without regressions; 0 regressed; 0 filtered; 1 benchmarks finished in 0.15254sOh, toll! Gungraun zeigt uns den Unterschied zwischen den geschätzten Zyklen unserer FizzBuzz- und FizzBuzzFibonacci-Spiele.
Ihre Zahlen werden etwas anders sein als meine.
Jedoch liegt der Unterschied zwischen den beiden Spielen wahrscheinlich im Bereich von 10-15x.
Das scheint mir gut! Besonders für das Hinzufügen einer so schick klingenden Funktion 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:
fn main() { let args: Vec<String> = std::env::args().collect(); let i = args .get(1) .map(|s| s.parse::<u32>()) .unwrap_or(Ok(15)) .unwrap_or(15); play_game(i, true);}- 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
15als 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:
$ cargo run -- 9 Compiling playground v0.0.1 (/home/bencher) Finished dev [unoptimized + debuginfo] target(s) in 0.44s Running `target/debug/game 9`Fizz$ cargo run -- 10 Finished dev [unoptimized + debuginfo] target(s) in 0.03s Running `target/debug/game 10`Buzz$ cargo run -- 13 Finished dev [unoptimized + debuginfo] target(s) in 0.04s Running `target/debug/game 13`FibonacciUnd wenn wir eine Zahl weglassen oder eine ungültige Zahl angeben:
$ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.03s Running `target/debug/game`FizzBuzz$ cargo run -- bad Finished dev [unoptimized + debuginfo] target(s) in 0.05s Running `target/debug/game bad`FizzBuzzWow, 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:
Hier zeigen Gungrauns parametrisierte Benchmarks ihre Stärke! Anstatt separate Benchmark-Funktionen für jede Eingabe zu schreiben, können wir das Attribut #[benches::...] verwenden:
#[library_benchmark]#[benches::play(100, 1_000_000)]fn bench_play_game(n: u32) { play_game(black_box(n), black_box(false));}
library_benchmark_group!( name = bench_play_game_group, benchmarks = [bench_play_game_100, bench_play_game]);- Fügen Sie das Attribut
#[benches::play(100, 1_000_000)]hinzu, um eine Benchmark-Variante mit Eingabe100und eine weitere mit Eingabe1_000_000zu erstellen. - Die Benchmark-Funktion nimmt einen Parameter
n: u32an, der jeden Wert empfängt. - Fügen Sie die Funktion
bench_play_gamezurlibrary_benchmark_group!hinzu.
Schön! Eine Benchmark-Funktion, mehrere Testfälle!
Als ich es laufen ließ, bekam ich das:
$ cargo bench Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)play_game::bench_play_game_group::bench_play_game_100 Instructions: 331835|331835 (No change) L1 Hits: 338831|338828 (+0.00089%) [+1.00001x] LL Hits: 1|2 (-50.0000%) [-2.00000x] RAM Hits: 20|22 (-9.09091%) [-1.10000x] Total read+write: 338852|338852 (No change) Estimated Cycles: 339536|339608 (-0.02120%) [-1.00021x]play_game::bench_play_game_group::bench_play_game play_0:(100) Instructions: 7072|N/A (*********) L1 Hits: 7128|N/A (*********) LL Hits: 1|N/A (*********) RAM Hits: 9|N/A (*********) Total read+write: 7138|N/A (*********) Estimated Cycles: 7448|N/A (*********)play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) Instructions: 183930316|N/A (*********) L1 Hits: 183930372|N/A (*********) LL Hits: 1|N/A (*********) RAM Hits: 9|N/A (*********) Total read+write: 183930382|N/A (*********) Estimated Cycles: 183930692|N/A (*********)
Gungraun result: Ok. 3 without regressions; 0 regressed; 0 filtered; 3 benchmarks finished in 1.45441sBenchmark in 1,45 Sekunden abgeschlossen. Das war schnell! Anstatt Benchmarks wie bei Wanduhrzeit-Benchmarks mehrfach auszuführen,
läuft jeder Gungraun-Benchmark nur einmal. Aber warten Sie, warum gibt es Änderungen im ersten Benchmark bench_play_game_100,
obwohl wir an diesem Benchmark nichts geändert haben?
Richtig, aber wir haben etwas anderes in der Benchmark-Datei geändert, und da Gungraun und Valgrind
empfindliche Instrumente sind, werden selbst sehr kleine Änderungen registriert.
Solche kleinen Änderungen, besonders in den Cache-Metriken, sind jedoch vernachlässigbar. Mit der Zeit werden Sie
ein Gefühl für kritische Änderungen in den Metriken entwickeln. Schauen wir uns unsere Ausgabe genauer an.
play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) Instructions: 183930316|N/A (*********) L1 Hits: 183930372|N/A (*********) LL Hits: 1|N/A (*********) RAM Hits: 9|N/A (*********) Total read+write: 183930382|N/A (*********) Estimated Cycles: 183930692|N/A (*********)Was! 7.448 geschätzte Zyklen x 1.000 sollte 7.448.000 geschätzte Zyklen ergeben, nicht 183.930.692 geschätzte Zyklen 🤯
Obwohl ich meinen Fibonacci-Sequenz-Code funktional richtig implementiert habe, muss ich irgendwo einen Performance-Bug haben.
FizzBuzzFibonacci in Rust korrigieren
Lassen Sie uns noch einmal einen Blick auf diese is_fibonacci_number Funktion werfen:
fn is_fibonacci_number(n: u32) -> bool { for i in 0..=n { let (mut previous, mut current) = (0, 1); while current < i { let next = previous + current; previous = current; current = next; } if current == n { return true; } } false}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) 🤦
fn is_fibonacci_number(n: u32) -> bool { let (mut previous, mut current) = (0, 1); while current < n { let next = previous + current; previous = current; current = next; } current == n}- Aktualisieren Sie Ihre
is_fibonacci_numberFunktion. - Initialisieren Sie unsere Fibonacci-Sequenz, die mit
0und1alspreviousundcurrentZahlen beginnt. - Iterieren Sie, solange die
currentZahl kleiner ist als die gegebene Zahln. - Addieren Sie die
previousundcurrentZahl, um dienextZahl zu erhalten. - Aktualisieren Sie die
previousZahl zurcurrentZahl. - Aktualisieren Sie die
currentZahl zurnextZahl. - Sobald
currentgrößer oder gleich der gegebenen Zahlnist, werden wir die Schleife verlassen. - Überprüfen Sie, ob die
currentZahl gleich der gegebenen Zahlnist und geben Sie dieses Ergebnis zurück.
Jetzt lassen Sie uns diese Benchmarks erneut ausführen und sehen, wie wir abgeschnitten haben:
$ cargo bench Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)play_game::bench_play_game_group::bench_play_game_100 Instructions: 23679|331835 (-92.8642%) [-14.0139x] L1 Hits: 30675|338831 (-90.9468%) [-11.0458x] LL Hits: 2|1 (+100.000%) [+2.00000x] RAM Hits: 19|20 (-5.00000%) [-1.05263x] Total read+write: 30696|338852 (-90.9412%) [-11.0390x] Estimated Cycles: 31350|339536 (-90.7668%) [-10.8305x]play_game::bench_play_game_group::bench_play_game play_0:(100) Instructions: 218|7072 (-96.9174%) [-32.4404x] L1 Hits: 273|7128 (-96.1700%) [-26.1099x] LL Hits: 1|1 (No change) RAM Hits: 10|9 (+11.1111%) [+1.11111x] Total read+write: 284|7138 (-96.0213%) [-25.1338x] Estimated Cycles: 628|7448 (-91.5682%) [-11.8599x]play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) Instructions: 332|183930316 (-99.9998%) [ -554007x] L1 Hits: 387|183930372 (-99.9998%) [ -475272x] LL Hits: 1|1 (No change) RAM Hits: 10|9 (+11.1111%) [+1.11111x] Total read+write: 398|183930382 (-99.9998%) [ -462137x] Estimated Cycles: 742|183930692 (-99.9996%) [ -247885x]
Gungraun result: Ok. 3 without regressions; 0 regressed; 0 filtered; 3 benchmarks finished in 0.45459sOh, wow! Unser 100-Benchmark geht um 11% zurück und unser 1_000_000-Benchmark ist um mehr als 200.000x gesunken! 183.930.692 geschätzte Zyklen auf 742 geschätzte Zyklen!
Das ist eine Reduktion von 99,9996%!
🐰 Hey, wenigstens haben wir diesen Performance-Bug gefangen, bevor er es in die Produktion geschafft hat… oh, richtig. Vergiss es…
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 Quickstart Anleitung gefolgt bin, kann ich meine Benchmarks ausführen und mit Bencher verfolgen.
$ bencher run --project game "cargo bench" Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)play_game::bench_play_game_group::bench_play_game_100 Instructions: 23679|23679 (No change) L1 Hits: 30675|30675 (No change) LL Hits: 2|2 (No change) RAM Hits: 19|19 (No change) Total read+write: 30696|30696 (No change) Estimated Cycles: 31350|31350 (No change)play_game::bench_play_game_group::bench_play_game play_0:(100) Instructions: 218|218 (No change) L1 Hits: 273|273 (No change) LL Hits: 1|1 (No change) RAM Hits: 10|10 (No change) Total read+write: 284|284 (No change) Estimated Cycles: 628|628 (No change)play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) Instructions: 332|332 (No change) L1 Hits: 387|387 (No change) LL Hits: 1|1 (No change) RAM Hits: 10|10 (No change) Total read+write: 398|398 (No change) Estimated Cycles: 742|742 (No change)
Gungraun result: Ok. 3 without regressions; 0 regressed; 0 filtered; 3 benchmarks finished in 0.45370s
Bencher New Report:...View results:- play_game::bench_play_game_group::bench_play_game play_0:(100) (Estimated Cycles): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=f03d9a6c-2b63-45c3-b34a-37149d1a7961&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_0:(100) (Instructions): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=17acf657-735b-4ece-ab32-ba857db5edce&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_0:(100) (L1 Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=009a129f-4476-4202-9e2b-cd7aed7110ac&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_0:(100) (LL Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=932a00d1-e064-4f18-81fb-aa94a5f6d5a0&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_0:(100) (RAM Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=c98672c7-8229-4e90-9773-482618b71dbf&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_0:(100) (Total read+write): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=0bd6ec91-2b29-47ea-801e-dc09338f3119&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (Estimated Cycles): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=f03d9a6c-2b63-45c3-b34a-37149d1a7961&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (Instructions): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=17acf657-735b-4ece-ab32-ba857db5edce&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (L1 Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=009a129f-4476-4202-9e2b-cd7aed7110ac&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (LL Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=932a00d1-e064-4f18-81fb-aa94a5f6d5a0&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (RAM Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=c98672c7-8229-4e90-9773-482618b71dbf&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (Total read+write): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=0bd6ec91-2b29-47ea-801e-dc09338f3119&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (Estimated Cycles): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=f03d9a6c-2b63-45c3-b34a-37149d1a7961&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (Instructions): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=17acf657-735b-4ece-ab32-ba857db5edce&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (L1 Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=009a129f-4476-4202-9e2b-cd7aed7110ac&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (LL Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=932a00d1-e064-4f18-81fb-aa94a5f6d5a0&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (RAM Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=c98672c7-8229-4e90-9773-482618b71dbf&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (Total read+write): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=0bd6ec91-2b29-47ea-801e-dc09338f3119&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409Mit 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 Tools für kontinuierliches Benchmarking. Hatten Sie jemals eine Performance Regression, die Ihre Nutzer beeinflusste? Bencher hätte das verhindern können. Bencher ermöglicht es Ihnen, Leistungsregressionen zu erkennen und zu verhindern, bevor sie gemergt werden.
- Ausführen: Führen Sie Ihre Benchmarks lokal oder in CI mit exakt denselben Bare-Metal-Runnern und Ihren bevorzugten Benchmarking-Tools aus. Das
bencherCLI orchestriert die Ausführung Ihrer Benchmarks auf Bare Metal 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 lokal oder in CI mit exakt derselben Bare-Metal-Hardware ab. Bencher verwendet modernste, anpassbare Analysen, um Leistungsregressionen zu erkennen, bevor sie gemergt werden.
Aus denselben Gründen, warum Unit Tests laufen, um Feature Regressionen zu verhindern, sollten Benchmarks mit Bencher ausgeführt werden, um Leistungsregressionen zu verhindern. Performance-Bugs sind Fehler!
Beginnen Sie damit, Leistungsregressionen aufzufangen - probieren Sie Bencher Cloud kostenlos aus.