Comment tester la performance du code Rust avec Iai


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 Iai ? Iai utilise des compteurs d’instructions plutôt que le temps d’horloge mural. Cela le rend idéal pour les benchmark continus, c’est-à-dire les benchmarks en CI. Je suggérerais d’utiliser Iai pour le benchmarking continu, surtout si vous utilisez des runners partagés. Il est important de comprendre qu’Iai ne mesure qu’une approximation de ce qui vous importe vraiment. Passer de 1 000 instructions à 2 000 instructions double-t-il la latence de votre application ? Peut-être ou peut-être pas. Pour cette raison, il peut être utile de faire également des benchmarks basés sur le temps d’horloge mural en parallèle avec les benchmarks basés sur les compteurs d’instructions.

🐰 Iai n’a pas été mis à jour depuis plus de 3 ans. Vous devriez donc envisager d’utiliser Iai-Callgrind à la place.

Installer Valgrind

Iai utilise un outil appelé Valgrind pour collecter les compteurs d’instructions. Valgrind supporte Linux, Solaris, FreeBSD, et MacOS. Cependant, le support de MacOS est limité aux processeurs x86_64 car les processeurs arm64 (M1, M2, etc) ne sont pas encore supportés.

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

Sur MacOS (seulement les puces x86_64/Intel): brew install valgrind

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.

Tester la performance de FizzBuzz

Pour tester notre code, nous devons créer un dossier 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

Ajoutez le code suivant à l’intérieur de play_game.rs :

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);
  • Importez la fonction play_game de notre crate game.
  • Créez une fonction appelée bench_play_game.
  • Exécutez notre macro-benchmark à l’intérieur d’une “boîte noire” pour que le compilateur n’optimise pas notre code.
  • Itérerez de 1 à 100 inclusivement.
  • Pour chaque nombre, appelez play_game, avec print réglé sur false.

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

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

[dev-dependencies]
iai = "0.1"
[[bench]]
name = "play_game"
harness = false
  • iai: Ajoutez iai comme un dépendance de développement, puisque nous l’utilisons que pour les tests de performance.
  • bench: Enregistrez play_game comme un benchmark et réglez harness à false, puisque nous utiliserons Iai comme notre harnais de benchmarking.

Maintenant, nous sommes prêts à tester la performance de notre code, exécutez 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

🐰 Laissez tourner la betterave ! Nous avons nos premières métriques de benchmark !

Enfin, nous pouvons reposer nos têtes fatiguées de développeurs … Juste 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.

Tester la performance de FizzBuzzFibonacci

Maintenant, nous pouvons relancer notre benchmark :

$ 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, génial ! Iai nous dit que la différence entre les cycles estimés de nos jeux FizzBuzz et FizzBuzzFibonacci est de +522.6091%. Vos chiffres seront un peu différents des miens. Cependant, la différence entre les deux jeux est probablement dans la plage des 5x. Cela 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() {
iai::black_box(play_game(100, false));
}
fn bench_play_game_1_000_000() {
iai::black_box(play_game(1_000_000, false));
}
  • Un micro-benchmark bench_play_game_100 pour jouer au jeu avec le nombre cent (100)
  • Un micro-benchmark bench_play_game_1_000_000 pour jouer au jeu avec le nombre un million (1_000_000)

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

$ 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

Attendez-le… attendez-le…

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

Quoi! 6,685 cycles estimés x 1,000 devrait être 6,685,000 cycles estimés et non pas 155,109,206 cycles estimés 🤯 Même si j’ai bien programmé ma fonction de la 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, réexécutons ces benchmarks pour voir comment nous avons réagi :

$ 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, wouah ! Notre benchmark bench_play_game est revenu à peu près au même niveau qu’il était pour le FizzBuzz original. J’aurais aimé me souvenir exactement de ce score. Ça fait trois semaines cependant. Mon historique de terminal ne remonte pas aussi loin. Et Iai 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 diminué près de 10 fois, -87.22513%. Et le benchmark bench_play_game_1_000_000 a diminué de plus de 10,000 fois ! De 155,109,206 cycles estimés à 950 cycles estimés ! C’est -99.99939% !

🐰 Heureusement, nous avons repéré ce bug de performance avant qu’il n’atteigne la production… ah, non. Oublie ça…

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

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.