Skip to main content
โšก Calmops

Benchmarking Go Code

Benchmarking Go Code

Benchmarking is essential for understanding and optimizing Go program performance. This guide covers writing benchmarks, analyzing results, and identifying bottlenecks.

Basic Benchmarks

Simple Benchmark

package main

import "testing"

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}

func Add(a, b int) int {
    return a + b
}

Running Benchmarks

# Run all benchmarks
go test -bench=.

# Run specific benchmark
go test -bench=BenchmarkAdd

# Show memory allocations
go test -bench=. -benchmem

# Run for specific duration
go test -bench=. -benchtime=10s

# Run with CPU count
go test -bench=. -cpu=1,2,4,8

# Save results
go test -bench=. -benchmem > results.txt

Benchmark Output

Understanding Results

BenchmarkAdd-8              1000000000     1.23 ns/op     0 B/op     0 allocs/op
  • BenchmarkAdd-8: Benchmark name and GOMAXPROCS
  • 1000000000: Number of iterations (b.N)
  • 1.23 ns/op: Nanoseconds per operation
  • 0 B/op: Bytes allocated per operation
  • 0 allocs/op: Number of allocations per operation

Benchmark Patterns

Benchmarking Different Inputs

package main

import "testing"

func BenchmarkAddSmall(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(1, 2)
    }
}

func BenchmarkAddLarge(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(1000000, 2000000)
    }
}

func Add(a, b int) int {
    return a + b
}

Benchmarking with Setup

package main

import (
    "testing"
)

func BenchmarkProcessWithSetup(b *testing.B) {
    // Setup (not counted)
    data := make([]int, 1000)
    for i := 0; i < len(data); i++ {
        data[i] = i
    }
    
    // Reset timer to exclude setup
    b.ResetTimer()
    
    // Benchmark (counted)
    for i := 0; i < b.N; i++ {
        processData(data)
    }
}

func processData(data []int) int {
    sum := 0
    for _, v := range data {
        sum += v
    }
    return sum
}

Benchmarking with Cleanup

package main

import "testing"

func BenchmarkWithCleanup(b *testing.B) {
    for i := 0; i < b.N; i++ {
        // Setup
        resource := allocateResource()
        
        // Benchmark
        b.StartTimer()
        useResource(resource)
        b.StopTimer()
        
        // Cleanup
        freeResource(resource)
    }
}

func allocateResource() *Resource {
    return &Resource{}
}

func useResource(r *Resource) {
    // Use resource
}

func freeResource(r *Resource) {
    // Free resource
}

type Resource struct{}

Comparing Implementations

Benchmark Multiple Implementations

package main

import (
    "testing"
)

// Implementation 1: Using loop
func SumLoop(data []int) int {
    sum := 0
    for _, v := range data {
        sum += v
    }
    return sum
}

// Implementation 2: Using recursion
func SumRecursive(data []int) int {
    if len(data) == 0 {
        return 0
    }
    return data[0] + SumRecursive(data[1:])
}

func BenchmarkSumLoop(b *testing.B) {
    data := make([]int, 1000)
    for i := 0; i < len(data); i++ {
        data[i] = i
    }
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        SumLoop(data)
    }
}

func BenchmarkSumRecursive(b *testing.B) {
    data := make([]int, 100)  // Smaller to avoid stack overflow
    for i := 0; i < len(data); i++ {
        data[i] = i
    }
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        SumRecursive(data)
    }
}

Memory Benchmarking

Measuring Allocations

package main

import "testing"

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

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

Analyzing Memory Usage

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

# Output shows:
# BenchmarkStringConcatenation-8    1000    1234567 ns/op    5120 B/op    100 allocs/op
# BenchmarkStringBuilder-8          5000     234567 ns/op     512 B/op      1 allocs/op

Profiling

CPU Profiling

package main

import (
    "os"
    "runtime/pprof"
    "testing"
)

func BenchmarkWithCPUProfile(b *testing.B) {
    f, _ := os.Create("cpu.prof")
    defer f.Close()
    
    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()
    
    for i := 0; i < b.N; i++ {
        expensiveOperation()
    }
}

func expensiveOperation() {
    sum := 0
    for i := 0; i < 1000000; i++ {
        sum += i
    }
}

Memory Profiling

package main

import (
    "os"
    "runtime"
    "runtime/pprof"
    "testing"
)

func BenchmarkWithMemProfile(b *testing.B) {
    f, _ := os.Create("mem.prof")
    defer f.Close()
    
    for i := 0; i < b.N; i++ {
        allocateMemory()
    }
    
    runtime.GC()
    pprof.WriteHeapProfile(f)
}

func allocateMemory() {
    data := make([]int, 1000000)
    _ = data
}

Practical Examples

Comparing String Operations

package main

import (
    "fmt"
    "strings"
    "testing"
)

func BenchmarkStringConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := "hello" + " " + "world"
        _ = s
    }
}

func BenchmarkStringFormat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := fmt.Sprintf("%s %s", "hello", "world")
        _ = s
    }
}

func BenchmarkStringJoin(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := strings.Join([]string{"hello", "world"}, " ")
        _ = s
    }
}

Comparing Data Structures

package main

import (
    "testing"
)

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

func BenchmarkSlicePreallocate(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := make([]int, 0, 1000)
        for j := 0; j < 1000; j++ {
            s = append(s, j)
        }
    }
}

func BenchmarkMapInsert(b *testing.B) {
    for i := 0; i < b.N; i++ {
        m := make(map[int]int)
        for j := 0; j < 1000; j++ {
            m[j] = j
        }
    }
}

Best Practices

โœ… Good Practices

  1. Benchmark realistic scenarios - Use real data
  2. Exclude setup time - Use ResetTimer()
  3. Run multiple times - Reduce variance
  4. Measure allocations - Use -benchmem
  5. Compare implementations - Side-by-side
  6. Profile before optimizing - Find bottlenecks
  7. Document results - Track performance
  8. Test on target hardware - Match production

โŒ Anti-Patterns

// โŒ Bad: Compiler optimizes away benchmark
func BenchmarkBad(b *testing.B) {
    for i := 0; i < b.N; i++ {
        x := 1 + 1
    }
}

// โœ… Good: Use result to prevent optimization
func BenchmarkGood(b *testing.B) {
    var result int
    for i := 0; i < b.N; i++ {
        result = 1 + 1
    }
    _ = result
}

// โŒ Bad: Including setup in benchmark
func BenchmarkBad(b *testing.B) {
    for i := 0; i < b.N; i++ {
        data := make([]int, 1000)
        processData(data)
    }
}

// โœ… Good: Exclude setup
func BenchmarkGood(b *testing.B) {
    data := make([]int, 1000)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        processData(data)
    }
}

Resources and References

Official Documentation

Tools and Resources

Summary

Benchmarking in Go:

  • Write benchmarks in *_test.go files
  • Use b.N for iteration count
  • Exclude setup with ResetTimer()
  • Measure allocations with -benchmem
  • Compare implementations side-by-side
  • Profile to find bottlenecks
  • Optimize based on data
  • Document performance results

Master benchmarking for optimized Go code.

Comments