3.2. 基准测试 (Benchmark)

3.2.1. 基准测试基础

Go 内置了基准测试框架,用于测量代码性能。

3.2.1.1. 基本语法

// benchmark_test.go
package mypackage

import "testing"

func BenchmarkFunction(b *testing.B) {
    for i := 0; i < b.N; i++ {
        // 被测试的代码
        MyFunction()
    }
}

3.2.1.2. 运行基准测试

# 运行所有基准测试
go test -bench=.

# 运行特定基准测试
go test -bench=BenchmarkFunction

# 运行并显示内存分配
go test -bench=. -benchmem

# 指定运行时间
go test -bench=. -benchtime=5s

# 运行多次取平均
go test -bench=. -count=5

3.2.2. 理解测试结果

BenchmarkFunction-8    1000000    1234 ns/op    256 B/op    4 allocs/op

字段

含义

BenchmarkFunction-8

测试名称-GOMAXPROCS

1000000

运行次数

1234 ns/op

每次操作耗时

256 B/op

每次操作分配的字节数

4 allocs/op

每次操作的内存分配次数

3.2.3. 高级基准测试技巧

3.2.3.1. 1. 子基准测试

func BenchmarkConcat(b *testing.B) {
    sizes := []int{10, 100, 1000}
    for _, size := range sizes {
        b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                concatStrings(size)
            }
        })
    }
}

3.2.3.2. 2. 重置计时器

func BenchmarkExpensiveSetup(b *testing.B) {
    // 昂贵的初始化
    data := loadLargeDataSet()
    
    b.ResetTimer() // 重置计时器,不计算初始化时间
    
    for i := 0; i < b.N; i++ {
        processData(data)
    }
}

3.2.3.3. 3. 暂停/恢复计时

func BenchmarkWithCleanup(b *testing.B) {
    for i := 0; i < b.N; i++ {
        data := generateData()
        
        b.StartTimer()
        processData(data)
        b.StopTimer()
        
        cleanupData(data)
    }
}

3.2.3.4. 4. 并行基准测试

func BenchmarkParallel(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            // 并行执行的代码
            myFunction()
        }
    })
}

3.2.3.5. 5. 报告自定义指标

func BenchmarkThroughput(b *testing.B) {
    totalBytes := int64(0)
    
    for i := 0; i < b.N; i++ {
        n := processData(data)
        totalBytes += int64(n)
    }
    
    b.SetBytes(totalBytes / int64(b.N)) // 报告吞吐量
}

3.2.4. 实际案例

3.2.4.1. 案例 1:字符串拼接对比

func BenchmarkStringConcat(b *testing.B) {
    strs := []string{"Hello", " ", "World", "!"}
    
    b.Run("Plus", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var s string
            for _, str := range strs {
                s += str
            }
        }
    })
    
    b.Run("StringBuilder", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var sb strings.Builder
            for _, str := range strs {
                sb.WriteString(str)
            }
            _ = sb.String()
        }
    })
    
    b.Run("Join", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = strings.Join(strs, "")
        }
    })
}

3.2.4.2. 案例 2:Map vs Slice 查找

func BenchmarkLookup(b *testing.B) {
    sizes := []int{10, 100, 1000, 10000}
    
    for _, size := range sizes {
        // 准备数据
        slice := make([]int, size)
        m := make(map[int]bool, size)
        for i := 0; i < size; i++ {
            slice[i] = i
            m[i] = true
        }
        target := size / 2
        
        b.Run(fmt.Sprintf("Slice-%d", size), func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                for _, v := range slice {
                    if v == target {
                        break
                    }
                }
            }
        })
        
        b.Run(fmt.Sprintf("Map-%d", size), func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                _ = m[target]
            }
        })
    }
}

3.2.4.3. 案例 3:sync.Pool vs 直接分配

var bufferPool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 1024)
    },
}

func BenchmarkAllocation(b *testing.B) {
    b.Run("DirectAlloc", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            buf := make([]byte, 1024)
            _ = buf
        }
    })
    
    b.Run("SyncPool", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            buf := bufferPool.Get().([]byte)
            bufferPool.Put(buf)
        }
    })
}

3.2.5. 使用 benchstat 比较结果

3.2.5.1. 安装

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

3.2.5.2. 使用

# 运行旧版本并保存结果
go test -bench=. -count=10 > old.txt

# 修改代码后运行新版本
go test -bench=. -count=10 > new.txt

# 比较结果
benchstat old.txt new.txt

3.2.5.3. 输出示例

name          old time/op  new time/op  delta
Function-8    1.23µs ± 2%  0.98µs ± 1%  -20.33%  (p=0.001 n=10+10)

name          old alloc/op new alloc/op delta
Function-8    256B ± 0%    128B ± 0%    -50.00%  (p=0.001 n=10+10)

3.2.6. 常见陷阱

3.2.6.1. 陷阱 1:编译器优化导致代码被消除

// ❌ 结果可能被优化掉
func BenchmarkBad(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = compute() // 编译器可能消除这行
    }
}

// ✅ 使用全局变量防止优化
var result int

func BenchmarkGood(b *testing.B) {
    var r int
    for i := 0; i < b.N; i++ {
        r = compute()
    }
    result = r // 防止编译器优化
}

3.2.6.2. 陷阱 2:未预热缓存

func BenchmarkWithWarmup(b *testing.B) {
    // 预热
    for i := 0; i < 100; i++ {
        myFunction()
    }
    
    b.ResetTimer()
    
    for i := 0; i < b.N; i++ {
        myFunction()
    }
}

3.2.6.3. 陷阱 3:测试数据不具代表性

// ❌ 只测试最好情况
func BenchmarkBad(b *testing.B) {
    data := []int{1, 2, 3} // 数据太小
    for i := 0; i < b.N; i++ {
        sort(data)
    }
}

// ✅ 测试多种情况
func BenchmarkGood(b *testing.B) {
    for _, size := range []int{10, 100, 1000, 10000} {
        b.Run(fmt.Sprintf("n=%d", size), func(b *testing.B) {
            data := generateRandomData(size)
            b.ResetTimer()
            for i := 0; i < b.N; i++ {
                dataCopy := make([]int, len(data))
                copy(dataCopy, data)
                sort(dataCopy)
            }
        })
    }
}

3.2.7. 最佳实践

  1. 使用 -benchmem:总是关注内存分配

  2. 多次运行:使用 -count=10 确保结果稳定

  3. 使用 benchstat:科学比较优化前后的差异

  4. 测试真实数据:使用生产环境的数据分布

  5. 避免编译器优化:使用全局变量保存结果

  6. 隔离测试环境:关闭其他程序,固定 CPU 频率

3.2.8. 参考资源