Comment faire un benchmark du code Rust avec Criterion

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 trois sont supportés par Bencher. Alors pourquoi choisir Criterion ? Criterion est le standard de facto pour le benchmarking dans la communauté Rust. Je suggérerais d’utiliser Criterion pour faire le benchmark du temps de latence de votre code. C’est-à-dire, Criterion est excellent pour mesurer le temps d’exécution réel.

Refactoriser FizzBuzz

Pour tester notre application FizzBuzz, nous devons découpler notre logique de la fonction main du programme. Les harnais de benchmark ne peuvent pas évaluer la fonction main. 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 numéro, et si print est true, affiche le résultat.
  • fizz_buzz : Prend un entier non signé n et effectue la logique réelle de Fizz, Buzz, FizzBuzz, ou numéro et retourne le résultat sous forme de chaîne.

Ensuite, mettez à jour main.rs pour qu’il ressemble à ceci :

use game::play_game;
fn main() {
for i in 1..=100 {
play_game(i, true);
}
}
  • game::play_game : Importez play_game à partir de la crate game que nous venons de créer avec lib.rs.
  • main : Le point d’entrée principal de notre programme qui parcourt les numéros de 1 à 100 inclus et appelle play_game pour chaque numéro, avec print défini sur true.

Faire le benchmark 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 :

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

À l’intérieur de play_game.rs ajoutez le code suivant :

use criterion::{criterion_group, criterion_main, Criterion};
use game::play_game;
fn bench_play_game(c: &mut Criterion) {
c.bench_function("bench_play_game", |b| {
b.iter(|| {
std::hint::black_box(for i in 1..=100 {
play_game(i, false)
});
});
});
}
criterion_group!(
benches,
bench_play_game,
);
criterion_main!(benches);
  • Importez le runner de benchmark Criterion.
  • Importez la fonction play_game de notre crate game.
  • Créez une fonction nommée bench_play_game qui prend en argument une référence mutable à Criterion.
  • Utilisez l’instance Criterion (c) pour créer un benchmark nommé bench_play_game.
  • Utilisez ensuite le runner de benchmark (b) pour exécuter notre macro-benchmark plusieurs fois.
  • Exécutez notre macro-benchmark à l’intérieur d’une “black box” pour que le compilateur n’optimise pas notre code.
  • Itérez de 1 à 100 inclusivement.
  • Pour chaque nombre, appelez play_game, avec print défini à false.

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

Ajoutez ce qui suit à la fin de votre fichier Cargo.toml :

[dev-dependencies]
criterion = "0.5"
[[bench]]
name = "play_game"
harness = false
  • criterion : Ajoutez criterion en tant que dépendance de développement, puisque nous l’utilisons uniquement pour les tests de performance.
  • bench : Enregistrez play_game comme un benchmark et définissez harness à false, puisque nous allons utiliser Criterion comme notre harnais de benchmarking.

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

$ cargo bench
Compiling playground v0.0.1 (/home/bencher)
Finished bench [optimized] target(s) in 4.79s
Running unittests src/main.rs (target/release/deps/game-68f58c96f4025bd4)
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-043972c4132076a9)
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-e0857103eb02eb56)
bench_play_game time: [3.0020 µs 3.0781 µs 3.1730 µs]
Found 12 outliers among 100 measurements (12.00%)
2 (2.00%) high mild
10 (10.00%) high severe

🐰 Liffe la salade, levons le son de la betterave ! Nous avons nos premières mesures de benchmark !

Enfin, nous pouvons reposer nos cerveaux d’informaticiens fatigués… Juste pour rire, 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.

Faire le benchmark de FizzBuzzFibonacci

Maintenant, nous pouvons ré-exécuter notre benchmark :

$ cargo bench
Compiling playground v0.0.1 (/home/bencher)
Finished bench [optimized] target(s) in 4.79s
Running unittests src/main.rs (target/release/deps/game-68f58c96f4025bd4)
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-043972c4132076a9)
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-e0857103eb02eb56)
bench_play_game time: [20.067 µs 20.107 µs 20.149 µs]
change: [+557.22% +568.69% +577.93%] (p = 0.00 < 0.05)
Performance has regressed.
Found 6 outliers among 100 measurements (6.00%)
4 (4.00%) high mild
2 (2.00%) high severe

Oh, c’est bien ! Criterion nous indique que la différence de performance entre nos jeux FizzBuzz et FizzBuzzFibonacci est de +568.69%. Vos nombres seront légèrement différents des miens. Cependant, la différence entre les deux jeux est probablement dans la plage de 5x. Ça me semble bien ! Surtout pour ajouter une fonctionnalité aussi sophistiquée 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 :

fn bench_play_game_100(c: &mut Criterion) {
c.bench_function("bench_play_game_100", |b| {
b.iter(|| std::hint::black_box(play_game(100, false)));
});
}
fn bench_play_game_1_000_000(c: &mut Criterion) {
c.bench_function("bench_play_game_1_000_000", |b| {
b.iter(|| std::hint::black_box(play_game(1_000_000, false)));
});
}
  • Un micro-benchmark bench_play_game_100 pour jouer le jeu avec le nombre cent (100)
  • Un micro-benchmark bench_play_game_1_000_000 pour jouer le jeu avec le nombre un million (1_000_000)

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

$ cargo bench
Compiling playground v0.0.1 (/home/bencher)
Finished bench [optimized] target(s) in 4.79s
Running unittests src/main.rs (target/release/deps/game-68f58c96f4025bd4)
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-043972c4132076a9)
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-e0857103eb02eb56)
bench_play_game time: [20.024 µs 20.058 µs 20.096 µs]
change: [-0.0801% +0.1431% +0.3734%] (p = 0.21 > 0.05)
No change in performance detected.
Found 17 outliers among 100 measurements (17.00%)
9 (9.00%) high mild
8 (8.00%) high severe
bench_play_game_100 time: [403.00 ns 403.57 ns 404.27 ns]
Found 13 outliers among 100 measurements (13.00%)
6 (6.00%) high mild
7 (7.00%) high severe

Attendez… patientez…

bench_play_game_1_000_000
time: [9.5865 ms 9.5968 ms 9.6087 ms]
Found 16 outliers among 100 measurements (16.00%)
8 (8.00%) high mild
8 (8.00%) high severe

Quoi ! 403.57 ns x 1,000 devrait être 403,570 ns et non 9,596,800 ns (9.5968 ms x 1_000_000 ns/1 ms) 🤯 Même si j’ai correctement codé ma séquence de Fibonacci, 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, refaisons ces benchmarks et voyons comment nous nous en sommes sortis :

$ cargo bench
Compiling playground v0.0.1 (/home/bencher)
Finished bench [optimized] target(s) in 4.79s
Running unittests src/main.rs (target/release/deps/game-68f58c96f4025bd4)
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-043972c4132076a9)
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-e0857103eb02eb56)
bench_play_game time: [3.1201 µs 3.1772 µs 3.2536 µs]
change: [-84.469% -84.286% -84.016%] (p = 0.00 < 0.05)
Performance has improved.
Found 5 outliers among 100 measurements (5.00%)
1 (1.00%) high mild
4 (4.00%) high severe
bench_play_game_100 time: [24.460 ns 24.555 ns 24.650 ns]
change: [-93.976% -93.950% -93.927%] (p = 0.00 < 0.05)
Performance has improved.
bench_play_game_1_000_000
time: [30.260 ns 30.403 ns 30.564 ns]
change: [-100.000% -100.000% -100.000%] (p = 0.00 < 0.05)
Performance has improved.
Found 4 outliers among 100 measurements (4.00%)
1 (1.00%) high mild
3 (3.00%) high severe

Oh, wow ! Notre benchmark bench_play_game est revenu à peu près où il était pour le FizzBuzz original. J’aimerais me souvenir exactement de ce score. Ça fait trois semaines cependant. Mon historique de terminal ne remonte pas aussi loin. Et Criterion ne compare qu’avec le résultat le plus récent. Mais je pense que c’est proche !

Le benchmark bench_play_game_100 a baissé de près de 10x, -93.950%. Et le benchmark bench_play_game_1_000_000 a baissé de plus de 10,000x ! De 9,596,800 ns à 30.403 ns ! Nous avons même atteint le maximum de l’indicateur de changement Criterion, qui ne va que jusqu’à -100.000% !

🐰 Hé, au moins nous avons attrapé ce bug de performance avant qu’il n’atteigne la production… oh, wait. Nevermind…

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 [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
Running unittests src/main.rs (target/release/deps/game-043972c4132076a9)
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-e0857103eb02eb56)
Gnuplot not found, using plotters backend
bench_play_game time: [3.0713 µs 3.0902 µs 3.1132 µs]
Found 16 outliers among 100 measurements (16.00%)
3 (3.00%) high mild
13 (13.00%) high severe
bench_play_game_100 time: [23.938 ns 23.970 ns 24.009 ns]
Found 15 outliers among 100 measurements (15.00%)
5 (5.00%) high mild
10 (10.00%) high severe
bench_play_game_1_000_000
time: [30.004 ns 30.127 ns 30.279 ns]
Found 5 outliers among 100 measurements (5.00%)
1 (1.00%) high mild
4 (4.00%) high severe
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

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

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 n’arrivent en production.

  • Exécuter: Exécutez vos benchmarks localement ou en CI en utilisant vos outils de benchmarking préférés. La CLI bencher enveloppe simplement votre harnais de benchmarking existant 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 en CI. Bencher utilise des analyses de pointe et personnalisables pour détecter les régressions de performances avant qu’elles n’arrivent en production.

Pour les mêmes raisons que les tests unitaires sont exécutés en CI pour prévenir les régressions de fonctionnalités, les benchmarks devraient être exécutés en CI 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 en CI — essayez Bencher Cloud gratuitement.

🤖 Ce document a été automatiquement généré par OpenAI GPT-4. Il peut ne pas être précis et peut contenir des erreurs. Si vous trouvez des erreurs, veuillez ouvrir une issue sur GitHub.