Basic Benchmarks
Master the fundamentals of Go benchmarking with practical examples and best practices for creating reliable performance measurements.
Benchmark Fundamentals
Go's built-in testing package provides powerful benchmarking capabilities. A benchmark function must:
- Begin with the word "Benchmark"
- Take a
*testing.Bparameter - Run the code under test
b.Ntimes
Basic Benchmark Structure
package main
import (
"testing"
"strings"
)
// Simple string concatenation benchmark
func BenchmarkStringConcatenation(b *testing.B) {
for i := 0; i < b.N; i++ {
result := "hello" + "world"
_ = result // Prevent compiler optimization
}
}
// String builder benchmark for comparison
func BenchmarkStringBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
sb.WriteString("hello")
sb.WriteString("world")
_ = sb.String()
}
}
Essential Benchmark Patterns
1. Memory Allocation Measurement
func BenchmarkSliceAllocation(b *testing.B) {
b.ReportAllocs() // Report allocation statistics
for i := 0; i < b.N; i++ {
slice := make([]int, 1000)
_ = slice
}
}
// Pre-allocated slice for comparison
func BenchmarkSlicePreallocated(b *testing.B) {
b.ReportAllocs()
slice := make([]int, 1000)
for i := 0; i < b.N; i++ {
// Reuse pre-allocated slice
for j := range slice {
slice[j] = j
}
}
}
2. Setup and Teardown
func BenchmarkDatabaseOperation(b *testing.B) {
// Setup (not measured)
db := setupTestDatabase()
defer db.Close()
b.ResetTimer() // Start measuring from here
for i := 0; i < b.N; i++ {
// Only this code is measured
result := db.Query("SELECT * FROM users LIMIT 1")
result.Close()
}
}
3. Proper Resource Management
func BenchmarkFileOperation(b *testing.B) {
// Create test data
testData := make([]byte, 1024)
for i := range testData {
testData[i] = byte(i % 256)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer() // Exclude setup from measurement
file, err := os.CreateTemp("", "benchmark")
if err != nil {
b.Fatal(err)
}
b.StartTimer() // Resume measurement
// Measured operation
_, err = file.Write(testData)
if err != nil {
b.Fatal(err)
}
b.StopTimer() // Exclude cleanup
file.Close()
os.Remove(file.Name())
b.StartTimer()
}
}
Best Practices
1. Prevent Compiler Optimizations
// BAD: Compiler might optimize away the operation
func BenchmarkBad(b *testing.B) {
for i := 0; i < b.N; i++ {
result := computeExpensiveFunction()
// result is unused - might be optimized away
}
}
// GOOD: Assign to package-level variable
var result int
func BenchmarkGood(b *testing.B) {
var r int
for i := 0; i < b.N; i++ {
r = computeExpensiveFunction()
}
result = r // Prevent optimization
}
2. Realistic Data Sizes
// Test with various data sizes
func BenchmarkSortSmall(b *testing.B) {
data := generateRandomSlice(100)
b.ResetTimer()
for i := 0; i < b.N; i++ {
sort.Ints(data)
}
}
func BenchmarkSortLarge(b *testing.B) {
data := generateRandomSlice(10000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
sort.Ints(data)
}
}
3. Stable Benchmark Environment
func BenchmarkCriticalPath(b *testing.B) {
// Stabilize goroutine scheduler
runtime.GC()
runtime.GC()
// Ensure consistent CPU state
runtime.GOMAXPROCS(1)
b.ResetTimer()
for i := 0; i < b.N; i++ {
criticalOperation()
}
}
Common Pitfalls
1. Including Setup in Measurement
// BAD: Setup time included in benchmark
func BenchmarkBadSetup(b *testing.B) {
for i := 0; i < b.N; i++ {
data := generateLargeDataset() // Expensive setup
processData(data)
}
}
// GOOD: Setup excluded from measurement
func BenchmarkGoodSetup(b *testing.B) {
data := generateLargeDataset() // Setup once
b.ResetTimer()
for i := 0; i < b.N; i++ {
processData(data)
}
}
2. Incorrect Loop Structure
// BAD: Timer operations inside loop
func BenchmarkBadLoop(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
setup()
b.StartTimer()
operation()
}
}
// GOOD: Minimize timer operations
func BenchmarkGoodLoop(b *testing.B) {
for i := 0; i < b.N; i++ {
operation()
}
}
3. Non-deterministic Operations
// BAD: Random operations affect consistency
func BenchmarkBadRandom(b *testing.B) {
for i := 0; i < b.N; i++ {
data := generateRandomData() // Different each time
processData(data)
}
}
// GOOD: Consistent test data
func BenchmarkGoodConsistent(b *testing.B) {
data := generateFixedTestData()
b.ResetTimer()
for i := 0; i < b.N; i++ {
processData(data)
}
}
Running Benchmarks
Basic Execution
# Run all benchmarks
go test -bench=.
# Run specific benchmark
go test -bench=BenchmarkStringConcatenation
# Run with memory allocation stats
go test -bench=. -benchmem
# Multiple runs for stability
go test -bench=. -count=5
Advanced Options
# Control benchmark time
go test -bench=. -benchtime=10s
# Control iterations
go test -bench=. -benchtime=1000x
# CPU profiling during benchmark
go test -bench=. -cpuprofile=cpu.prof
# Memory profiling during benchmark
go test -bench=. -memprofile=mem.prof
Interpreting Results
BenchmarkStringConcatenation-8 50000000 25.4 ns/op 0 B/op 0 allocs/op
BenchmarkStringBuilder-8 30000000 41.2 ns/op 64 B/op 1 allocs/op
This output shows:
50000000: Number of iterations25.4 ns/op: Nanoseconds per operation0 B/op: Bytes allocated per operation0 allocs/op: Allocations per operation-8: Number of CPUs used
Example: Complete Benchmark Suite
package main
import (
"testing"
"fmt"
"strings"
)
// Benchmark different string joining methods
func BenchmarkStringJoinPlus(b *testing.B) {
strings := []string{"hello", "world", "this", "is", "a", "test"}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
result := ""
for _, s := range strings {
result += s
}
_ = result
}
}
func BenchmarkStringJoinBuilder(b *testing.B) {
strings := []string{"hello", "world", "this", "is", "a", "test"}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var builder strings.Builder
for _, s := range strings {
builder.WriteString(s)
}
_ = builder.String()
}
}
func BenchmarkStringJoinBuilderPrealloc(b *testing.B) {
strings := []string{"hello", "world", "this", "is", "a", "test"}
// Calculate total length
totalLen := 0
for _, s := range strings {
totalLen += len(s)
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var builder strings.Builder
builder.Grow(totalLen) // Pre-allocate
for _, s := range strings {
builder.WriteString(s)
}
_ = builder.String()
}
}
func BenchmarkStringJoinStdlib(b *testing.B) {
strings := []string{"hello", "world", "this", "is", "a", "test"}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
result := strings.Join(strings, "")
_ = result
}
}
This comprehensive approach to basic benchmarking provides the foundation for accurate performance measurement in Go applications.