函数格式

基准测试就是在一定的工作负载之下检测程序性能的一种方法。基准测试的基本格式如下:

  1. func BenchmarkName(b *testing.B){
  2. // ...
  3. }

基准测试以Benchmark为前缀,需要一个*testing.B类型的参数b,基准测试必须要执行b.N次,这样的测试才有对照性,b.N的值是系统根据实际情况去调整的,从而保证测试的稳定性。 testing.B拥有的方法如下:

  1. func (c *B) Error(args ...interface{})
  2. func (c *B) Errorf(format string, args ...interface{})
  3. func (c *B) Fail()
  4. func (c *B) FailNow()
  5. func (c *B) Failed() bool
  6. func (c *B) Fatal(args ...interface{})
  7. func (c *B) Fatalf(format string, args ...interface{})
  8. func (c *B) Log(args ...interface{})
  9. func (c *B) Logf(format string, args ...interface{})
  10. func (c *B) Name() string
  11. func (b *B) ReportAllocs()
  12. func (b *B) ResetTimer()
  13. func (b *B) Run(name string, f func(b *B)) bool
  14. func (b *B) RunParallel(body func(*PB))
  15. func (b *B) SetBytes(n int64)
  16. func (b *B) SetParallelism(p int)
  17. func (c *B) Skip(args ...interface{})
  18. func (c *B) SkipNow()
  19. func (c *B) Skipf(format string, args ...interface{})
  20. func (c *B) Skipped() bool
  21. func (b *B) StartTimer()
  22. func (b *B) StopTimer()

测试示例

  1. func BenchmarkSplit(b *testing.B) {
  2. for i := 0; i < b.N; i++ {
  3. SplitStr("a:b:c:d", ":")
  4. }
  5. }

然后运行测试用例:
使用代码go test -bench=Split

  1. PS E:\DEV\Go\src\code.rookieops.com\day05\splitStr> go test -bench=Split
  2. goos: windows
  3. goarch: amd64
  4. pkg: code.rookieops.com/day05/splitStr
  5. BenchmarkSplit-4 2999503 409 ns/op
  6. PASS
  7. ok code.rookieops.com/day05/splitStr 3.633s

其中BenchmarkSplit-4表示对Split函数进行基准测试,数字4表示GOMAXPROCS的值,这个对于并发基准测试很重要。2999503和409ns/op表示每次调用Split函数耗时409ns,这个结果是2999503次调用的平均值。

我们还可以为基准测试添加-benchmem参数,来获得内存分配的统计数据。

  1. go test -bench=Split -benchmem
  2. goos: windows
  3. goarch: amd64
  4. pkg: code.rookieops.com/day05/splitStr
  5. BenchmarkSplit-4 3591652 311 ns/op 112 B/op 3 allocs/op
  6. PASS
  7. ok code.rookieops.com/day05/splitStr 2.219s

其中,112 B/op表示每次操作内存分配了112字节,3 allocs/op则表示每次操作进行了3次内存分配。
我们将我们的Split函数优化如下:

  1. package splitStr
  2. import "strings"
  3. // SplitStr ..
  4. func SplitStr(s, sep string) (res []string) {
  5. // 取索引
  6. res = make([]string, 0, strings.Count(s, sep)+1)
  7. index := strings.Index(s, sep)
  8. for index >= 0 {
  9. res = append(res, s[:index])
  10. s = s[index+len(sep):]
  11. index = strings.Index(s, sep)
  12. }
  13. res = append(res, s)
  14. return
  15. }

然后运行测试代码如下:

  1. go test -bench=Split -benchmem
  2. goos: windows
  3. goarch: amd64
  4. pkg: code.rookieops.com/day05/splitStr
  5. BenchmarkSplit-4 4508462 233 ns/op 64 B/op 1 allocs/op
  6. PASS
  7. ok code.rookieops.com/day05/splitStr 3.234s

这一次我们提前使用make函数将res初始化为一个容量足够大的切片,而不再像之前一样通过调用append函数来追加。这样以来减少了2/3的内存分配次数,并且减少了一半的内存分配。

性能比较函数

性能比较函数是对一个函数处理不同请求的差别。
如下编写一个斐波拉契函数:

  1. // Fib 斐波拉契函数
  2. func Fib(n int) int {
  3. if n < 2 {
  4. return n
  5. }
  6. return Fib(n-1) + Fib(n-2)
  7. }

然后我们编写性能比较函数:

  1. func benchmarkFib(b *testing.B, n int) {
  2. for i := 0; i < b.N; i++ {
  3. Fib(n)
  4. }
  5. }
  6. func BenchmarkFib1(b *testing.B) {
  7. benchmarkFib(b, 1)
  8. }
  9. func BenchmarkFib2(b *testing.B) {
  10. benchmarkFib(b, 2)
  11. }
  12. func BenchmarkFib3(b *testing.B) {
  13. benchmarkFib(b, 3)
  14. }

运行测试用例代码如下:

  1. PS E:\DEV\Go\src\code.rookieops.com\day05\splitStr> go test -bench=Fib1
  2. goos: windows
  3. goarch: amd64
  4. pkg: code.rookieops.com/day05/splitStr
  5. BenchmarkFib1-4 278520369 4.16 ns/op
  6. PASS
  7. ok code.rookieops.com/day05/splitStr 2.819s
  8. PS E:\DEV\Go\src\code.rookieops.com\day05\splitStr> go test -bench=Fib2
  9. goos: windows
  10. goarch: amd64
  11. pkg: code.rookieops.com/day05/splitStr
  12. BenchmarkFib2-4 128817837 8.50 ns/op
  13. PASS
  14. ok code.rookieops.com/day05/splitStr 2.764s
  15. PS E:\DEV\Go\src\code.rookieops.com\day05\splitStr> go test -bench=Fib3
  16. goos: windows
  17. goarch: amd64
  18. pkg: code.rookieops.com/day05/splitStr
  19. BenchmarkFib3-4 80003732 16.0 ns/op
  20. PASS
  21. ok code.rookieops.com/day05/splitStr 2.283s

需要注意的是,默认情况下,每个基准测试至少运行1秒。如果在Benchmark函数返回时没有到1秒,则b.N的值会按1,2,5,10,20,50,…增加,并且函数再次运行。