Comment faire un benchmark du code Rust avec Gungraun

Everett Pompeii

Everett Pompeii


Qu’est-ce que le Benchmarking ?

Le benchmarking est la pratique consistant à tester les performances de votre code pour voir à quelle vitesse (latence) ou combien (débit) de travail il peut effectuer. Cette étape souvent négligée dans le développement logiciel est cruciale pour créer et maintenir un code rapide et performant. Le benchmarking fournit les métriques nécessaires aux développeurs pour comprendre comment leur code se comporte sous diverses charges de travail et conditions. Pour les mêmes raisons que vous écrivez des tests unitaires et d’intégration pour éviter les régressions de fonctionnalités, vous devriez écrire des benchmarks pour éviter les régressions de performances. Les bugs de performance sont des bugs !

Écrire FizzBuzz en Rust

Pour écrire des benchmarks, nous avons besoin de code source à évaluer. Pour commencer, nous allons écrire un programme très simple, FizzBuzz.

Les règles pour FizzBuzz sont les suivantes :

Écrivez un programme qui imprime les entiers de 1 à 100 (inclus) :

  • Pour les multiples de trois, imprimez Fizz
  • Pour les multiples de cinq, imprimez Buzz
  • Pour les multiples de trois et de cinq, imprimez FizzBuzz
  • Pour tous les autres, imprimez le numéro

Il existe de nombreuses façons d’écrire FizzBuzz. Nous allons donc choisir ma préférée :

fn main() {
for i in 1..=100 {
match (i % 3, i % 5) {
(0, 0) => println!("FizzBuzz"),
(0, _) => println!("Fizz"),
(_, 0) => println!("Buzz"),
(_, _) => println!("{i}"),
}
}
}
  • Créer une fonction main
  • Itérer de 1 à 100 inclusivement.
  • Pour chaque nombre, calculez le modulo (reste après division) pour 3 et 5.
  • Utilisez le pattern matching sur les deux restes. Si le reste est 0, alors le nombre est un multiple du facteur donné.
  • Si le reste est 0 pour 3 et 5, alors imprimez FizzBuzz.
  • Si le reste est 0 pour seulement 3, alors imprimez Fizz.
  • Si le reste est 0 pour seulement 5, alors imprimez Buzz.
  • Sinon, imprimez simplement le nombre.

Suivre étape par étape

Pour suivre ce tutoriel étape par étape, vous devrez installer Rust.

🐰 Le code source de ce post est disponible sur GitHub

Avec Rust installé, vous pouvez alors ouvrir une fenêtre de terminal et entrer : cargo init game

Ensuite, naviguez dans le nouveau répertoire game créé.

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

Vous devriez voir un répertoire appelé src avec un fichier nommé main.rs :

fn main() {
println!("Hello, world!");
}

Remplacez son contenu par l’implémentation FizzBuzz ci-dessus. Puis exécutez cargo run. Le résultat devrait ressembler à :

$ 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

🐰 Bam ! Vous craquez l’entretien de codage !

Un nouveau fichier Cargo.lock devrait avoir été généré :

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

Avant de continuer, il est important de discuter des différences entre le micro-benchmarking et le macro-benchmarking.

Micro-Benchmarking vs Macro-Benchmarking

Il existe deux grandes catégories de benchmarks logiciels : les micro-benchmarks et les macro-benchmarks. Les micro-benchmarks fonctionnent à un niveau similaire aux tests unitaires. Par exemple, un benchmark pour une fonction qui détermine Fizz, Buzz, ou FizzBuzz pour un seul nombre serait un micro-benchmark. Les macro-benchmarks fonctionnent à un niveau similaire aux tests d’intégration. Par exemple, un benchmark pour une fonction qui joue l’ensemble du jeu de FizzBuzz, de 1 à 100, serait un macro-benchmark.

Généralement, il est préférable de tester au niveau le plus bas d’abstraction possible. Dans le cas des benchmarks, cela les rend à la fois plus faciles à maintenir, et cela aide à réduire le bruit dans les mesures. Cependant, tout comme avoir des tests de bout en bout peut être très utile pour vérifier la cohérence de l’ensemble du système tel que prévu, avoir des macro-benchmarks peut être très utile pour s’assurer que les chemins critiques à travers votre logiciel restent performants.

Benchmarking en Rust

Les trois options populaires pour le benchmarking en Rust sont : libtest bench, Criterion, et Iai.

libtest est le framework intégré de tests unitaires et de benchmarking de Rust. Bien qu’il fasse partie de la bibliothèque standard de Rust, libtest bench est encore considéré comme instable, il n’est donc disponible que sur les versions nightly du compilateur. Pour fonctionner sur le compilateur Rust stable, un harnais de benchmarking séparé doit être utilisé. Cependant, aucun des deux n’est activement développé.

Le harnais de benchmarking le plus populaire dans l’écosystème Rust est Criterion. Il fonctionne à la fois sur les versions stables et nightly du compilateur Rust, et il est devenu le standard de facto au sein de la communauté Rust. Criterion est également beaucoup plus riche en fonctionnalités comparé à libtest bench.

Une alternative expérimentale à Criterion est Iai, créé par le même auteur que Criterion. Cependant, il utilise des comptes d’instructions plutôt que du temps d’horloge murale : instructions CPU, accès L1, accès L2 et accès RAM. Cela permet un benchmarking en une seule passe puisque ces métriques devraient rester quasiment identiques entre les exécutions.

Les quatre sont pris en charge par Bencher. Alors pourquoi choisir Gungraun (le successeur renommé d’Iai-Callgrind) ? Gungraun utilise des compteurs d’instructions au lieu du temps réel. Cela le rend idéal pour le benchmarking continu, c’est-à-dire le benchmarking en CI. Je suggérerais d’utiliser Gungraun pour le benchmarking continu, surtout si vous utilisez des runners partagés. Gungraun est activement maintenu et dispose d’une documentation en ligne complète, ce qui en fait un choix fiable pour les projets à long terme. Il est important de comprendre que Gungraun ne mesure qu’un proxy de ce qui vous intéresse vraiment. Passer de 1 000 instructions à 2 000 instructions double-t-il la latence de votre application ? Peut-être oui, peut-être non. Pour cette raison, il peut être utile d’exécuter également des benchmarks basés sur le temps réel en parallèle avec des benchmarks basés sur le comptage d’instructions.

Installer Valgrind

Gungraun utilise un outil appelé Valgrind pour collecter les compteurs d’instructions. Valgrind prend en charge Linux, Solaris, FreeBSD et macOS. Cependant, le support macOS est limité aux processeurs x86_64 car les processeurs arm64 (M1, M2, etc.) ne sont pas encore pris en charge.

Sur Debian exécutez : sudo apt-get install valgrind

Sur macOS (puce x86_64/Intel uniquement) : brew install valgrind

Refactoriser FizzBuzz

Pour tester notre application FizzBuzz, nous découplons notre logique de la fonction main de notre programme. Contrairement à d’autres harnais de benchmark, Gungraun peut faire le benchmark du binaire de benchmark et de la fonction main, mais c’est purement du macro-benchmarking. Nous voulons faire les deux, macro et micro. Pour ce faire, nous devons apporter quelques modifications.

Sous src, créez un nouveau fichier nommé lib.rs :

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

Ajoutez le code suivant à lib.rs :

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 : Prend un entier non signé n, appelle fizz_buzz avec ce nombre, et si print est true, affiche le résultat.
  • fizz_buzz : Prend un entier non signé n et exécute la logique réelle de Fizz, Buzz, FizzBuzz ou nombre, retournant le résultat sous forme de chaîne.

Ensuite, le main.rs mis à jour ressemble à ceci :

use game::play_game;
fn main() {
for i in 1..=100 {
play_game(i, true);
}
}
  • game::play_game : Importe play_game du crate game que nous venons de créer avec lib.rs.
  • main : Le point d’entrée principal de notre programme qui itère à travers les nombres de 1 à 100 inclus et appelle play_game pour chaque nombre, avec print défini sur true.

Benchmarking de FizzBuzz

Pour faire le benchmark de notre code, nous devons créer un répertoire benches et ajouter un fichier pour contenir nos benchmarks, play_game.rs. Notez que nous dévions de la méthode recommandée pour structurer les benchmarks par souci de simplicité. Pour votre projet, vous devriez suivre les recommandations :

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

Dans play_game.rs, ajoutez le code suivant :

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);
  • Importez le module gungraun::prelude qui apporte les macros nécessaires.
  • Utilisez std::hint::black_box pour empêcher le compilateur d’optimiser notre benchmark.
  • Importez la fonction play_game de notre crate game.
  • Créez une fonction de benchmark de bibliothèque nommée bench_play_game en utilisant l’attribut #[library_benchmark].
  • Itérez de 1 à 100 et appelez play_game avec print défini sur false.
  • Créez un groupe de benchmarks de bibliothèque nommé bench_play_game_group contenant notre benchmark bench_play_game.
  • Utilisez le macro main! pour exécuter le groupe de benchmarks.

Maintenant, nous devons configurer le crate game pour exécuter nos benchmarks.

Ajoutez ce qui suit au bas de votre fichier Cargo.toml :

[dev-dependencies]
gungraun = "0.18.0"
[[bench]]
name = "play_game"
harness = false
[profile.bench]
debug = true
  • gungraun : Ajoutez gungraun comme dépendance de développement, puisque nous ne l’utilisons que pour les tests de performance.
  • bench : Enregistrez play_game comme benchmark et définissez harness sur false, car nous utiliserons Gungraun comme notre harnais de benchmarking.
  • debug = true : Activez les informations de débogage dans les builds de benchmark, ce qui est requis pour que Gungraun fournisse une sortie détaillée.

Maintenant nous sommes prêts à faire le benchmark de notre code, exécutez cargo bench :

$ 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

🐰 Laitue navet la betterave ! Nous avons nos premières métriques de benchmark !

Enfin, nous pouvons reposer nos têtes fatiguées de développeurs… C’est une blague, nos utilisateurs veulent une nouvelle fonctionnalité !

Écrire FizzBuzzFibonacci en Rust

Nos indicateurs de performance clés (KPIs) sont en baisse, donc notre chef de produit (PM) veut que nous ajoutions une nouvelle fonctionnalité. Après beaucoup de brainstorming et de nombreuses interviews d’utilisateurs, il est décidé que le bon vieux FizzBuzz ne suffit pas. Les gens d’aujourd’hui veulent un nouveau jeu, FizzBuzzFibonacci.

Les règles du FizzBuzzFibonacci sont les suivantes :

Écrivez un programme qui imprime les entiers de 1 à 100 (inclus) :

  • Pour les multiples de trois, imprimez Fizz
  • Pour les multiples de cinq, imprimez Buzz
  • Pour les multiples de trois et cinq, imprimez FizzBuzz
  • Pour les nombres qui font partie de la séquence de Fibonacci, imprimez uniquement Fibonacci
  • Pour tous les autres, imprimez le nombre

La séquence de Fibonacci est une séquence dans laquelle chaque nombre est la somme des deux précédents. Par exemple, en commençant par 0 et 1, le prochain nombre dans la séquence de Fibonacci serait 1. Suivi par : 2, 3, 5, 8 et ainsi de suite. Les nombres qui font partie de la séquence de Fibonacci sont connus sous le nom de nombres de Fibonacci. Nous allons donc devoir écrire une fonction qui détecte les nombres de Fibonacci.

Il y a plusieurs façons de générer la séquence de Fibonacci et plusieurs façons de détecter un nombre de Fibonacci. Nous allons donc choisir ma méthode préférée :

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
}
  • Créez une fonction nommée is_fibonacci_number qui prend un entier non signé et retourne un booléen.
  • Itérer pour tous les nombres de 0 à notre nombre donné n inclus.
  • Initialiser notre séquence Fibonacci en commençant par 0 et 1 comme les nombres previous et current respectivement.
  • Itérer pendant que le nombre current est inférieur au nombre d’itération i en cours.
  • Ajoutez le nombre previous et le nombre current pour obtenir le nombre next.
  • Mettre à jour le nombre previous avec le nombre current.
  • Mettre à jour le nombre current avec le nombre next.
  • Une fois que current est supérieur ou égal au nombre donné n, nous sortirons de la boucle.
  • Vérifiez si le nombre current est égal au nombre donné n et si c’est le cas retourner true.
  • Sinon, retournez false.

Maintenant, nous devrons mettre à jour notre fonction fizz_buzz:

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(),
}
}
}
  • Renommez la fonction fizz_buzz en fizz_buzz_fibonacci pour la rendre plus descriptive.
  • Appelez notre fonction associée is_fibonacci_number.
  • Si le résultat de is_fibonacci_number est true, alors retournez Fibonacci.
  • Si le résultat de is_fibonacci_number est false, alors effectuez la même logique Fizz,Buzz, FizzBuzz, ou nombre en retournant le résultat.

Parce que nous renommons fizz_buzz en fizz_buzz_fibonacci, il nous faut également mettre à jour notre fonction play_game:

pub fn play_game(n: u32, print: bool) {
let result = fizz_buzz_fibonacci(n);
if print {
println!("{result}");
}
}

Les fonctions main et bench_play_game peuvent rester exactement les mêmes.

Benchmarking de FizzBuzzFibonacci

Maintenant nous pouvons relancer notre benchmark :

$ 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.15254s

Oh, chouette ! Gungraun nous indique la différence entre les cycles estimés de nos jeux FizzBuzz et FizzBuzzFibonacci. Vos chiffres seront un peu différents des miens. Cependant, la différence entre les deux jeux est probablement dans la plage de 10-15x. Ça me semble bien ! Surtout pour avoir ajouté une fonctionnalité aussi élégante que Fibonacci à notre jeu. Les enfants vont adorer !

Étendre FizzBuzzFibonacci en Rust

Notre jeu est un succès! Les enfants adorent jouer à FizzBuzzFibonacci. Tellement que les dirigeants veulent une suite. Mais c’est le monde moderne, nous avons besoin de revenus récurrents annuels (ARR) et non de ventes uniques! La nouvelle vision de notre jeu est qu’il est sans fin, plus besoin de vivre entre les limites de 1 et 100 (même si c’est inclusif). Non, nous partons vers de nouveaux horizons!

Les règles pour Open World FizzBuzzFibonacci sont les suivantes :

Écrivez un programme qui prend en entrée n’importe quel nombre entier positif et affiche :

  • Pour les multiples de trois, affichez Fizz
  • Pour les multiples de cinq, affichez Buzz
  • Pour les multiples à la fois de trois et de cinq, affichez FizzBuzz
  • Pour les nombres qui font partie de la séquence de Fibonacci, affichez uniquement Fibonacci
  • Pour tous les autres, affichez le nombre

Pour faire fonctionner notre jeu pour n’importe quel nombre, nous devrons accepter un argument de ligne de commande. Mettez à jour la fonction main pour qu’elle ressemble à ceci :

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);
}
  • Collectez tous les arguments (args) passés à notre jeu depuis la ligne de commande.
  • Obtenez le premier argument passé à notre jeu et analysez-le comme un entier non signé i.
  • Si l’analyse échoue ou si aucun argument n’est transmis, par défaut, jouez à notre jeu avec 15 comme entrée.
  • Enfin, jouez à notre jeu avec le nouvel entier non signé i analysé.

Maintenant, nous pouvons jouer à notre jeu avec n’importe quel nombre! Utilisez cargo run suivi de -- pour passer des arguments à notre jeu :

$ 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

Et si nous oublions de fournir un numéro ou fournissons un numéro invalide :

$ 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, c’était des tests très approfondis! CI passe. Nos patrons sont ravis. Expédions-le! 🚀

La fin


SpongeBob SquarePants Trois semaines plus tard
Meme C'est bien

🐰 … la fin de votre carrière peut-être ?


Rien que pour rire! Tout est en feu! 🔥

Au début, tout semblait aller bien. Et puis à 02h07 du matin le samedi, mon bip a sonné :

📟 Votre jeu est en feu! 🔥

Après me être précipité hors du lit, j’ai essayé de comprendre ce qui se passait. J’ai essayé de rechercher dans les journaux, mais c’était difficile parce que tout continuait de s’effondrer. Finalement, j’ai trouvé le problème. Les enfants ! Ils adorent notre jeu tellement, qu’ils jouaient jusqu’à un million! Dans un éclair de génie, j’ai ajouté deux nouveaux points de référence :

C’est là que les benchmarks paramétrés de Gungraun brillent ! Au lieu d’écrire des fonctions de benchmark séparées pour chaque entrée, nous pouvons utiliser l’attribut #[benches::...] :

#[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]
);
  • Ajoutez l’attribut #[benches::play(100, 1_000_000)] pour créer une variante de benchmark avec l’entrée 100 et une autre avec l’entrée 1_000_000.
  • La fonction de benchmark prend un paramètre n: u32 qui reçoit chaque valeur.
  • Ajoutez la fonction bench_play_game au library_benchmark_group!

Super ! Une fonction de benchmark, plusieurs cas de test !

Quand je l’ai exécuté, j’ai obtenu ceci :

$ 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.45441s

Benchmark terminé en 1,45 secondes. C’était rapide ! Au lieu d’exécuter les benchmarks plusieurs fois comme le font les benchmarks de temps réel, chaque benchmark Gungraun ne s’exécute qu’une seule fois. Mais attendez, pourquoi y a-t-il des changements dans le premier benchmark bench_play_game_100, bien que nous n’ayons rien changé dans ce benchmark ? C’est exact, mais nous avons changé quelque chose de différent dans le fichier de benchmark et puisque Gungraun et Valgrind sont des instruments sensibles, même de très petits changements sont enregistrés. Cependant, de si petits changements, surtout dans les métriques de cache, sont négligeables. Avec le temps, vous développerez un sens pour les changements critiques dans les métriques. Examinons notre sortie de plus près.

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 (*********)

Quoi ! 7 448 cycles estimés x 1 000 devrait donner 7 448 000 cycles estimés pas 183 930 692 cycles estimés 🤯 Même si j’ai obtenu mon code de séquence Fibonacci fonctionnellement correct, je dois avoir un bug de performance quelque part.

Corriger FizzBuzzFibonacci en Rust

Jetons un autre coup d’œil à cette fonction is_fibonacci_number :

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
}

Maintenant que je pense à la performance, je réalise que j’ai une boucle supplémentaire inutile. Nous pouvons complètement nous débarrasser de la boucle for i in 0..=n {} et il suffit de comparer la valeur current au nombre donné (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
}
  • Mettez à jour votre fonction is_fibonacci_number.
  • Initialisez notre séquence de Fibonacci en commençant par 0 et 1 comme nombres previous et current respectivement.
  • Itérez tant que le numéro current est inférieur au nombre donné n.
  • Ajoutez le numéro previous et current pour obtenir le numéro next.
  • Mettez à jour le numéro previous pour le numéro current.
  • Mettez à jour le numéro current pour le numéro next.
  • Une fois que current est supérieur ou égal au nombre donné n, nous sortirons de la boucle.
  • Vérifiez si le numéro current est égal au nombre donné n et renvoyez ce résultat.

Maintenant relançons ces benchmarks et voyons comment nous nous en sommes sortis :

$ 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.45459s

Oh, wow ! Notre benchmark de 100 diminue de 11% et notre benchmark de 1_000_000 est en baisse de plus de 200 000x ! De 183 930 692 cycles estimés à 742 cycles estimés ! C’est une réduction de 99,9996% !

🐰 Hé, au moins nous avons attrapé ce bug de performance avant qu’il n’arrive en production… oh, c’est vrai. Pas grave…

Détection des régressions de performances dans l’intégration continue (CI)

Les dirigeants n’étaient pas contents du torrent de critiques négatives que notre jeu a reçu à cause de mon petit bug de performance. Ils m’ont dit de ne pas laisser cela se reproduire, et quand j’ai demandé comment, ils m’ont juste dit de ne pas le refaire. Comment suis-je censé gérer cela‽

Heureusement, j’ai trouvé cet outil open source génial appelé Bencher. Il y a un niveau gratuit super généreux, donc je peux simplement utiliser Bencher Cloud pour mes projets personnels. Et au travail où tout doit être dans notre cloud privé, j’ai commencé à utiliser Bencher Self-Hosted.

Bencher a des adaptateurs intégrés, il est donc facile de l’intégrer dans CI. Après avoir suivi le guide de démarrage rapide, je suis en mesure d’exécuter mes benchmarks et de les suivre avec Bencher.

$ 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-cb69d679b409

En utilisant cet astucieux appareil de voyage dans le temps qu’un gentil lapin m’a donné, j’ai pu revenir en arrière et revivre ce qui se serait passé si nous utilisions Bencher depuis le début. Vous pouvez voir où nous avons d’abord poussé l’implémentation buggée de FizzBuzzFibonacci. J’ai immédiatement obtenu des échecs dans CI en commentaire sur ma demande de tirage. Ce même jour, j’ai corrigé le bug de performance, en supprimant cette boucle inutile et supplémentaire. Pas de feux. Juste des utilisateurs heureux.

Bencher: Benchmarking Continu

Bencher est une suite d’outils de benchmarking continu. Avez-vous déjà eu une régression de performance qui a impacté vos utilisateurs ? Bencher aurait pu empêcher cela de se produire. Bencher vous permet de détecter et de prévenir les régressions de performance avant qu’elles ne soient fusionnées.

  • Exécuter: Exécutez vos benchmarks localement ou en CI en utilisant exactement les mêmes runners bare metal et vos outils de benchmarking préférés. La CLI bencher orchestre l’exécution de vos benchmarks sur bare metal et stocke ses résultats.
  • Suivre: Suivez les résultats de vos benchmarks au fil du temps. Surveillez, interrogez et graphiquez les résultats à l’aide de la console web Bencher en fonction de la branche source, du banc d’essai et de la mesure.
  • Détecter: Détectez les régressions de performances localement ou en CI en utilisant exactement le même matériel bare metal. Bencher utilise des analyses de pointe et personnalisables pour détecter les régressions de performances avant qu’elles ne soient fusionnées.

Pour les mêmes raisons que les tests unitaires sont exécutés pour prévenir les régressions de fonctionnalités, les benchmarks devraient être exécutés avec Bencher pour prévenir les régressions de performance. Les bugs de performance sont des bugs !

Commencez à détecter les régressions de performances — essayez Bencher Cloud gratuitement.

🤖 Ce document a été automatiquement traduit par IA. Il peut ne pas être précis et peut contenir des erreurs. Si vous trouvez des erreurs, veuillez ouvrir une issue sur GitHub.