Web Analytics

Benchmarking

Advanced ~30 min read

Benchmarking measures code performance. Go's testing package includes built-in benchmarking tools. In this lesson, you'll learn to write benchmarks, analyze results, and optimize your code.

Writing Benchmarks

// fibonacci.go
package fib

func Fibonacci(n int) int {
    if n <= 1 {
        return n
    }
    return Fibonacci(n-1) + Fibonacci(n-2)
}

// fibonacci_test.go
package fib

import "testing"

func BenchmarkFibonacci(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Fibonacci(10)
    }
}

func BenchmarkFibonacci20(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Fibonacci(20)
    }
}
Benchmark Rules:
  • Function name starts with Benchmark
  • Takes *testing.B parameter
  • Loop b.N times (framework sets N)
  • File name ends with _test.go

Running Benchmarks

# Run all benchmarks
go test -bench=.

# Run specific benchmark
go test -bench=BenchmarkFibonacci

# Run with memory stats
go test -bench=. -benchmem

# Run for longer (more accurate)
go test -bench=. -benchtime=10s

Understanding Benchmark Output

BenchmarkFibonacci-8     3000000    450 ns/op    0 B/op    0 allocs/op
BenchmarkFibonacci20-8      5000  250000 ns/op    0 B/op    0 allocs/op
Column Meaning
BenchmarkFibonacci-8 Benchmark name, 8 = GOMAXPROCS
3000000 Number of iterations (b.N)
450 ns/op Time per operation
0 B/op Bytes allocated per operation
0 allocs/op Allocations per operation

Benchmark Patterns

Table-Driven Benchmarks

func BenchmarkFibonacci(b *testing.B) {
    benchmarks := []struct {
        name string
        n    int
    }{
        {"Fib10", 10},
        {"Fib20", 20},
        {"Fib30", 30},
    }
    
    for _, bm := range benchmarks {
        b.Run(bm.name, func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                Fibonacci(bm.n)
            }
        })
    }
}

Resetting Timer

func BenchmarkWithSetup(b *testing.B) {
    // Expensive setup
    data := generateLargeDataset()
    
    b.ResetTimer()  // Don't count setup time
    
    for i := 0; i < b.N; i++ {
        process(data)
    }
}

Stopping and Starting Timer

func BenchmarkWithPauses(b *testing.B) {
    for i := 0; i < b.N; i++ {
        b.StopTimer()
        // Expensive setup per iteration
        data := setup()
        b.StartTimer()
        
        // Actual code to benchmark
        process(data)
    }
}

Comparing Performance

Example: String Concatenation

func BenchmarkStringConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := ""
        for j := 0; j < 100; j++ {
            s += "x"
        }
    }
}

func BenchmarkStringBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var sb strings.Builder
        for j := 0; j < 100; j++ {
            sb.WriteString("x")
        }
        _ = sb.String()
    }
}

Using benchstat

# Install benchstat
go install golang.org/x/perf/cmd/benchstat@latest

# Run benchmark before optimization
go test -bench=. -count=10 > old.txt

# Make changes, run again
go test -bench=. -count=10 > new.txt

# Compare
benchstat old.txt new.txt

Profiling

CPU Profiling

# Generate CPU profile
go test -bench=. -cpuprofile=cpu.prof

# Analyze with pprof
go tool pprof cpu.prof

# Commands in pprof:
# top - Show top functions
# list FunctionName - Show source
# web - Open in browser (requires graphviz)

Memory Profiling

# Generate memory profile
go test -bench=. -memprofile=mem.prof

# Analyze
go tool pprof mem.prof

In-Code Profiling

import (
    "os"
    "runtime/pprof"
)

func main() {
    // CPU profiling
    f, _ := os.Create("cpu.prof")
    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()
    
    // Your code here
    
    // Memory profiling
    mf, _ := os.Create("mem.prof")
    pprof.WriteHeapProfile(mf)
    mf.Close()
}

Optimization Tips

Optimization Rules:
  1. Measure first - Don't guess, benchmark!
  2. Optimize hot paths - Focus on frequently called code
  3. Avoid premature optimization - Clarity first, speed second
  4. Use profiling - Find real bottlenecks
  5. Benchmark changes - Verify improvements

Common Optimizations

// 1. Reduce allocations
// ❌ Slow - allocates every time
func slow() []int {
    return []int{1, 2, 3}
}

// ✅ Fast - reuse slice
var pool = []int{1, 2, 3}
func fast() []int {
    return pool
}

// 2. Use sync.Pool for temporary objects
var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func process() {
    buf := bufferPool.Get().(*bytes.Buffer)
    defer bufferPool.Put(buf)
    buf.Reset()
    // Use buf
}

// 3. Preallocate slices
// ❌ Slow - grows dynamically
s := []int{}
for i := 0; i < 1000; i++ {
    s = append(s, i)
}

// ✅ Fast - preallocated
s := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
    s = append(s, i)
}

Common Mistakes

1. Not running enough iterations

// ❌ Wrong - fixed iterations
func BenchmarkWrong(b *testing.B) {
    for i := 0; i < 100; i++ {
        doWork()
    }
}

// ✅ Correct - use b.N
func BenchmarkCorrect(b *testing.B) {
    for i := 0; i < b.N; i++ {
        doWork()
    }
}

2. Including setup in benchmark

// ❌ Wrong - setup counted
func BenchmarkWrong(b *testing.B) {
    for i := 0; i < b.N; i++ {
        data := setup()  // Counted!
        process(data)
    }
}

// ✅ Correct - reset timer
func BenchmarkCorrect(b *testing.B) {
    data := setup()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        process(data)
    }
}

Exercise: Optimize String Building

Task: Benchmark and optimize string concatenation.

Requirements:

  • Benchmark 3 methods: +=, strings.Builder, bytes.Buffer
  • Test with 100 and 1000 iterations
  • Include memory stats
  • Identify the fastest method
Show Solution
package stringbench

import (
    "bytes"
    "strings"
    "testing"
)

func BenchmarkStringConcat(b *testing.B) {
    benchmarks := []struct {
        name string
        n    int
    }{
        {"100", 100},
        {"1000", 1000},
    }
    
    for _, bm := range benchmarks {
        b.Run("Plus_"+bm.name, func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                s := ""
                for j := 0; j < bm.n; j++ {
                    s += "x"
                }
            }
        })
        
        b.Run("Builder_"+bm.name, func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                var sb strings.Builder
                for j := 0; j < bm.n; j++ {
                    sb.WriteString("x")
                }
                _ = sb.String()
            }
        })
        
        b.Run("Buffer_"+bm.name, func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                var buf bytes.Buffer
                for j := 0; j < bm.n; j++ {
                    buf.WriteString("x")
                }
                _ = buf.String()
            }
        })
    }
}

// Run with: go test -bench=. -benchmem

Summary

  • Benchmarks start with Benchmark
  • Loop b.N times for accurate results
  • go test -bench=. runs benchmarks
  • -benchmem shows memory stats
  • b.ResetTimer() excludes setup time
  • benchstat compares results
  • pprof profiles CPU and memory
  • Measure before optimizing

What's Next?

You've learned to measure and optimize performance! Now you're ready for the final lesson: Best Practices. You'll learn Go idioms, code organization, and professional development practices to write production-ready code!