Benchmark
Che cosa sono i Benchmark?
I benchmark in Go permettono di misurare le prestazioni del codice in modo preciso e ripetibile. Il pacchetto testing include un supporto nativo per i benchmark, accessibili tramite il comando go test -bench. Sono fondamentali per prendere decisioni informate sull’ottimizzazione del codice.
Scrivere funzioni di Benchmark
Una funzione di benchmark deve iniziare con Benchmark, seguito da una lettera maiuscola, e accettare un parametro *testing.B:
// stringhe.go
package stringhe
import (
"fmt"
"strings"
)
func ConcatenaConPlus(parole []string) string {
risultato := ""
for _, p := range parole {
risultato += p + " "
}
return risultato
}
func ConcatenaConBuilder(parole []string) string {
var sb strings.Builder
for _, p := range parole {
sb.WriteString(p)
sb.WriteString(" ")
}
return sb.String()
}
func ConcatenaConSprintf(parole []string) string {
risultato := ""
for _, p := range parole {
risultato = fmt.Sprintf("%s%s ", risultato, p)
}
return risultato
}
// stringhe_test.go
package stringhe
import "testing"
var paroleTest = []string{
"Go", "e", "un", "linguaggio", "di", "programmazione",
"moderno", "efficiente", "e", "potente",
}
func BenchmarkConcatenaConPlus(b *testing.B) {
for i := 0; i < b.N; i++ {
ConcatenaConPlus(paroleTest)
}
}
func BenchmarkConcatenaConBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
ConcatenaConBuilder(paroleTest)
}
}
func BenchmarkConcatenaConSprintf(b *testing.B) {
for i := 0; i < b.N; i++ {
ConcatenaConSprintf(paroleTest)
}
}
Il valore b.N
Il parametro b.N e il numero di iterazioni che il framework di benchmark determina automaticamente. Go aumenta progressivamente b.N fino a ottenere una misurazione stabile e affidabile. Non dovete mai impostare b.N manualmente: il framework lo gestisce per voi.
func BenchmarkEsempio(b *testing.B) {
// b.N viene determinato automaticamente
// Puo essere 1, 100, 10000, 1000000, ecc.
for i := 0; i < b.N; i++ {
// Operazione da misurare
_ = fmt.Sprintf("iterazione %d", i)
}
}
Eseguire i Benchmark
Per eseguire i benchmark si usa go test con il flag -bench:
# Esegue tutti i benchmark
go test -bench=.
# Esegue benchmark specifici (con regex)
go test -bench=BenchmarkConcatenaConBuilder
# Esegue con piu iterazioni per risultati stabili
go test -bench=. -benchtime=5s
# Esegue benchmark senza i test normali
go test -bench=. -run=^$
# Esegue con un numero specifico di iterazioni
go test -bench=. -benchtime=1000x
Leggere i risultati
L’output di un benchmark ha questo formato:
BenchmarkConcatenaConPlus-8 1000000 1150 ns/op
BenchmarkConcatenaConBuilder-8 5000000 215 ns/op
BenchmarkConcatenaConSprintf-8 500000 3200 ns/op
- Nome-8: il nome del benchmark e il numero di CPU utilizzate (GOMAXPROCS).
- 1000000: il numero di iterazioni eseguite (b.N finale).
- 1150 ns/op: il tempo medio per operazione in nanosecondi.
Benchmark della memoria
Per misurare le allocazioni di memoria, si usa b.ReportAllocs() o il flag -benchmem:
func BenchmarkConcatenaConPlusMem(b *testing.B) {
b.ReportAllocs() // Riporta le allocazioni di memoria
for i := 0; i < b.N; i++ {
ConcatenaConPlus(paroleTest)
}
}
func BenchmarkConcatenaConBuilderMem(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ConcatenaConBuilder(paroleTest)
}
}
Oppure dalla riga di comando:
go test -bench=. -benchmem
L’output includera informazioni aggiuntive:
BenchmarkConcatenaConPlus-8 1000000 1150 ns/op 480 B/op 9 allocs/op
BenchmarkConcatenaConBuilder-8 5000000 215 ns/op 112 B/op 2 allocs/op
- 480 B/op: byte allocati per operazione.
- 9 allocs/op: numero di allocazioni per operazione.
Setup nel Benchmark
Quando il benchmark richiede una fase di preparazione, possiamo usare b.ResetTimer():
func BenchmarkConSetup(b *testing.B) {
// Fase di setup (non misurata)
dati := make([]int, 10000)
for i := range dati {
dati[i] = i
}
b.ResetTimer() // Resetta il timer dopo il setup
for i := 0; i < b.N; i++ {
// Operazione da misurare
somma := 0
for _, v := range dati {
somma += v
}
}
}
Per operazioni costose che si ripetono ad ogni iterazione:
func BenchmarkConStopStart(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
dati := preparaDati() // Non misurato
b.StartTimer()
elabora(dati) // Misurato
}
}
Sub-Benchmark
Come per i test, possiamo usare b.Run per creare sotto-benchmark parametrizzati:
func BenchmarkConcatenazione(b *testing.B) {
dimensioni := []int{10, 100, 1000}
for _, n := range dimensioni {
parole := make([]string, n)
for i := range parole {
parole[i] = "parola"
}
b.Run(fmt.Sprintf("Plus/%d", n), func(b *testing.B) {
for i := 0; i < b.N; i++ {
ConcatenaConPlus(parole)
}
})
b.Run(fmt.Sprintf("Builder/%d", n), func(b *testing.B) {
for i := 0; i < b.N; i++ {
ConcatenaConBuilder(parole)
}
})
}
}
Confrontare i Benchmark
Per confrontare benchmark tra diverse versioni del codice, possiamo salvare i risultati e usare strumenti come benchstat:
# Salva i risultati prima della modifica
go test -bench=. -count=10 > prima.txt
# Dopo la modifica
go test -bench=. -count=10 > dopo.txt
# Confronta con benchstat
go install golang.org/x/perf/cmd/benchstat@latest
benchstat prima.txt dopo.txt
L’output di benchstat mostra le variazioni percentuali e la significativita statistica delle differenze.
Conclusione
I benchmark sono uno strumento essenziale per ottimizzare il codice Go in modo scientifico. Permettono di misurare tempi di esecuzione e allocazioni di memoria, confrontare approcci diversi e verificare che le ottimizzazioni producano reali miglioramenti. Ricordate di eseguire i benchmark in condizioni controllate, usare -benchtime per risultati stabili e -benchmem per monitorare le allocazioni di memoria.