Wie man Rust-Code mit Iai benchmarkt


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 erledigen kann. Dieser oft übersehene Schritt in der Softwareentwicklung ist entscheidend für die Erstellung und Aufrechterhaltung von schnellem und leistungsfähigem Code. Benchmarking liefert die notwendigen Metriken, damit Entwickler verstehen, wie gut ihr Code unter verschiedenen Arbeitslasten und Bedingungen funktioniert. Aus denselben Gründen, aus denen Sie Unit- und Integrationstests schreiben, um Funktionsregressionen zu verhindern, sollten Sie Benchmarks schreiben, um Leistungsregressionen zu verhindern. Performance-Bugs sind Fehler!

Was ist Rust?

Rust ist eine Open-Source-Programmiersprache, die Geschwindigkeit, Zuverlässigkeit und Produktivität betont. Es erreicht Speichersicherheit ohne die Notwendigkeit eines Garbage Collectors.

Sie sollten Rust in Betracht ziehen, wenn Sie schreiben:

  • Ein Low-Level-Programm, bei dem die Leistung wichtig ist
  • Gemeinsam genutzte Bibliothek, die von mehreren verschiedenen Sprachen verwendet wird
  • Komplexe Command Line Interface (CLI)
  • Langlebiges Softwareprojekt mit vielen Mitwirkenden

Rust legt großen Wert auf die Produktivität der Entwickler. Cargo ist der offizielle Paketmanager und erledigt viele Aufgaben wie:

  • Verwaltung von Projekt-Abhängigkeiten
  • Kompilieren von Binärdateien, Tests und Benchmarks
  • Linting
  • Formatierung

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 bis 100 (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 main Funktion
  • Iteriere von 1 bis 100 einschließlich.
  • Berechne für jede Zahl den Modulus (Rest nach der Division) sowohl für 3 als auch für 5.
  • 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 und 5 beide 0 ist, dann drucke FizzBuzz.
  • Wenn der Rest nur für 3 0 ist, dann drucke Fizz.
  • Wenn der Rest nur für 5 0 ist, dann drucke Buzz.
  • 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.rs

Sie 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`
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
...
97
98
Fizz
Buzz

🐰 Boom! Sie meistern das Coding-Interview!

Eine neue Datei Cargo.lock sollte erzeugt worden sein:

game
├── Cargo.lock
├── Cargo.toml
└── src
└── main.rs

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 beliebtesten Optionen für das Benchmarking in Rust sind: libtest bench, Criterion und Iai.

libtest ist das eingebaute Unit-Testing- und Benchmarking-Framework von Rust. Obwohl es Teil der Rust-Standardbibliothek ist, wird libtest bench immer noch als instabil angesehen, so dass es nur auf nightly Compiler-Releases verfügbar ist. Um auf dem stabilen Rust-Compiler zu arbeiten, muss ein separates Benchmarking-Harness verwendet werden. Beide werden jedoch nicht aktiv entwickelt.

Das aktivste Benchmarking-Harness im Rust-Ökosystem ist Criterion. Es funktioniert sowohl auf stabilen als auch auf nightly Rust-Compiler-Releases und ist zum de facto Standard in der Rust-Community geworden. Criterion ist auch deutlich feature-reicher im Vergleich zu libtest bench.

Eine experimentelle Alternative zu Criterion ist Iai, vom selben Ersteller wie Criterion. Allerdings verwendet es Anweisungszählungen statt der Wanduhrzeit: CPU-Anweisungen, L1-Zugriffe, L2-Zugriffe und RAM-Zugriffe. Dies ermöglicht Single-Shot-Benchmarking, da diese Metriken zwischen den Läufen nahezu identisch bleiben sollten.

Alle drei werden unterstützt von Bencher. Warum also Iai wählen? Iai verwendet Anweisungszähler statt Echtzeit. Das macht es ideal für dauerndes Benchmarking, also Benchmarking in CI. Ich würde vorschlagen, Iai für dauerndes Benchmarking zu verwenden, besonders wenn Sie gemeinsam genutzte Runner verwenden. Es ist wichtig zu verstehen, dass Iai nur ein Proxy für das misst, was Sie wirklich interessiert. Bedeutet der Wechsel von 1.000 Anweisungen auf 2.000 Anweisungen eine Verdoppelung der Latenz Ihrer Anwendung? Vielleicht ja, vielleicht nein. Aus diesem Grund kann es nützlich sein, auch Echtzeit-basierte Benchmarks parallel zu Anweisungszähler-basierten Benchmarks durchzuführen.

🐰 Iai wurde seit über 3 Jahren nicht mehr aktualisiert. Daher könnten Sie in Betracht ziehen, Iai-Callgrind zu verwenden.

Install Valgrind

Iai verwendet ein Tool namens Valgrind zur Erfassung von Anweisungszählern. Valgrind unterstützt Linux, Solaris, FreeBSD und MacOS. Die Unterstützung von MacOS ist jedoch auf x86_64-Prozessoren beschränkt, da arm64-Prozessoren (M1, M2 usw.) noch nicht unterstützt werden.

Auf Debian laufen: sudo apt-get install valgrind

Auf MacOS (x86_64/Intel-Chip only): brew install valgrind

Refactoring von FizzBuzz

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. Um dies zu tun, müssen wir einige Änderungen vornehmen.

Erstelle unter src eine neue Datei namens lib.rs:

game
├── Cargo.lock
├── Cargo.toml
└── src
└── lib.rs
└── main.rs

Füge 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 Ganzzahl n entgegen, ruft fizz_buzz mit dieser Zahl auf und gibt das Ergebnis aus, wenn print true ist.
  • fizz_buzz: Nimmt eine vorzeichenlose Ganzzahl n entgegen und führt die tatsächliche Fizz, Buzz, FizzBuzz oder Zahlenlogik aus und gibt das Ergebnis als Zeichenkette zurück.

Aktualisiere dann main.rs, um so auszusehen:

use game::play_game;
fn main() {
for i in 1..=100 {
play_game(i, true);
}
}
  • game::play_game: Importiere play_game aus der game Crate, die wir gerade mit lib.rs erstellt haben.
  • main: Der Haupt-Einstiegspunkt in unser Programm, welcher durch die Zahlen 1 bis 100 inklusive iteriert und play_game für jede Zahl aufruft, wobei print auf true gesetzt ist.

Benchmarking von FizzBuzz

Um unseren Code zu benchmarken, benötigen wir ein benches Verzeichnis und fügen eine Datei hinzu, um unsere Benchmarks zu enthalten, play_game.rs:

game
├── Cargo.lock
├── Cargo.toml
└── benches
└── play_game.rs
└── src
└── lib.rs
└── main.rs

Fügen Sie in play_game.rs den folgenden Code hinzu:

use game::play_game;
fn bench_play_game() {
iai::black_box(for i in 1..=100 {
play_game(i, false)
});
}
iai::main!(bench_play_game);
  • Importieren Sie die Funktion play_game aus unserer game crate.
  • Erstellen Sie eine Funktion namens bench_play_game.
  • Führen Sie unser Macro-Benchmark in einer “Black Box” aus, damit der Compiler unseren Code nicht optimiert.
  • Zählen Sie von 1 bis 100 einschließlich.
  • Rufen Sie für jede Zahl play_game auf, mit print auf false gesetzt.

Jetzt müssen wir das game crate konfigurieren, um unsere Benchmarks auszuführen.

Fügen Sie folgendes am Ende Ihrer Cargo.toml Datei hinzu:

[dev-dependencies]
iai = "0.1"
[[bench]]
name = "play_game"
harness = false
  • iai: Fügen Sie iai als Entwicklungsabhängigkeit hinzu, da wir es nur für Performance-Tests verwenden.
  • bench: Registrieren Sie play_game als Benchmark und stellen Sie harness auf false, da wir Iai als unser Benchmarking-Gestell verwenden.

Jetzt sind wir bereit, unseren Code zu benchmarken, laufen Sie cargo bench:

$ cargo bench
Compiling iai v0.1.1
Compiling game v0.1.0 (/home/bencher)
Finished bench [optimized] target(s) in 2.55s
Running unittests src/lib.rs (target/release/deps/game-9b1b504669ca4b29)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/release/deps/game-8d61ca5a97299729)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running benches/play_game.rs (target/release/deps/play_game-6896309faf45cd96)
bench_play_game
Instructions: 34370
L1 Accesses: 50373
L2 Accesses: 9
RAM Accesses: 35
Estimated Cycles: 51643

🐰 Salat Rübe die Rübe! Wir haben unsere ersten Benchmark-Messwerte!

Endlich können wir unsere müden Entwicklerköpfe zur Ruhe legen… 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 bis 100 (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_number die eine nicht signierte ganze Zahl nimmt und einen boolschen Wert zurückgibt.
  • Iterieren Sie über alle Zahlen von 0 bis zur gegebenen Zahl n einschließlich.
  • Initialisieren Sie unsere Fibonacci-Sequenz mit 0 und 1 als den previous- und current-Zahlen.
  • Iterieren Sie solange die current Zahl kleiner ist als die aktuelle Iteration i.
  • Addieren Sie die previous und current Zahlen, um die next Zahl zu bekommen.
  • Aktualisieren Sie die previous Zahl auf die current Zahl.
  • Aktualisieren Sie die current Zahl auf die next Zahl.
  • Sobald current größer oder gleich der gegebenen Zahl n ist, beenden wir die Schleife.
  • Überprüfen Sie, ob die current Zahl gleich der gegebenen Zahl n ist und geben Sie in diesem Fall true zurück.
  • Andernfalls, geben Sie false zurü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_buzz Funktion in fizz_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 Sie Fibonacci zurück.
  • Wenn das Ergebnis von is_fibonacci_number false ist, dann führen Sie die gleiche Fizz, 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 unser Benchmark erneut ausführen:

$ cargo bench
Compiling game v0.1.0 (/home/bencher)
Finished bench [optimized] target(s) in 2.20s
Running unittests src/lib.rs (target/release/deps/game-9b1b504669ca4b29)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/release/deps/game-8d61ca5a97299729)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running benches/play_game.rs (target/release/deps/play_game-6896309faf45cd96)
bench_play_game
Instructions: 304598 (+786.2322%)
L1 Accesses: 320024 (+535.3086%)
L2 Accesses: 8 (-11.11111%)
RAM Accesses: 42 (+20.00000%)
Estimated Cycles: 321534 (+522.6091%)

Oh, toll! Iai sagt uns, dass der Unterschied zwischen den geschätzten Zyklen unserer FizzBuzz und FizzBuzzFibonacci-Spiele +522,6091% beträgt. Ihre Zahlen werden ein wenig anders als meine sein. Der Unterschied zwischen den beiden Spielen liegt jedoch wahrscheinlich im Bereich von 5x. Das scheint mir gut zu sein! Vor allem für die Hinzufügung eines so ausgefallenen Features wie Fibonacci zu unserem Spiel. Die Kinder werden es lieben!

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 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:

$ 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`
Fibonacci

Und 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`
FizzBuzz

Wow, das war ein gründlicher Test! CI besteht. Unsere Chefs sind begeistert. Lasst uns es ausliefern! 🚀

Das Ende


SpongeBob Schwammkopf Drei Wochen später
Dies ist in Ordnung Meme

🐰 … 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:

fn bench_play_game_100() {
iai::black_box(play_game(100, false));
}
fn bench_play_game_1_000_000() {
iai::black_box(play_game(1_000_000, false));
}
  • Ein Mikro-Benchmark bench_play_game_100 zum Spielen des Spiels mit der Zahl einhundert (100)
  • Ein Mikro-Benchmark bench_play_game_1_000_000 zum Spielen des Spiels mit der Zahl eine Million (1_000_000)

Als ich es laufen ließ, bekam ich dies:

$ cargo bench
Compiling game v0.1.0 (/home/bencher)
Finished bench [optimized] target(s) in 1.92s
Running unittests src/lib.rs (target/release/deps/game-9b1b504669ca4b29)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/release/deps/game-8d61ca5a97299729)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running benches/play_game.rs (target/release/deps/play_game-6896309faf45cd96)
bench_play_game
Instructions: 304598 (No change)
L1 Accesses: 320025 (+0.000312%)
L2 Accesses: 7 (-12.50000%)
RAM Accesses: 42 (No change)
Estimated Cycles: 321530 (-0.001244%)
bench_play_game_100
Instructions: 6194
L1 Accesses: 6290
L2 Accesses: 2
RAM Accesses: 11
Estimated Cycles: 6685

Warten Sie darauf… warten Sie darauf…

bench_play_game_1_000_000
Instructions: 155108715
L1 Accesses: 155108811
L2 Accesses: 2
RAM Accesses: 11
Estimated Cycles: 155109206

Was?! 6,685 geschätzte Zyklen x 1,000 sollten 6,685,000 geschätzte Zyklen sein, nicht 155,109,206 geschätzte Zyklen 🤯 Obwohl ich meinen Fibonacci-Sequenz-Code funktional korrekt bekommen habe, muss ich irgendwo einen Leistungsfehler drin 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_number Funktion.
  • Initialisieren Sie unsere Fibonacci-Sequenz, die mit 0 und 1 als previous und current Zahlen beginnt.
  • Iterieren Sie, solange die current Zahl kleiner ist als die gegebene Zahl n.
  • Addieren Sie die previous und current Zahl, um die next Zahl zu erhalten.
  • Aktualisieren Sie die previous Zahl zur current Zahl.
  • Aktualisieren Sie die current Zahl zur next Zahl.
  • Sobald current größer oder gleich der gegebenen Zahl n ist, werden wir die Schleife verlassen.
  • Überprüfen Sie, ob die current Zahl gleich der gegebenen Zahl n ist und geben Sie dieses Ergebnis zurück.

Jetzt lassen Sie uns diese Benchmarks erneut laufen und sehen, wie es uns ergangen ist:

$ cargo bench
Compiling game v0.1.0 (/home/bencher)
Finished bench [optimized] target(s) in 4.22s
Running unittests src/lib.rs (target/release/deps/game-9b1b504669ca4b29)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/release/deps/game-8d61ca5a97299729)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running benches/play_game.rs (target/release/deps/play_game-6896309faf45cd96)
bench_play_game
Instructions: 38313 (-87.42178%)
L1 Accesses: 53739 (-83.20787%)
L2 Accesses: 7 (No change)
RAM Accesses: 43 (+2.380952%)
Estimated Cycles: 55279 (-82.80751%)
bench_play_game_100
Instructions: 295 (-95.23733%)
L1 Accesses: 389 (-93.81558%)
L2 Accesses: 2 (No change)
RAM Accesses: 13 (+18.18182%)
Estimated Cycles: 854 (-87.22513%)
bench_play_game_1_000_000
Instructions: 391 (-99.99975%)
L1 Accesses: 485 (-99.99969%)
L2 Accesses: 2 (No change)
RAM Accesses: 13 (+18.18182%)
Estimated Cycles: 950 (-99.99939%)

Oh, wow! Unser Benchmark bench_play_game ist wieder etwa da, wo es für den originalen FizzBuzz war. Ich wünschte, ich könnte mich genau an diesen Score erinnern. Es sind jedoch drei Wochen vergangen. Meine Terminalhistorie reicht nicht so weit zurück. Und Iai vergleicht nur mit dem neuesten Resultat. Aber ich denke, es ist nahe dran!

Der Benchmark bench_play_game_100 ist fast 10x down, -87,22513%. Und der Benchmark bench_play_game_1_000_000 ist mehr als 10,000x down! Von 155,109,206 geschätzten Zyklen auf 950 geschätzte geschätzte Zyklen! Das ist -99,99939%!

🐰 Hey, zumindest haben wir diesen Performance-Bug erwischt, bevor er es bis zur Produktion geschafft hat… oh, richtig. Leider nicht…

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.

$ bencher run --project game "cargo bench"
Finished bench [optimized] target(s) in 0.18s
Running unittests src/lib.rs (target/release/deps/game-9b1b504669ca4b29)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/release/deps/game-8d61ca5a97299729)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running benches/play_game.rs (target/release/deps/play_game-6896309faf45cd96)
bench_play_game
Instructions: 38331 (+0.046981%)
L1 Accesses: 53765 (+0.048382%)
L2 Accesses: 6 (-14.28571%)
RAM Accesses: 45 (+4.651163%)
Estimated Cycles: 55370 (+0.164619%)
bench_play_game_100
Instructions: 313 (+6.101695%)
L1 Accesses: 416 (+6.940874%)
L2 Accesses: 2 (No change)
RAM Accesses: 13 (No change)
Estimated Cycles: 881 (+3.161593%)
bench_play_game_1_000_000
Instructions: 409 (+4.603581%)
L1 Accesses: 512 (+5.567010%)
L2 Accesses: 2 (No change)
RAM Accesses: 13 (No change)
Estimated Cycles: 977 (+2.842105%)
Finished bench [optimized] target(s) in 0.07s
Running unittests src/lib.rs (target/release/deps/game-13f4bad779fbfde4)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Bencher New Report:
...
View results:
- bench_play_game (Latency): https://bencher.dev/console/projects/game/perf?measures=52507e04-ffd9-4021-b141-7d4b9f1e9194&branches=3a27b3ce-225c-4076-af7c-75adbc34ef9a&testbeds=bc05ed88-74c1-430d-b96a-5394fdd18bb0&benchmarks=077449e5-5b45-4c00-bdfb-3a277413180d&start_time=1697224006000&end_time=1699816009000&upper_boundary=true
- bench_play_game_100 (Latency): https://bencher.dev/console/projects/game/perf?measures=52507e04-ffd9-4021-b141-7d4b9f1e9194&branches=3a27b3ce-225c-4076-af7c-75adbc34ef9a&testbeds=bc05ed88-74c1-430d-b96a-5394fdd18bb0&benchmarks=96508869-4fa2-44ac-8e60-b635b83a17b7&start_time=1697224006000&end_time=1699816009000&upper_boundary=true
- bench_play_game_1_000_000 (Latency): https://bencher.dev/console/projects/game/perf?measures=52507e04-ffd9-4021-b141-7d4b9f1e9194&branches=3a27b3ce-225c-4076-af7c-75adbc34ef9a&testbeds=bc05ed88-74c1-430d-b96a-5394fdd18bb0&benchmarks=ff014217-4570-42ea-8813-6ed0284500a4&start_time=1697224006000&end_time=1699816009000&upper_boundary=true

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

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.

🤖 Dieses Dokument wurde automatisch von OpenAI GPT-4 generiert. Es ist möglicherweise nicht korrekt und kann Fehler enthalten. Wenn Sie Fehler finden, öffnen Sie bitte ein Problem auf GitHub.