O que é Benchmark em Go?

Um benchmark em Go é uma forma de medir a performance de um trecho de código de maneira precisa e reproduzível. Assim como os testes, benchmarks são nativos da linguagem — fazem parte do package testing e são executados pelo comando go test. Não é necessário instalar nenhuma ferramenta externa.

Benchmarks respondem perguntas fundamentais: “Quantas vezes por segundo essa operação executa?”, “Quanta memória ela aloca?” e “Qual implementação é mais rápida?”. São indispensáveis para otimização de código, validação de mudanças de performance e tomada de decisões de arquitetura baseadas em dados concretos.

O sistema de benchmarks do Go usa um mecanismo adaptativo: ele executa a func benchmarked repetidamente, aumentando o número de iterações até obter uma medição estatisticamente confiável. Esse design elimina a necessidade de configurar manualmente quantas vezes executar o código.

Escrevendo seu primeiro benchmark

Um benchmark é uma func que começa com Benchmark, recebe *testing.B e está em um arquivo _test.go:

// string_utils.go
package stringutils

import "strings"

func ConcatenarLoop(palavras []string) string {
    resultado := ""
    for _, p := range palavras {
        resultado += p + " "
    }
    return strings.TrimSpace(resultado)
}

func ConcatenarBuilder(palavras []string) string {
    var sb strings.Builder
    for i, p := range palavras {
        if i > 0 {
            sb.WriteString(" ")
        }
        sb.WriteString(p)
    }
    return sb.String()
}
// string_utils_test.go
package stringutils

import "testing"

var palavras = []string{
    "Go", "é", "uma", "linguagem", "de",
    "programação", "open", "source", "criada",
    "pelo", "Google",
}

func BenchmarkConcatenarLoop(b *testing.B) {
    for i := 0; i < b.N; i++ {
        ConcatenarLoop(palavras)
    }
}

func BenchmarkConcatenarBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        ConcatenarBuilder(palavras)
    }
}

Para executar:

go test -bench=. -benchmem ./...

A saída típica será algo como:

BenchmarkConcatenarLoop-8       2847396    420.3 ns/op    352 B/op    10 allocs/op
BenchmarkConcatenarBuilder-8    6421790    186.7 ns/op    120 B/op     3 allocs/op

Cada coluna significa:

ColunaSignificado
-8Número de CPUs usadas (GOMAXPROCS)
2847396Número de iterações executadas
420.3 ns/opTempo médio por operação
352 B/opBytes alocados por operação
10 allocs/opNúmero de alocações por operação

Nesse caso, strings.Builder é 2.25x mais rápido e aloca 3x menos memória que a concatenação com +.

O loop b.N

O b.N é ajustado automaticamente pelo framework de benchmark. O Go começa com um valor pequeno (1, 100, 10000…) e vai aumentando até que a medição seja estável o suficiente. Sua única responsabilidade é garantir que o código dentro do loop executa a operação que você quer medir:

func BenchmarkOperacao(b *testing.B) {
    // Setup aqui (fora do loop b.N) — NÃO é medido
    dados := prepararDados()

    b.ResetTimer() // reseta o timer após o setup

    for i := 0; i < b.N; i++ {
        // Apenas o que está aqui é medido
        processar(dados)
    }
}

b.ResetTimer e b.StopTimer

Quando seu benchmark precisa de setup que não deve ser medido:

func BenchmarkComSetup(b *testing.B) {
    // Setup custoso
    db := conectarBancoDeTeste()
    popularbanco(db)

    b.ResetTimer() // ignora o tempo de setup

    for i := 0; i < b.N; i++ {
        consultarBanco(db)
    }
}

Para setup dentro do loop:

func BenchmarkComSetupPorIteracao(b *testing.B) {
    for i := 0; i < b.N; i++ {
        b.StopTimer()
        dados := gerarDadosAleatorios() // não é medido
        b.StartTimer()

        processar(dados) // isso é medido
    }
}

Use b.StopTimer/b.StartTimer com cuidado — eles têm overhead próprio e podem distorcer medições se a operação medida for muito rápida.

b.ReportAllocs

O método b.ReportAllocs() ativa a contagem de alocações de memória sem precisar da flag -benchmem na linha de comando:

func BenchmarkAlocacoes(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        dados := make([]byte, 1024)
        _ = processar(dados)
    }
}

A análise de alocações é crucial porque o garbage collector do Go — incluindo o recente Green Tea GC — precisa rastrear e limpar cada alocação. Menos alocações significam menos pressão no GC e melhor performance geral.

Sub-benchmarks

Assim como subtestes em testing, você pode criar sub-benchmarks para comparar variantes:

func BenchmarkOrdenacao(b *testing.B) {
    tamanhos := []int{10, 100, 1000, 10000}

    for _, n := range tamanhos {
        b.Run(fmt.Sprintf("tamanho=%d", n), func(b *testing.B) {
            dados := gerarSliceAleatorio(n)

            b.ResetTimer()
            for i := 0; i < b.N; i++ {
                copia := make([]int, len(dados))
                copy(copia, dados)
                sort.Ints(copia)
            }
        })
    }
}

Executar sub-benchmarks específicos:

go test -bench=BenchmarkOrdenacao/tamanho=1000

Benchstat para comparação estatística

A ferramenta benchstat (instalável via go install golang.org/x/perf/cmd/benchstat@latest) faz comparação estatística entre duas execuções de benchmark:

# Executar benchmark antes da otimização
go test -bench=. -benchmem -count=10 ./... > antes.txt

# Aplicar otimização no código...

# Executar benchmark depois
go test -bench=. -benchmem -count=10 ./... > depois.txt

# Comparar
benchstat antes.txt depois.txt

O -count=10 executa cada benchmark 10 vezes para obter dados estatisticamente significativos. O benchstat mostra a diferença percentual com intervalo de confiança:

                        │  antes.txt  │         depois.txt         │
                        │   sec/op    │   sec/op     vs base       │
ConcatenarLoop-8          420.3n ± 2%   186.7n ± 1%  -55.58% (p=0.000)

Profiling com pprof

Para ir além dos benchmarks e entender onde o tempo é gasto, combine benchmarks com o profiler pprof:

# Gerar perfil de CPU
go test -bench=BenchmarkMinhaFuncao -cpuprofile=cpu.prof ./...

# Gerar perfil de memória
go test -bench=BenchmarkMinhaFuncao -memprofile=mem.prof ./...

# Analisar com pprof
go tool pprof cpu.prof

# Visualizar no navegador (requer graphviz)
go tool pprof -http=:8080 cpu.prof

O pprof oferece visualizações como flame graphs e call graphs que mostram exatamente quais funções consomem mais CPU ou memória.

Evitando otimizações do compilador

O compilador Go pode eliminar código que considera sem efeito colateral. Para evitar que seu benchmark seja “otimizado para nada”:

// ERRADO — o compilador pode eliminar a chamada
func BenchmarkErrado(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Fibonacci(20) // resultado descartado — compilador pode otimizar
    }
}

// CORRETO — armazenar resultado em variável de pacote
var resultado int

func BenchmarkCorreto(b *testing.B) {
    var r int
    for i := 0; i < b.N; i++ {
        r = Fibonacci(20)
    }
    resultado = r // previne eliminação pelo compilador
}

A variável resultado em nível de package garante que o compilador não pode provar que o cálculo é desnecessário.

Padrões comuns de benchmark

Comparando implementações de map

func BenchmarkMapLookup(b *testing.B) {
    m := map[string]int{"chave": 42}

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = m["chave"]
    }
}

func BenchmarkSyncMapLookup(b *testing.B) {
    var m sync.Map
    m.Store("chave", 42)

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        m.Load("chave")
    }
}

Benchmark de operações com goroutines e channels

func BenchmarkChannelSend(b *testing.B) {
    ch := make(chan int, 1)

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        ch <- i
        <-ch
    }
}

Benchmark paralelo com b.RunParallel

func BenchmarkParalelo(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            operacaoConcorrente()
        }
    })
}

Isso testa a performance sob carga concorrente, usando GOMAXPROCS goroutines simultaneamente.

Boas práticas para benchmarks

Para obter medições confiáveis:

  • Execute com -count=10 para obter dados estatísticos significativos
  • Use benchstat para comparar resultados — não confie em uma única execução
  • Feche programas intensivos antes de executar benchmarks — eles afetam os resultados
  • Use -benchtime=3s para operações muito rápidas que precisam de mais iterações
  • Sempre use -benchmem — alocações de memória são tão importantes quanto tempo de CPU
  • Previna otimizações do compilador com a técnica da variável de pacote
  • Mantenha benchmarks junto aos testes no mesmo arquivo _test.go

Para mais sobre profiling e otimização, confira o tutorial Performance e Profiling em Go e o artigo sobre PGO (Profile-Guided Optimization).

Perguntas frequentes sobre Benchmark em Go

Com que frequência devo rodar benchmarks?

Execute benchmarks sempre que fizer mudanças em código crítico de performance. Idealmente, automatize comparações de benchmark no CI usando benchstat — assim regressões de performance são detectadas antes do merge. Projetos como o próprio Go usam ferramentas contínuas de benchmark tracking.

O que é um bom número de operações por segundo?

Depende inteiramente do contexto. Para serialização JSON, milhões de ops/sec é comum. Para queries de banco de dados, milhares pode ser excelente. O importante não é o número absoluto, mas a comparação: sua implementação está mais rápida ou mais lenta que a alternativa? O -benchmem é tão valioso quanto o tempo — menos alocações geralmente significa menos pausas de GC em produção.

Posso fazer benchmark de funções com I/O?

Sim, mas entenda que I/O (rede, disco) introduz variabilidade. Para benchmarks de I/O, use -count=20 ou mais para obter médias confiáveis. Considere usar mocks ou implementações em memória para isolar a lógica que você quer medir. O package httptest é útil para benchmarks de handlers HTTP sem rede real.

Qual a diferença entre benchmark e profiling?

Benchmark mede quanto tempo uma operação leva (throughput). Profiling identifica onde o tempo é gasto dentro da operação (hotspots). Use benchmarks para comparar implementações, e profiling (pprof) para diagnosticar gargalos. Os dois são complementares — primeiro identifique que há um problema com benchmarks, depois localize a causa com profiling.