Comment tester la performance du code Rust avec Iai
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 :
- Créer une fonction
main
- Itérer de
1
à100
inclusivement. - Pour chaque nombre, calculez le modulo (reste après division) pour
3
et5
. - 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
pour3
et5
, alors imprimezFizzBuzz
. - Si le reste est
0
pour seulement3
, alors imprimezFizz
. - Si le reste est
0
pour seulement5
, alors imprimezBuzz
. - 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éé.
Vous devriez voir un répertoire appelé src
avec un fichier nommé main.rs
:
Remplacez son contenu par l’implémentation FizzBuzz ci-dessus. Puis exécutez cargo run
.
Le résultat devrait ressembler à :
🐰 Bam ! Vous craquez l’entretien de codage !
Un nouveau fichier Cargo.lock
devrait avoir été généré :
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
:
Ajoutez le code suivant à lib.rs
:
play_game
: Prend un entier non signén
, appellefizz_buzz
avec ce numéro, et siprint
esttrue
, affiche le résultat.fizz_buzz
: Prend un entier non signén
et effectue la logique réelle deFizz
,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 :
game::play_game
: Importezplay_game
à partir de la crategame
que nous venons de créer aveclib.rs
.main
: Le point d’entrée principal de notre programme qui parcourt les numéros de1
à100
inclus et appelleplay_game
pour chaque numéro, avecprint
défini surtrue
.
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
:
Ajoutez le code suivant à l’intérieur de play_game.rs
:
- Importez la fonction
play_game
de notre crategame
. - 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
, avecprint
réglé surfalse
.
Maintenant, nous devons configurer la crate game
pour exécuter nos benchmarks.
Ajoutez ce qui suit au bas de votre fichier Cargo.toml
:
iai
: Ajouteziai
comme un dépendance de développement, puisque nous l’utilisons que pour les tests de performance.bench
: Enregistrezplay_game
comme un benchmark et réglezharness
à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
:
🐰 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 :
- 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
et1
comme les nombresprevious
etcurrent
respectivement. - Itérer pendant que le nombre
current
est inférieur au nombre d’itérationi
en cours. - Ajoutez le nombre
previous
et le nombrecurrent
pour obtenir le nombrenext
. - Mettre à jour le nombre
previous
avec le nombrecurrent
. - Mettre à jour le nombre
current
avec le nombrenext
. - 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 retournertrue
. - Sinon, retournez
false
.
Maintenant, nous devrons mettre à jour notre fonction fizz_buzz
:
- Renommez la fonction
fizz_buzz
enfizz_buzz_fibonacci
pour la rendre plus descriptive. - Appelez notre fonction associée
is_fibonacci_number
. - Si le résultat de
is_fibonacci_number
esttrue
, alors retournezFibonacci
. - Si le résultat de
is_fibonacci_number
estfalse
, alors effectuez la même logiqueFizz
,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
:
Les fonctions main
et bench_play_game
peuvent rester exactement les mêmes.
Tester la performance de FizzBuzzFibonacci
Maintenant, nous pouvons relancer notre benchmark :
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 :
- 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 :
Et si nous oublions de fournir un numéro ou fournissons un numéro invalide :
Wow, c’était des tests très approfondis! CI passe. Nos patrons sont ravis. Expédions-le! 🚀
La fin
🐰 … 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 :
- 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 :
Attendez-le… attendez-le…
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
:
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
) 🤦
- Mettez à jour votre fonction
is_fibonacci_number
. - Initialisez notre séquence de Fibonacci en commençant par
0
et1
comme nombresprevious
etcurrent
respectivement. - Itérez tant que le numéro
current
est inférieur au nombre donnén
. - Ajoutez le numéro
previous
etcurrent
pour obtenir le numéronext
. - Mettez à jour le numéro
previous
pour le numérocurrent
. - Mettez à jour le numéro
current
pour le numéronext
. - 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 :
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.
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 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.