Comment mettre en benchmark le code Rust avec le banc d'essai libtest
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.
Tous trois sont supportĂ©s par Bencher. Alors pourquoi choisir le banc dâessai libtest ?
Cela peut ĂȘtre une bonne idĂ©e si vous essayez de limiter les dĂ©pendances externes de votre projet et que votre projet utilise dĂ©jĂ la chaĂźne dâoutils nightly
.
En dehors de cela, je suggĂ©rerais dâutiliser soit Criterion soit Iai selon votre cas dâutilisation.
Installer Rust nightly
Cela dit, nous allons utiliser le banc dâessai libtest, alors rĂ©glons notre chaĂźne dâoutils Rust sur nightly
.
Créez un fichier rust-toolchain.toml
Ă la racine de votre projet game
, à cÎté de Cargo.toml
.
La structure de votre répertoire devrait maintenant ressembler à ceci :
Une fois cela fini, relancez cargo run
.
Il faudra une minute pour que la nouvelle chaĂźne dâoutils nightly sâinstalle avant de redĂ©marrer et de vous donner la mĂȘme sortie quâauparavant.
Refactorisation FizzBuzz
Pour tester notre application FizzBuzz, nous devons découpler notre logique de la fonction principale main
du programme.
Les harnais de test ne peuvent pas Ă©valuer la fonction main
.
Mettez Ă jour votre code pour quâil ressemble Ă ceci :
Nous avons maintenant séparé notre code en trois fonctions différentes :
main
: LâentrĂ©e principale dans notre programme qui parcourt les nombres de1
Ă100
inclus et appelleplay_game
pour chaque nombre.play_game
: Prend un entier non signén
, appellefizz_buzz
avec ce nombre et imprime le résultat.fizz_buzz
: Prend un entier non signén
et effectue la logique réelleFizz
,Buzz
,FizzBuzz
, ou le nombre renvoie le résultat sous forme de chaßne.
Benchmarking de FizzBuzz
Pour utiliser la crate libtest instable, nous devons activer la fonction test
pour notre code et importer la crate test
. Ajoutez ce qui suit au tout début de main.rs
:
Maintenant nous sommes prĂȘts Ă ajouter notre premier benchmark !
Ajoutez ce qui suit au tout bas de main.rs
:
- Créez un module appelé
benchmarks
et réglez la configuration du compilateur sur le modetest
. - Importez le runner de benchmark
Bencher
. (đ° HĂ©, cool nom !) - Importez notre fonction
play_game
. - Créez un benchmark appelé
bench_play_game
qui prend en entrĂ©e une rĂ©fĂ©rence mutable ĂBencher
. - Configurez lâattribut
#[bench]
pour indiquer quebench_play_game
est un benchmark. - Utilisez lâinstance de
Bencher
(b
) pour exĂ©cuter plusieurs fois notre macro-benchmark. - ExĂ©cutez notre macro-benchmark Ă lâintĂ©rieur dâune âboĂźte noireâ pour que le compilateur nâoptimise pas notre code.
- Itérez de
1
Ă100
inclusivement. - Pour chaque nombre, appelez
play_game
.
Maintenant nous sommes prĂȘts pour benchmark notre code, exĂ©cutez cargo bench
:
đ° Laissons la betterave monter ! Nous avons nos premiĂšres mesures de benchmark !
Finalement, nous pouvons reposer nos tĂȘtes de dĂ©veloppeurs fatiguĂ©s⊠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.
Benchmarking de FizzBuzzFibonacci
Maintenant nous pouvons relancer notre benchmark :
En remontant notre historique de terminal, nous pouvons faire une comparaison visuelle entre les performances de nos jeux FizzBuzz et FizzBuzzFibonacci : 4,879 ns
contre 22,167 ns
.
Vos chiffres seront un peu différents des miens.
Cependant, la diffĂ©rence entre les deux jeux est probablement dans lâordre de 5x.
Cela me semble bon ! Surtout pour ajouter une fonctionnalité aussi fantaisiste 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
).
Lorsque je lâai exĂ©cutĂ©, jâai obtenu ceci :
Attendez un peu⊠attendez un peuâŠ
Quoi ! 439 ns
x 1,000
devrait ĂȘtre 439,000 ns
pas 9,586,977 ns
đ€Ż
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
:
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, relançons ces benchmarks et voyons comment nous nous en sommes sortis :
Oh, wow ! Notre benchmark bench_play_game
est revenu Ă peu prĂšs au niveau oĂč il Ă©tait pour le FizzBuzz original.
Jâaimerais me souvenir exactement de ce score. Mais ça fait trois semaines.
Mon historique de terminal ne remonte pas si loin.
Mais je pense que câest proche !
Le benchmark bench_play_game_100
est descendu prĂšs de 10 fois, de 439 ns
Ă 46 ns
.
Et le benchmark bench_play_game_1_000_000
est descendu plus de 10 000 fois ! De 9,586,977 ns
Ă 53 ns
!
đ° HĂ©, au moins on a attrapĂ© ce bug de performance avant quâil nâarrive en production⊠oh, attendez. Jamais dâespritâŠ
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.