Retour au cours

backend / go

Benchmarks

Leçon 181 exercice

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

OptionInformation ajoutéeUtilité
-bench=.Temps moyen par opérationComparaison brute de vitesse
-benchmemAllocations et octets alloués par opérationRévèle la pression sur le garbage collector
-cpuprofile=cpu.outFichier de profil exploitable par pprofLocaliser précisément les points chauds
-benchtime=3sDurée totale de mesure imposéeRé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.

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
}
go
// 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)
	}
}
bash
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 pprof

Résumé

  • Un benchmark boucle b.N fois : le framework calibre b.N pour une mesure statistiquement stable.
  • -benchmem révèle le coût mémoire (allocations/op), souvent plus parlant que le temps seul.
  • b.Run permet 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

1 disponible
1

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.

Résoudre l’exercice →