Escrever benchmarks em Go é uma prática essencial para desenvolvedores que buscam otimizar a performance de suas aplicações.
Um benchmark bem elaborado pode revelar gargalos e oportunidades de otimização que, de outra forma, poderiam passar despercebidos.
No entanto, existem armadilhas comuns que podem distorcer os resultados dos benchmarks, levando a conclusões incorretas.
Neste post, exploraremos os principais cuidados que devem ser tomados ao escrever benchmarks em Go, com exemplos de código que ilustram boas e más práticas.
Importância de escrever Benchmarks
Benchmarking é uma técnica que permite medir a eficiência de determinadas partes do código, como funções ou métodos, em termos de tempo de execução.
Ao escrever benchmarks, você pode identificar quais partes do código precisam ser otimizadas e quais já estão suficientemente rápidas. No contexto de desenvolvimento de software, benchmarks são fundamentais para garantir que mudanças no código não degradem a performance.
No entanto, como dito anteriormente, existem algumas armadilhas que podem nos levar a conclusões incorretas.
Reiniciar o Timer
O método b.ResetTimer() é usado para garantir que o benchmark não inclua o tempo gasto em operações de setup ou inicialização, ou seja, funções irrelevantes para o código sendo medido. É crucial reiniciar o timer logo antes da parte do código que realmente interessa ser medida.
Exemplo Ruim:
func BenchmarkWithoutReset(b *testing.B) {
expensiveSetup()
for i := 0; i < b.N; i++ {
operation()
}
}
Exemplo Bom:
func BenchmarkWithReset(b *testing.B) {
expensiveSetup()
b.ResetTimer()
for i := 0; i < b.N; i++ {
operation()
}
}
No exemplo ruim, o tempo de execução do expensiveSetup() está incluído nos resultados do benchmark, o que pode levar a interpretações equivocadas. O exemplo bom isola a operação que realmente importa.
Parar o Timer
Em alguns casos, você pode querer excluir operações de cleanup ou manipulações de dados dos resultados do benchmark. O método b.StopTimer() é útil para pausar a medição do tempo durante essas operações.
Exemplo Ruim:
func BenchmarkWithoutStop(b *testing.B) {
for i := 0; i < b.N; i++ {
result := operation()
cleanup(result)
}
}
Exemplo Bom:
func BenchmarkWithStop(b *testing.B) {
b.StopTimer()
for i := 0; i < b.N; i++ {
b.StartTimer()
result := operation()
b.StopTimer()
cleanup(result)
}
}
No exemplo ruim, o tempo de execução da função cleanup() é incluído no benchmark, distorcendo os resultados. O exemplo bom corrige isso, garantindo que apenas o tempo de execução da operação principal seja medido.
Cache de CPU
O cache de CPU pode influenciar significativamente os resultados de um benchmark. Operações que cabem inteiramente no cache podem parecer mais rápidas do que realmente são em um cenário de produção, onde os dados podem não estar todos no cache.
Exemplo Ruim:
func BenchmarkWithCache(b *testing.B) {
data := make([]int, 1024)
for i := 0; i < b.N; i++ {
for j := range data {
data[j]++
}
}
}
Exemplo Bom:
func BenchmarkWithoutCache(b *testing.B) {
data := make([]int, 10*1024*1024) // grande o suficiente para não caber no cache
for i := 0; i < b.N; i++ {
for j := range data {
data[j]++
}
}
}
No exemplo ruim, o conjunto de dados é pequeno o suficiente para caber no cache da CPU, o que pode levar a resultados de benchmark otimistas. No exemplo bom, o tamanho dos dados é aumentado para simular um cenário mais realista.
Importância de Usar Variáveis Globais
Usar variáveis globais em benchmarks pode prevenir que o compilador faça otimizações indesejadas, como a eliminação de código que ele considera desnecessário.
Exemplo Ruim:
func BenchmarkLocalVar(b *testing.B) {
for i := 0; i < b.N; i++ {
x := DoSomething()
_ = x
}
}
Exemplo Bom:
var result int
func BenchmarkGlobalVar(b *testing.B) {
for i := 0; i < b.N; i++ {
result = DoSomething()
}
}
No exemplo ruim, a variável x é local e pode ser otimizada pelo compilador, resultando em um benchmark inútil. No exemplo bom, a variável result é global, forçando o compilador a realizar realmente a operação, proporcionando um benchmark mais preciso.
Conclusão
Escrever benchmarks precisos em Go exige atenção a detalhes que podem parecer insignificantes, mas que têm um impacto enorme nos resultados.
Ao seguir as melhores práticas discutidas neste post — como reiniciar e parar o timer nos momentos apropriados, considerar o impacto do cache de CPU e utilizar variáveis globais para evitar otimizações indesejadas —, você pode garantir que seus benchmarks reflitam com precisão a performance do código. Assim, as otimizações realizadas terão um efeito real e positivo no desempenho da aplicação em produção.
Até a próxima!
Faça parte da comunidade!
Receba os melhores conteúdos sobre Go, Kubernetes, arquitetura de software, Cloud e esteja sempre atualizado com as tendências e práticas do mercado.
Livros Recomendados
Abaixo listei alguns dos melhores livros que já li sobre GO.

