Un benchmark en Go es una prueba automática que mide cuánto tarda en ejecutarse una parte del código.
No sirve para demostrar que algo funciona. Para eso tenemos tests. Un benchmark sirve para responder otra pregunta: ¿cuánto cuesta esto?
Go incluye soporte de benchmarks en el mismo paquete testing, así que podemos medir rendimiento sin instalar herramientas externas.
Crear un benchmark
Supongamos esta función:
package texto
func Repetir(s string, n int) string {
resultado := ""
for i := 0; i < n; i++ {
resultado += s
}
return resultado
}Creamos un fichero texto_test.go:
package texto
import "testing"
func BenchmarkRepetir(b *testing.B) {
for b.Loop() {
Repetir("go", 100)
}
}La función empieza por Benchmark y recibe b *testing.B.
El bucle de benchmark
Desde Go 1.24, la forma recomendada es usar b.Loop().
for b.Loop() {
// Código que queremos medir
}Go ejecuta el bucle tantas veces como necesita para alcanzar el tiempo de medición. b.Loop() excluye automáticamente la preparación y la limpieza situadas fuera del bucle, impide depender de la iteración actual y mantiene vivos los argumentos y resultados de las llamadas dentro del cuerpo.
También veréis muchos ejemplos antiguos con b.N:
for i := 0; i < b.N; i++ {
// Código que queremos medir
}Ese patrón sigue funcionando, pero para código nuevo conviene usar b.Loop().
Ejecutar benchmarks
Usamos:
go test -bench=.Salida típica:
BenchmarkRepetir-12 120000 9800 ns/op
PASSEl número final (ns/op) indica nanosegundos por operación. Cuanto menor, mejor, siempre que estemos midiendo lo correcto.
Evitar que el compilador elimine el trabajo
En un benchmark clásico, si el resultado no se usa, el compilador puede eliminar parte del trabajo.
Con b.Loop() los argumentos, resultados y variables asignadas dentro del cuerpo se mantienen vivos, siempre que la condición esté escrita exactamente como b.Loop(). Por eso no necesitamos una variable global solo para evitar la eliminación del resultado:
func BenchmarkRepetir(b *testing.B) {
for b.Loop() {
Repetir("go", 100)
}
}Preparación dentro y fuera del bucle
Muchas veces necesitamos preparar datos antes de medir.
func BenchmarkProcesar(b *testing.B) {
datos := cargarDatosDePrueba()
for b.Loop() {
Procesar(datos)
}
}Con b.Loop(), la preparación anterior queda fuera de la medición. Si necesitamos preparar datos en cada iteración, podemos rodear esa parte con b.StopTimer() y b.StartTimer(). b.ResetTimer() sigue apareciendo en benchmarks clásicos con b.N, pero no hace falta en este ejemplo.
Medir asignaciones de memoria
Para ver asignaciones:
go test -bench=. -benchmemSalida:
BenchmarkRepetir-12 120000 9800 ns/op 5300 B/op 99 allocs/opEsto nos dice:
B/op: bytes asignados por operación.allocs/op: número de asignaciones por operación.
En Go, muchas optimizaciones interesantes consisten en reducir asignaciones, no solo en ahorrar nanosegundos.
Comparar dos implementaciones
Podemos escribir otro benchmark usando strings.Builder, que es más adecuado para concatenar muchas piezas.
func RepetirBuilder(s string, n int) string {
var builder strings.Builder
for i := 0; i < n; i++ {
builder.WriteString(s)
}
return builder.String()
}
func BenchmarkRepetirBuilder(b *testing.B) {
for b.Loop() {
RepetirBuilder("go", 100)
}
}Ahora ejecutamos:
go test -bench=. -benchmemUna sola ejecución puede verse afectada por frecuencia de CPU, temperatura y procesos del sistema. Para una comparación seria, ejecutamos varias muestras y usamos benchstat:
go test -run '^$' -bench=. -benchmem -count=10 > resultados.txt
benchstat resultados.txtbenchstat pertenece a golang.org/x/perf/cmd/benchstat. Lo importante no es que una versión parezca más rápida, sino tener datos repetidos en nuestro caso concreto.