1. go
  2. /testing
  3. /benchmarking

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