Benchmarking in Go

Benchmarking helps you measure the performance of your Go code. Go’s testing package includes built-in support for benchmarks, making it easy to compare different implementations and spot performance issues.

Basic Benchmark

To write a benchmark, create a function that starts with Benchmark and takes a *testing.B parameter. The testing framework will run your code multiple times to get accurate measurements.

fib_test.go
package main

import "testing"

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

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

Run benchmarks with go test -bench=. or go test -bench=BenchmarkFibonacci. You’ll see output like:

BenchmarkFibonacci-8   	  100000	     15296 ns/op

This means the benchmark ran 100000 times, with each operation taking about 15296 nanoseconds.

Comparing Implementations

Benchmarks are great for comparing different ways to solve the same problem:

fib_test.go
package main

import "testing"

// Recursive implementation
func FibonacciRecursive(n int) int {
    if n <= 1 {
        return n
    }
    return FibonacciRecursive(n-1) + FibonacciRecursive(n-2)
}

// Iterative implementation
func FibonacciIterative(n int) int {
    if n <= 1 {
        return n
    }
    a, b := 0, 1
    for i := 2; i <= n; i++ {
        a, b = b, a+b
    }
    return b
}

func BenchmarkFibonacciRecursive(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibonacciRecursive(20)
    }
}

func BenchmarkFibonacciIterative(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibonacciIterative(20)
    }
}

Running this will show which implementation is faster.

Benchmarking with Setup

Sometimes you need to set up data before benchmarking:

sort_test.go
package main

import (
    "math/rand"
    "sort"
    "testing"
)

func BenchmarkSort(b *testing.B) {
    // Setup: create a slice of random numbers
    data := make([]int, 1000)
    for i := range data {
        data[i] = rand.Int()
    }
    
    b.ResetTimer() // Reset timer to exclude setup time
    
    for i := 0; i < b.N; i++ {
        // Make a copy to avoid modifying the original
        copy := make([]int, len(data))
        copy(copy, data)
        sort.Ints(copy)
    }
}

Use b.ResetTimer() to exclude setup time from the measurement.

Sub-Benchmarks

For testing different variations of the same operation:

string_test.go
package main

import (
    "strings"
    "testing"
)

func BenchmarkStringConcat(b *testing.B) {
    s1 := "Hello"
    s2 := "World"
    
    b.Run("plus", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = s1 + s2
        }
    })
    
    b.Run("sprintf", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = fmt.Sprintf("%s%s", s1, s2)
        }
    })
    
    b.Run("builder", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var builder strings.Builder
            builder.WriteString(s1)
            builder.WriteString(s2)
            _ = builder.String()
        }
    })
}

This will run separate benchmarks for each concatenation method.

Memory Benchmarks

To measure memory allocations:

slice_test.go
package main

import "testing"

func BenchmarkSliceAppend(b *testing.B) {
    b.ReportAllocs()
    
    var s []int
    for i := 0; i < b.N; i++ {
        s = append(s, i)
    }
}

b.ReportAllocs() includes memory allocation statistics in the output.

Parallel Benchmarks

To test concurrent performance:

parallel_test.go
package main

import "testing"

func BenchmarkParallel(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            // Do some work
            _ = FibonacciIterative(10)
        }
    })
}

This runs the benchmark in parallel across multiple goroutines.

Running Benchmarks

  • go test -bench=.: Run all benchmarks
  • go test -bench=BenchmarkName: Run specific benchmark
  • go test -bench=. -benchmem: Include memory allocation info
  • go test -bench=. -count=5: Run benchmarks 5 times for more accurate results
  • go test -bench=. -benchtime=10s: Run benchmarks for at least 10 seconds

Interpreting Results

  • ns/op: Nanoseconds per operation - lower is better
  • B/op: Bytes allocated per operation
  • allocs/op: Number of allocations per operation

When comparing benchmarks, look for significant differences (usually 10% or more).

Common Benchmarking Mistakes

  1. Including setup time: Use b.ResetTimer() to exclude setup
  2. Not resetting state: Make sure each iteration starts with clean state
  3. Testing too small operations: Use b.N to run enough iterations
  4. Ignoring compiler optimizations: Benchmarks can be affected by inlining, etc.
  5. Not using realistic data: Test with data that matches real usage

Profiling with Benchmarks

You can use benchmarks to trigger profiling:

go test -bench=. -cpuprofile=cpu.prof
go tool pprof cpu.prof

This helps identify performance bottlenecks.

Remember, benchmarks help you understand performance characteristics, but always profile your actual application for the best insights.

For more on profiling, see our profiling tutorial. If you need to learn about testing basics first, check out unit testing.

Last updated on