Benchmarks em Go
1. Introdução aos Benchmarks em Go
Benchmarks são ferramentas essenciais para medir o desempenho de código em Go. Enquanto testes unitários verificam a correção funcional, benchmarks medem tempo de execução, alocações de memória e eficiência computacional. Em Go, os benchmarks são integrados ao pacote testing e seguem uma estrutura padronizada.
A principal diferença entre um teste unitário e um benchmark é que o primeiro executa uma verificação de resultado, enquanto o segundo executa repetidamente uma operação para coletar métricas estatísticas. A assinatura básica de uma função de benchmark é:
func BenchmarkXxx(b *testing.B) {
// código a ser medido
}
O parâmetro b *testing.B gerencia a execução e fornece métodos para controlar o temporizador, relatar alocações e configurar iterações.
2. Escrevendo seu Primeiro Benchmark
A anatomia de um benchmark gira em torno do loop b.N. O framework ajusta automaticamente b.N para garantir que a execução seja estatisticamente significativa, normalmente entre 1 e 100 segundos de execução total.
Vamos criar um benchmark para comparar diferentes formas de concatenar strings:
// concat.go
package main
func ConcatWithPlus(a, b string) string {
return a + b
}
func ConcatWithSprintf(a, b string) string {
return fmt.Sprintf("%s%s", a, b)
}
// concat_test.go
package main
import (
"testing"
"fmt"
)
func BenchmarkConcatWithPlus(b *testing.B) {
a := "Hello"
c := "World"
for i := 0; i < b.N; i++ {
ConcatWithPlus(a, c)
}
}
func BenchmarkConcatWithSprintf(b *testing.B) {
a := "Hello"
c := "World"
for i := 0; i < b.N; i++ {
ConcatWithSprintf(a, c)
}
}
Para executar os benchmarks:
go test -bench=.
Saída típica:
BenchmarkConcatWithPlus-8 1000000000 0.2436 ns/op
BenchmarkConcatWithSprintf-8 50000000 28.45 ns/op
O sufixo -8 indica 8 CPUs lógicas usadas. O número antes do resultado (ex: 1000000000) é o valor de b.N que o framework escolheu.
3. Controlando a Execução de Benchmarks
Podemos filtrar benchmarks específicos com expressões regulares:
go test -bench=ConcatWithPlus
Para limitar o tempo de execução:
go test -bench=. -benchtime=5s
Para testar com diferentes números de CPUs:
go test -bench=. -cpu=1,2,4
Para benchmarks paralelos, usamos b.SetParallelism():
func BenchmarkParallelConcat(b *testing.B) {
b.SetParallelism(4)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
ConcatWithPlus("Hello", "World")
}
})
}
4. Métricas e Resultados de Benchmark
A saída padrão mostra:
- ns/op: nanossegundos por operação (menor é melhor)
- allocs/op: alocações de memória por operação
- bytes/op: bytes alocados por operação
Para incluir métricas de alocação:
func BenchmarkWithAllocs(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = make([]int, 1000)
}
}
Controlar o temporizador é útil quando você precisa de setup antes da medição:
func BenchmarkWithSetup(b *testing.B) {
data := make([]byte, 1024*1024)
b.ResetTimer() // ignora o tempo de alocação inicial
for i := 0; i < b.N; i++ {
processData(data)
}
}
Use b.StopTimer() e b.StartTimer() para pausar/retomar a medição durante operações auxiliares.
5. Benchmarks Avançados e Configurações
Benchmarks com entradas variáveis permitem testar diferentes tamanhos de dados:
func BenchmarkSort(b *testing.B) {
sizes := []int{10, 100, 1000, 10000}
for _, size := range sizes {
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
data := generateRandomSlice(size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
sortSlice(data)
}
})
}
}
Benchmarks paralelos com b.RunParallel são ideais para testar concorrência:
func BenchmarkParallelSum(b *testing.B) {
nums := make([]int, 1000000)
for i := range nums {
nums[i] = i
}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
sum(nums)
}
})
}
6. Evitando Armadilhas Comuns
Dead code elimination: O compilador pode otimizar código que não produz resultados observáveis. Para evitar isso, atribua resultados a variáveis globais:
var globalResult int
func BenchmarkSum(b *testing.B) {
nums := []int{1, 2, 3}
for i := 0; i < b.N; i++ {
globalResult = sum(nums)
}
}
Inicialização no loop: Colocar alocações caras dentro do loop pode distorcer resultados. Prefira inicializar fora:
// Ruim: aloca a cada iteração
func BenchmarkBad(b *testing.B) {
for i := 0; i < b.N; i++ {
s := make([]byte, 1024)
_ = len(s)
}
}
// Bom: aloca uma vez
func BenchmarkGood(b *testing.B) {
b.StopTimer()
s := make([]byte, 1024)
b.StartTimer()
for i := 0; i < b.N; i++ {
_ = len(s)
}
}
Variação de resultados: Para medições estáveis, execute múltiplas vezes com -benchtime=10x ou use benchstat para análise estatística.
7. Ferramentas de Análise e Comparação
benchstat compara resultados entre execuções:
go test -bench=. -count=5 > old.txt
# após alterações no código
go test -bench=. -count=5 > new.txt
benchstat old.txt new.txt
Gere perfis de CPU e memória:
go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof
Analise com go tool pprof:
go tool pprof cpu.prof
(pprof) top10
(pprof) web
8. Boas Práticas e Padrões
- Representatividade: Use dados realistas, não apenas tamanhos pequenos
- Documentação: Comente benchmarks complexos explicando o que está sendo medido
- CI/CD: Integre benchmarks em pipelines para detectar regressões:
# Exemplo de workflow GitHub Actions
- name: Run benchmarks
run: |
go test -bench=. -benchmem -count=5 > bench.txt
benchstat bench.txt
- Versionamento: Mantenha resultados de benchmark em um arquivo separado para histórico
- Foco: Cada benchmark deve medir uma única operação ou comportamento
Referências
- Testing package documentation — Documentação oficial do pacote testing com especificações completas sobre benchmarks em Go
- Go Blog: Profiling Go Programs — Artigo oficial da equipe Go sobre profiling e análise de performance
- benchstat — Ferramenta oficial para análise estatística de resultados de benchmarks
- Go Wiki: Performance — Coletânea de dicas e práticas recomendadas para performance em Go
- Dave Cheney: Benchmarking Go Programs — Tutorial prático e detalhado sobre como escrever benchmarks eficientes em Go
- pprof documentation — Documentação da ferramenta de profiling usada para análise de CPU e memória