backend / go
Benchmarks
Explication
Ce que vous allez apprendre
- Écrire un benchmark avec le package standard
testing, sans outil externe - Comprendre le rôle de
b.N, calibré automatiquement pour une mesure stable - Comparer plusieurs implémentations d'un même algorithme objectivement
- Mesurer les allocations mémoire par opération avec
-benchmem - Générer un profil CPU directement depuis un benchmark
Dans quel contexte ?
Un développeur doit choisir entre trois implémentations possibles d'une fonction de calcul de Fibonacci pour un service qui l'appelle des millions de fois par jour : une version récursive naïve, une version itérative, et une version avec mémoïsation. Deviner laquelle est la plus rapide "à l'œil" est une mauvaise idée : les benchmarks permettent de trancher avec des chiffres réels, mesurés dans les conditions du langage lui-même.
D'abord, un benchmark ressemble beaucoup à un test
La convention est similaire à celle des tests : un fichier _test.go, une fonction qui commence par Benchmark (au lieu de Test), prenant un paramètre *testing.B (au lieu de *testing.T). La différence essentielle se trouve dans le corps de la fonction : le code à mesurer est répété b.N fois dans une boucle.
b.N n'est jamais fixé manuellement par le développeur : le framework l'ajuste lui-même automatiquement, en augmentant progressivement ce nombre jusqu'à obtenir une mesure statistiquement stable, peu sensible au bruit d'une seule exécution.
Une fois un premier benchmark lancé, comment interpréter le résultat ?
go test -bench=. affiche un temps moyen par opération (en nanosecondes), calculé sur l'ensemble des b.N itérations. Comparer ce chiffre entre plusieurs implémentations donne une réponse objective, bien plus fiable qu'une intuition sur "ce qui devrait être plus rapide".
| Option | Information ajoutée | Utilité |
|---|---|---|
-bench=. | Temps moyen par opération | Comparaison brute de vitesse |
-benchmem | Allocations et octets alloués par opération | Révèle la pression sur le garbage collector |
-cpuprofile=cpu.out | Fichier de profil exploitable par pprof | Localiser précisément les points chauds |
-benchtime=3s | Durée totale de mesure imposée | Réduire le bruit statistique sur un run court |
Prérequis
Il faut être à l'aise avec les tests unitaires et t.Run (leçon précédente) : b.Run fonctionne de façon très similaire pour des sous-benchmarks paramétrés.
Il reste un enseignement souvent contre-intuitif à ce stade
-benchmem révèle une information que le simple temps d'exécution cache totalement : le nombre d'allocations mémoire par opération. Une implémentation légèrement plus lente mais qui alloue zéro mémoire par appel peut s'avérer bien meilleure en production qu'une version plus rapide en isolation mais qui sature le garbage collector sous forte charge réelle.
Piège fréquent
Comparer des benchmarks compilés en mode debug (sans -race, mais aussi sans optimisations désactivées par erreur) sur des machines différentes ou sous charge CPU concurrente fausse complètement les résultats. Lance toujours les benchmarks sur une machine calme, plusieurs fois de suite, pour vérifier la stabilité des chiffres obtenus.
Enfin, benchmark et profiling se combinent naturellement
Bonne pratique
Une fois qu'un benchmark révèle qu'une fonction est lente, génère directement un profil CPU avec -cpuprofile et analyse-le avec go tool pprof (vu dans une prochaine leçon dédiée), plutôt que d'optimiser à l'aveugle sur une intuition qui se révèle souvent fausse.
Maintenant que tu sais mesurer objectivement la performance, la prochaine leçon change de sujet pour aborder l'une des fonctionnalités les plus attendues et récentes de Go : les generics.
Commandes & code
Benchmarks
Mesurer précisément la performance du code avec l'outillage natif de Go.
// fichier: fibonacci.go
package fib
func FibRecursif(n int) int {
if n < 2 {
return n
}
return FibRecursif(n-1) + FibRecursif(n-2)
}
func FibIteratif(n int) int {
if n < 2 {
return n
}
a, b := 0, 1
for i := 2; i <= n; i++ {
a, b = b, a+b
}
return b
}
func FibMemo(n int, cache map[int]int) int {
if n < 2 {
return n
}
if v, ok := cache[n]; ok {
return v
}
resultat := FibMemo(n-1, cache) + FibMemo(n-2, cache)
cache[n] = resultat
return resultat
}// fichier: fibonacci_test.go
package fib
import "testing"
// Le nom DOIT commencer par Benchmark, parametre *testing.B
func BenchmarkFibRecursif(b *testing.B) {
// b.N est ajuste automatiquement par le framework pour une mesure stable
for i := 0; i < b.N; i++ {
FibRecursif(20)
}
}
func BenchmarkFibIteratif(b *testing.B) {
for i := 0; i < b.N; i++ {
FibIteratif(20)
}
}
func BenchmarkFibMemo(b *testing.B) {
for i := 0; i < b.N; i++ {
cache := make(map[int]int)
FibMemo(20, cache)
}
}
// Benchmark avec sous-cas parametres, pattern table-driven applique au perf
func BenchmarkFibIteratifTailles(b *testing.B) {
tailles := []int{10, 20, 30, 40}
for _, taille := range tailles {
b.Run(fmt.Sprintf("n=%d", taille), func(b *testing.B) {
for i := 0; i < b.N; i++ {
FibIteratif(taille)
}
})
}
}
// ReportAllocs affiche le nombre d'allocations memoire par operation
func BenchmarkFibMemoAvecAllocs(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
cache := make(map[int]int)
FibMemo(20, cache)
}
}go test -bench=. # lancer tous les benchmarks
go test -bench=FibIteratif -benchtime=3s
go test -bench=. -benchmem # ajoute B/op et allocs/op
go test -bench=. -cpuprofile=cpu.out # generer un profil CPU exploitable avec pprofRésumé
- Un benchmark boucle
b.Nfois : le framework calibreb.Npour une mesure statistiquement stable. -benchmemrévèle le coût mémoire (allocations/op), souvent plus parlant que le temps seul.b.Runpermet des sous-benchmarks paramétrés, comme les sous-tests.- Comparer plusieurs implémentations d'un même algorithme via benchmark évite les optimisations "à l'aveugle".
Exercices pratiques
Mission : choisir la bonne implémentation de Fibonacci pour un service à fort trafic
Objectif : Comparer objectivement trois implémentations avec des benchmarks, puis repérer un piège d'allocation caché par une mesure de temps seule.
Contexte
Le service de recommandation doit appeler une fonction de calcul de Fibonacci des millions de fois par jour pour n autour de 20. L'équipe hésite entre FibRecursif, FibIteratif et FibMemo, et un développeur propose de trancher "à l'œil" en lisant le code plutôt qu'en mesurant, faute de temps.