Go Benchmarking Tutorial
Benchmarking in Go helps measure and optimize code performance. The testing package provides powerful benchmarking tools that integrate seamlessly with your test suite. This tutorial covers everything from basic benchmarks to advanced techniques.
1. Benchmarking Basics
1.1 Benchmark Structure
Go benchmarks follow specific conventions:
- Benchmark functions start with
Benchmark - Take a single
*testing.Bparameter - Contain a loop running
b.Ntimes
// sum.go
func Sum(numbers []int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
// sum_test.go
func BenchmarkSum(b *testing.B) {
numbers := []int{1, 2, 3, 4, 5}
for i := 0; i < b.N; i++ {
Sum(numbers)
}
}
1.2 Running Benchmarks
Execute benchmarks with specific flags:
# Run all benchmarks
go test -bench=.
# Run specific benchmark
go test -bench=BenchmarkSum
# Run with memory allocation stats
go test -bench=. -benchmem
# Run with CPU profiling
go test -bench=. -cpuprofile=cpu.out
2. Benchmark Measurement
2.1 Timing Control
Accurate measurement requires proper timer control:
func BenchmarkExpensiveSetup(b *testing.B) {
// Setup not measured
data := make([]int, 10000)
for i := range data {
data[i] = i
}
b.ResetTimer() // Reset timer after setup
for i := 0; i < b.N; i++ {
process(data) // Only measure this
}
}
2.2 Memory Allocation Tracking
Measure memory usage with -benchmem:
func BenchmarkAllocations(b *testing.B) {
b.ReportAllocs() // Explicit allocation reporting
for i := 0; i < b.N; i++ {
// Slice creation causes allocations
data := make([]int, 100)
_ = data
}
}
3. Advanced Benchmark Patterns
3.1 Sub-benchmarks
Group related benchmarks with different inputs:
func BenchmarkProcess(b *testing.B) {
benchmarks := []struct {
name string
size int
}{
{"Small", 10},
{"Medium", 100},
{"Large", 1000},
}
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
data := make([]int, bm.size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
process(data)
}
})
}
}
3.2 Parallel Benchmarks
Test concurrent performance:
func BenchmarkParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// Concurrent benchmark code
process(data)
}
})
}
4. Benchmark Comparison
4.1 Comparing Implementations
Test alternative implementations side-by-side:
func BenchmarkOriginal(b *testing.B) {
data := generateData(1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
originalAlgorithm(data)
}
}
func BenchmarkOptimized(b *testing.B) {
data := generateData(1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
optimizedAlgorithm(data)
}
}
4.2 Benchmark Statistics
Interpret benchmark output:
BenchmarkSum-8 10000000 120 ns/op 0 B/op 0 allocs/op
BenchmarkParallelSum-8 20000000 65 ns/op 0 B/op 0 allocs/op
Key metrics:
- 10000000: Number of iterations
- 120 ns/op: Time per operation
- 0 B/op: Bytes allocated per operation
- 0 allocs/op: Memory allocations per operation
5. Benchmark Profiling
5.1 Generating Profiles
Create performance profiles for analysis:
# CPU profile
go test -bench=. -cpuprofile=cpu.pprof
# Memory profile
go test -bench=. -memprofile=mem.pprof
# Block profile
go test -bench=. -blockprofile=block.pprof
5.2 Analyzing Profiles
Use pprof to analyze performance:
# Interactive terminal mode
go tool pprof cpu.pprof
# Web UI visualization
go tool pprof -http=:8080 cpu.pprof
# Top CPU consumers
(pprof) top10
# Generate flamegraph
go tool pprof -http=:8080 -no_browser -tags= -nodefraction=0 -edgefraction=0 -nodecount=100000 cpu.pprof
6. Benchmark Best Practices
6.1 Benchmark Design Principles
- Isolate the code under test: Minimize setup in measurement loop
- Use realistic data: Test with production-like inputs
- Run benchmarks multiple times: Use
countflag to detect variance - Compare fairly: Ensure all benchmarks use same input data
6.2 Common Pitfalls
// ❌ Bad: Including setup in measurement
func BenchmarkBad(b *testing.B) {
for i := 0; i < b.N; i++ {
data := makeLargeDataset() // Expensive setup in loop
process(data)
}
}
// ✅ Good: Proper timer control
func BenchmarkGood(b *testing.B) {
data := makeLargeDataset() // Setup outside loop
b.ResetTimer()
for i := 0; i < b.N; i++ {
process(data)
}
}
// ❌ Bad: Not enough iterations
func BenchmarkShort(b *testing.B) {
for i := 0; i < 10; i++ { // Fixed small number
process(data)
}
}
// ✅ Good: Let testing.B determine iterations
func BenchmarkProper(b *testing.B) {
for i := 0; i < b.N; i++ {
process(data)
}
}
7. Real-world Benchmark Examples
7.1 String Concatenation
Compare different string building methods:
func BenchmarkConcatPlus(b *testing.B) {
for i := 0; i < b.N; i++ {
var s string
for j := 0; j < 100; j++ {
s += "a"
}
_ = s
}
}
func BenchmarkConcatBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
var builder strings.Builder
for j := 0; j < 100; j++ {
builder.WriteString("a")
}
_ = builder.String()
}
}
7.2 JSON Encoding
Benchmark different encoding approaches:
type User struct {
ID int
Name string
Email string
}
func BenchmarkJSONMarshal(b *testing.B) {
user := User{1, "Alice", "[email protected]"}
for i := 0; i < b.N; i++ {
_, err := json.Marshal(user)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkJSONEncoder(b *testing.B) {
user := User{1, "Alice", "[email protected]"}
for i := 0; i < b.N; i++ {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
if err := encoder.Encode(user); err != nil {
b.Fatal(err)
}
}
}