Writing Benchmarks in Go
Go's testing package includes built-in support for benchmarking code performance. This guide covers how to write, run, and analyze benchmarks effectively.
Basic Benchmarks
Writing Your First Benchmark
func BenchmarkExample(b *testing.B) {
// Reset timer before main benchmark loop
b.ResetTimer()
// Run the target code b.N times
for i := 0; i < b.N; i++ {
Calculate(100)
}
}
Running Benchmarks
# Run all benchmarks
go test -bench=.
# Run specific benchmark
go test -bench=BenchmarkExample
# Run benchmarks with more iterations
go test -bench=. -benchtime=5s
# Run benchmarks with memory allocation stats
go test -bench=. -benchmem
Advanced Benchmarking
Benchmarking with Setup
func BenchmarkComplexOperation(b *testing.B) {
// Setup code
data := make([]int, 1000)
for i := range data {
data[i] = rand.Intn(1000)
}
// Reset timer after setup
b.ResetTimer()
for i := 0; i < b.N; i++ {
ProcessData(data)
}
}
Sub-benchmarks
func BenchmarkProcessing(b *testing.B) {
sizes := []int{100, 1000, 10000}
for _, size := range sizes {
b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
data := make([]int, size)
for i := range data {
data[i] = rand.Intn(size)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
ProcessData(data)
}
})
}
}
Memory Benchmarks
Measuring Allocations
func BenchmarkMemoryUsage(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
data := make([]int, 1000)
ProcessData(data)
}
}
Comparing Implementations
func BenchmarkImplementation(b *testing.B) {
implementations := map[string]func([]int) int{
"simple": SimpleSum,
"optimized": OptimizedSum,
}
data := make([]int, 1000)
for i := range data {
data[i] = rand.Intn(100)
}
for name, impl := range implementations {
b.Run(name, func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
impl(data)
}
})
}
}
Parallel Benchmarks
Testing Concurrent Code
func BenchmarkParallel(b *testing.B) {
// Run parallel benchmark
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
ProcessData(100)
}
})
}
Controlling Parallelism
func BenchmarkWithParallelism(b *testing.B) {
// Set number of parallel routines
b.SetParallelism(4)
b.RunParallel(func(pb *testing.PB) {
// Local setup per parallel routine
local := NewLocalState()
for pb.Next() {
local.Process()
}
})
}
Best Practices
1. Reset Timer Appropriately
func BenchmarkWithSetup(b *testing.B) {
// Setup phase
heavySetup()
// Reset timer before the operation we want to measure
b.ResetTimer()
for i := 0; i < b.N; i++ {
Operation()
}
}
2. Prevent Compiler Optimizations
func BenchmarkCompute(b *testing.B) {
var result int
for i := 0; i < b.N; i++ {
result = Compute(100)
}
// Prevent compiler from optimizing away the computation
if result > 0 {
b.Log("Positive result")
}
}
3. Use Realistic Data
func BenchmarkRealWorld(b *testing.B) {
// Load real-world test data
data := loadTestData()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ProcessRealData(data)
}
}
4. Profile Memory Usage
func BenchmarkMemory(b *testing.B) {
b.ReportAllocs()
// Record initial memory stats
var m runtime.MemStats
runtime.ReadMemStats(&m)
initialAlloc := m.TotalAlloc
for i := 0; i < b.N; i++ {
ProcessWithMemory()
}
// Record final memory stats
runtime.ReadMemStats(&m)
b.Logf("Memory used: %d bytes", m.TotalAlloc-initialAlloc)
}
Analysis Tools
Using pprof
import "runtime/pprof"
func BenchmarkWithProfile(b *testing.B) {
// CPU profiling
if cpuProfile, err := os.Create("cpu.prof"); err == nil {
pprof.StartCPUProfile(cpuProfile)
defer pprof.StopCPUProfile()
}
// Memory profiling
if memProfile, err := os.Create("mem.prof"); err == nil {
defer func() {
runtime.GC()
pprof.WriteHeapProfile(memProfile)
memProfile.Close()
}()
}
for i := 0; i < b.N; i++ {
ComplexOperation()
}
}
Analyzing Results
# Generate CPU profile
go test -bench=. -cpuprofile=cpu.prof
# Generate memory profile
go test -bench=. -memprofile=mem.prof
# Analyze with pprof
go tool pprof cpu.prof
go tool pprof mem.prof
# Generate profile graph
go tool pprof -png cpu.prof > cpu.png
Common Patterns
Benchmark State
type BenchmarkState struct {
data []int
config *Config
cleanup func()
}
func setupBenchmark(b *testing.B) *BenchmarkState {
b.Helper()
state := &BenchmarkState{
data: generateData(1000),
config: NewConfig(),
}
state.cleanup = func() {
// Cleanup resources
}
return state
}
func BenchmarkWithState(b *testing.B) {
state := setupBenchmark(b)
defer state.cleanup()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ProcessWithState(state.data, state.config)
}
}
Next Steps
- Learn about Test Coverage
- Explore Mocking
- Study HTTP Testing