Benchmarks
Nesta aula, você aprenderá a escrever e executar benchmarks em Go usando o pacote de testes padrão. Vamos explorar a função BenchmarkXxx, o contador b.N, a medição de alocações e como comparar desempenho entre implementações, com exemplos práticos e boas práticas.
Benchmarks são uma ferramenta essencial para medir o desempenho do seu código Go. Eles permitem que você avalie o tempo de execução e o uso de memória de funções específicas, ajudando a identificar gargalos e a validar otimizações. Nesta aula, vamos mergulhar nos fundamentos dos benchmarks em Go, desde a sintaxe básica até técnicas avançadas para comparar diferentes implementações.
O pacote padrão testing fornece suporte integrado para benchmarks, tornando fácil escrevê-los e executá-los juntamente com seus testes. Você verá como criar funções de benchmark, interpretar os resultados e usar ferramentas como go test -bench para obter métricas precisas. Vamos começar!
func BenchmarkXxx
Para criar um benchmark em Go, você define uma função que começa com a palavra Benchmark e recebe um parâmetro do tipo *testing.B. Essa função deve conter o código que você deseja medir. O nome da função deve seguir o padrão BenchmarkNome, onde Nome descreve o que está sendo testado. Por exemplo, BenchmarkSoma para medir uma função de soma.
Dentro da função, você usa o loop for i := 0; i < b.N; i++ para executar o código repetidamente. O valor b.N é ajustado automaticamente pelo framework para obter medições estáveis. O benchmark é executado várias vezes, com b.N aumentando até que a medição seja confiável.
package main
import (
"testing"
)
// Função que queremos testar
func Soma(a, b int) int {
return a + b
}
// Benchmark da função Soma
func BenchmarkSoma(b *testing.B) {
for i := 0; i < b.N; i++ {
Soma(1, 2)
}
}Para executar o benchmark, use o comando go test -bench=. no diretório do pacote. O argumento -bench aceita uma expressão regular para filtrar quais benchmarks rodar. Por exemplo, go test -bench=Soma executa apenas o benchmark BenchmarkSoma. A saída mostrará o número de iterações e o tempo médio por iteração.
$ go test -bench=.
BenchmarkSoma-8 1000000000 0.2520 ns/op
PASS
ok _/tmp 0.337sO número 8 após o nome indica o número de CPUs usadas (GOMAXPROCS). O primeiro número é o total de iterações, e o segundo é o tempo por operação (ns/op). Quanto menor, melhor.
b.N
O campo b.N é um inteiro que representa o número de iterações que o benchmark deve executar. O framework de testes ajusta automaticamente esse valor para garantir que o benchmark tenha duração suficiente para obter uma medição precisa. Ele começa com um valor pequeno (geralmente 1) e aumenta progressivamente até que o tempo de execução seja estável.
Você não deve modificar b.N manualmente, mas pode usá-lo para controlar o loop. Em benchmarks mais complexos, você pode precisar inicializar dados dentro do loop, mas cuidado para não incluir a inicialização na medição. Se a inicialização for significativa, é comum usar b.ResetTimer() para zerar o timer após a preparação.
func BenchmarkComplexo(b *testing.B) {
// Preparação (não medido)
data := make([]int, 1000)
for i := range data {
data[i] = i
}
b.ResetTimer() // Zera o timer
for i := 0; i < b.N; i++ {
// Código medido
_ = processa(data)
}
}Além disso, você pode usar b.StopTimer() e b.StartTimer() para pausar e retomar a medição, útil quando você precisa fazer operações que não devem ser contabilizadas, como alocar grandes estruturas.
Medindo alocações
Além do tempo, é crucial medir o número de alocações de memória, pois alocações excessivas podem impactar o desempenho e a garbage collection. O Go fornece o método b.ReportAllocs() para incluir estatísticas de alocação nos resultados do benchmark.
func BenchmarkComAlocacao(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = make([]int, 100)
}
}Ao executar com go test -bench=., a saída incluirá uma coluna adicional B/op (bytes por operação) e allocs/op (alocações por operação).
BenchmarkComAlocacao-8 100000000 15.3 ns/op 800 B/op 1 allocs/opPara evitar alocações desnecessárias, você pode usar b.ReportAllocs() e analisar os resultados. Uma prática comum é pré-alocar buffers e reutilizá-los, ou usar tipos que não causam alocação no heap.
Comparando
Muitas vezes, você precisa comparar o desempenho de diferentes implementações para decidir qual usar. O Go oferece a ferramenta benchstat para comparar resultados de benchmarks entre execuções ou entre versões de código. Primeiro, instale a ferramenta:
go install golang.org/x/perf/cmd/benchstat@latestEm seguida, execute os benchmarks e salve os resultados em arquivos, depois use benchstat para compará-los.
# Executa e salva resultados
$ go test -bench=. -count=5 > old.txt
$ go test -bench=. -count=5 > new.txt
# Compara
$ benchstat old.txt new.txt
name old time/op new time/op delta
Soma-8 0.25ns ± 1% 0.25ns ± 1% ~ (p=0.954 n=5+5)O benchstat calcula a diferença percentual e a significância estatística. Isso é essencial para validar melhorias de desempenho.
Além disso, você pode usar go test -benchmem para incluir estatísticas de memória diretamente na saída, sem precisar chamar b.ReportAllocs() em cada função. A flag -benchmem adiciona colunas de alocação a todos os benchmarks.
$ go test -bench=. -benchmem
BenchmarkSoma-8 1000000000 0.25 ns/op 0 B/op 0 allocs/opBoas práticas
Ao escrever benchmarks, siga estas recomendações:
- Evite otimizações prematuras: meça primeiro, otimize depois.
- Use
b.ResetTimer()para excluir a inicialização da medição. - Use
b.RunParallel()para benchmarks que testam concorrência. - Execute benchmarks várias vezes (
-count=5) para obter resultados estáveis. - Compare com
benchstatpara decisões baseadas em dados.
Referências
- Documentação oficial do pacote testing - Benchmarks
- Go Blog: Benchmarks
- Documentação do benchstat
- How to write benchmarks in Go - Dave Cheney
- Go by Example: Testing and Benchmarking
- Go Wiki: Learn Testing
Exercícios
- Escreva um benchmark para a função
ConcatStringque concatena duas strings usando+. Execute-o e observe o tempo. - Modifique o benchmark anterior para usar
strings.Buildere compare os resultados usandobenchstat. - Adicione
b.ReportAllocs()a um benchmark e explique o que as colunasB/opeallocs/opsignificam. - Crie um benchmark que use
b.ResetTimer()para excluir a inicialização de um slice grande. - Use
go test -benchmempara medir alocações de um benchmark e identifique se há alocações desnecessárias.
func ConcatString(a, b string) string {
return a + b
}
func BenchmarkConcatString(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = ConcatString("hello", "world")
}
}func ConcatBuilder(a, b string) string {
var sb strings.Builder
sb.WriteString(a)
sb.WriteString(b)
return sb.String()
}
func BenchmarkConcatBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = ConcatBuilder("hello", "world")
}
}
// Execute: go test -bench=Concat -benchmem -count=5 > old.txt
// e depois compare com benchstat.B/op é o número médio de bytes alocados por operação, e allocs/op é o número médio de alocações por operação. Exemplo: func BenchmarkAloca(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = make([]int, 10)
}
}func BenchmarkProcessa(b *testing.B) {
data := make([]int, 10000)
for i := range data {
data[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = processa(data)
}
}go test -benchmem e observe a saída. Se allocs/op for alto, você pode investigar o código para reduzir alocações, por exemplo, reutilizando buffers ou usando ponteiros. Exemplo de saída: BenchmarkExemplo-8 1000000 1234 ns/op 512 B/op 3 allocs/op