go怎么通过benchmark对代码进行性能测试

其他教程   发布日期:2025年04月20日   浏览次数:165

本篇内容介绍了“go怎么通过benchmark对代码进行性能测试”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

benchmark的使用

在开发中我们要想编写高性能的代码,或者优化代码的性能时,你首先得知道当前代码的性能,在go中可以使用testing包的benchmark来做基准测试 ,首先我们写一个简单的返回随机字符串的方法

  1. func randomStr(length int) string {
  2. mathRand.Seed(time.Now().UnixNano())
  3. letters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  4. b := make([]byte, length)
  5. for i := range b {
  6. b[i] = letters[mathRand.Intn(len(letters))]
  7. }
  8. return string(b)
  9. }

要对上面的代码做基准测试,首先我们要新建一个测试文件,比如

  1. main_test.go
,然后新建一个基准测试方法
  1. BenchmarkRandomStr
,与普通的测试函数Test 开头,参数为t *testing.T类似,基准测试函数要以Benchmark开头,参数为b *testing.B,代码中的
  1. b.N
代表的是该用例的运行次数,这个值是会变的,对于每个用例都不一样,这个值会从1开始增加,具体的实现我会在下面的实现原理里进行介绍。
  1. func BenchmarkRandomStr(b *testing.B) {
  2. for i := 0; i < b.N; i++ {
  3. randomStr(10000)
  4. }
  5. }

运行Benchmark

我们可以使用

  1. go test -bench .
命令直接运行当前目录下的所有基准测试用例,-bench后面也可以跟正则或者是字符串来匹配对应的用例
  1. $ go test -bench='Str$'
  2. goos: darwin
  3. goarch: amd64
  4. pkg: learn/learn_test
  5. cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  6. BenchmarkRandomStr-12 6692 181262 ns/op
  7. PASS
  8. ok learn/learn_test 2.142s

对上面的一些关键指标我们要了解一下,首先BenchmarkRandomStr-12后面的

  1. -12
代表的是
  1. GOMAXPROCS
这个跟你机器CPU的逻辑核数有关,在基准测试中可以通过
  1. -cpu
参数指定需要以几核的cpu来运行测试用例
  1. $ go test -bench='Str$' -cpu=2,4,8 .
  2. goos: darwin
  3. goarch: amd64
  4. pkg: learn/learn_test
  5. cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  6. BenchmarkRandomStr-2 6715 181197 ns/op
  7. BenchmarkRandomStr-4 6471 180249 ns/op
  8. BenchmarkRandomStr-8 6616 179510 ns/op
  9. PASS
  10. ok learn/learn_test 4.516s

  1. 6715
  1. 181197 ns/op
代表用例执行了6715次,每次花费的时间约为0.0001812s,总耗时约为1.2s(ns:s的换算为1000000000:1)

指定测试时长或测试次数

-benchtime=3s 指定时长

-benchtime=100000x 指定次数

-coun=3 指定轮数

  1. $ go test -bench='Str$' -benchtime=3s .
  2. goos: darwin
  3. goarch: amd64
  4. pkg: learn/learn_test
  5. cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  6. BenchmarkRandomStr-12 19988 177572 ns/op
  7. PASS
  8. ok learn/learn_test 5.384s
  9. $ go test -bench='Str$' -benchtime=10000x .
  10. goos: darwin
  11. goarch: amd64
  12. pkg: learn/learn_test
  13. cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  14. BenchmarkRandomStr-12 10000 184832 ns/op
  15. PASS
  16. ok learn/learn_test 1.870s
  17. $ go test -bench='Str$' -count=2 .
  18. goos: darwin
  19. goarch: amd64
  20. pkg: learn/learn_test
  21. cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  22. BenchmarkRandomStr-12 6702 177048 ns/op
  23. BenchmarkRandomStr-12 6482 177861 ns/op
  24. PASS
  25. ok learn/learn_test 3.269s

重置时间和暂停计时

有时候我们的测试用例会需要一些前置准备的耗时行为,这对我们的测试结果会产生影响,这个时候就需要在耗时操作后重置计时。下面我们用一个伪代码来模拟一下

  1. func BenchmarkRandomStr(b *testing.B) {
  2. time.Sleep(time.Second * 2) // 模拟耗时操作
  3. for i := 0; i < b.N; i++ {
  4. randomStr(10000)
  5. }
  6. }

这时候我们再执行一下用例

  1. $ go test -bench='Str$' .
  2. goos: darwin
  3. goarch: amd64
  4. pkg: learn/learn_test
  5. cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  6. BenchmarkRandomStr-12 1 2001588866 ns/op
  7. PASS
  8. ok learn/learn_test 2.009s

发现只执行了一次,时间变成了2s多,这显然不符合我们的预期,这个时候需要调用

  1. b.ResetTime()
来重置时间
  1. func BenchmarkRandomStr(b *testing.B) {
  2. time.Sleep(time.Second * 2) // 模拟耗时操作
  3. b.ResetTimer()
  4. for i := 0; i < b.N; i++ {
  5. randomStr(10000)
  6. }
  7. }

再次执行基准测试

  1. $ go test -bench='Str$' .
  2. goos: darwin
  3. goarch: amd64
  4. pkg: learn/learn_test
  5. cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  6. BenchmarkRandomStr-12 6506 183098 ns/op
  7. PASS
  8. ok learn/learn_test 10.030s

运行次数和单次执行时间已经恢复到之前测试的情况了。基准测试还有

  1. b.StopTimer()
  1. b.StartTimer()
方法也是同样的道理,在影响耗时的操作之前停止计时,完成之后再开始计时。

查看内存使用情况

我们再评估代码的性能时,除了时间的快慢,还有一个重要的指标就是内存使用率,基准测试中可以通过

  1. -benchmem
来显示内存使用情况。下面我们用一组指定cap和不指定cap的返回int切片方法来看一下内存的使用情况
  1. func getIntArr(n int) []int {
  2. rand.Seed(uint64(time.Now().UnixNano()))
  3. arr := make([]int, 0)
  4. for i := 0; i < n; i++ {
  5. arr = append(arr, rand.Int())
  6. }
  7. return arr
  8. }
  9. func getIntArrWithCap(n int) []int {
  10. rand.Seed(uint64(time.Now().UnixNano()))
  11. arr := make([]int, 0, n)
  12. for i := 0; i < n; i++ {
  13. arr = append(arr, rand.Int())
  14. }
  15. return arr
  16. }
  17. //------------------------------------------
  18. // 基准测试代码
  19. //------------------------------------------
  20. func BenchmarkGetIntArr(b *testing.B) {
  21. for i := 0; i < b.N; i++ {
  22. getIntArr(100000)
  23. }
  24. }
  25. func BenchmarkGetIntArrWithCap(b *testing.B) {
  26. for i := 0; i < b.N; i++ {
  27. getIntArrWithCap(100000)
  28. }
  29. }

执行基准测试:

  1. $ go test -bench='Arr' -benchmem .
  2. goos: darwin
  3. goarch: amd64
  4. pkg: learn/learn_test
  5. cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  6. BenchmarkGetIntArr-12 598 1928991 ns/op 4101389 B/op 28 allocs/op
  7. BenchmarkGetIntArrWithCap-12 742 1556204 ns/op 802817 B/op 1 allocs/op
  8. PASS
  9. ok learn/learn_test 2.688s

可以看到指定了cap的方法执行的速度大约快20%,而内存的使用少了80%左右,

  1. 802817 B/op
代表每次的内存使用情况,
  1. 1 allocs/op
表示每次操作分配内存的次数

testing.B的底层实现

在写基准测试的时候,最让我搞不懂的是b.N的机制,如何根据不同的用例来自动调整执行的次数,然后我在源码中找到了一些蛛丝马迹。首先,先看一下基准测试的底层数据结构

  1. type B struct {
  2. common
  3. importPath string
  4. context *benchContext
  5. N int // 这个就是要搞懂的N,代表要执行的次数
  6. previousN int
  7. previousDuration time.Duration
  8. benchFunc func(b *B) // 测试函数
  9. benchTime durationOrCountFlag // 执行时间,默认是1s 可以通过-benchtime指定
  10. bytes int64
  11. missingBytes bool
  12. timerOn bool
  13. showAllocResult bool
  14. result BenchmarkResult
  15. parallelism int
  16. startAllocs uint64
  17. startBytes uint64
  18. netAllocs uint64
  19. netBytes uint64
  20. extra map[string]float64
  21. }

通过结构体中的N字段,可以找到几个关键的方法,

  1. runN()
:每一次执行都会调用的方法,设置N的值。
  1. run1()
:第一次迭代,根据它的结果决定是否需要运行更多的基准测试。
  1. run()
: run1()执行的结果为true的情况会调用,这个方法里调用
  1. doBench()
函数从而调用
  1. launch()
函数,这个是最终决定执行次数的函数
  1. // Run benchmarks f as a subbenchmark with the given name. It reports
  2. // whether there were any failures.
  3. //
  4. // A subbenchmark is like any other benchmark. A benchmark that calls Run at
  5. // least once will not be measured itself and will be called once with N=1.
  6. func (b *B) Run(name string, f func(b *B)) bool {
  7. // ...省略部分代码
  8. // Run()方法是基准测试的启动方法,会新建一个子测试
  9. sub := &B{
  10. common: common{
  11. signal: make(chan bool),
  12. name: benchName,
  13. parent: &b.common,
  14. level: b.level + 1,
  15. creator: pc[:n],
  16. w: b.w,
  17. chatty: b.chatty,
  18. bench: true,
  19. },
  20. importPath: b.importPath,
  21. benchFunc: f,
  22. benchTime: b.benchTime,
  23. context: b.context,
  24. }
  25. // ...省略部分代码
  26. if sub.run1() { // 执行一次子测试,如果不出错执行run()
  27. sub.run() //最终调用 launch()方法,决定需要执行多少次runN()
  28. }
  29. b.add(sub.result)
  30. return !sub.failed
  31. }
  32. // runN runs a single benchmark for the specified number of iterations.
  33. func (b *B) runN(n int) {
  34. // ....省略部分代码
  35. b.N = n //指定N
  36. // ...
  37. }
  38. // launch launches the benchmark function. It gradually increases the number
  39. // of benchmark iterations until the benchmark runs for the requested benchtime.
  40. // launch is run by the doBench function as a separate goroutine.
  41. // run1 must have been called on b.
  42. func (b *B) launch() {
  43. // ....省略部分代码
  44. d := b.benchTime.d
  45. // 最少执行时间为1s,最多执行次数为1e9次
  46. for n := int64(1); !b.failed && b.duration < d && n < 1e9; {
  47. last := n
  48. // 预测所需要的迭代次数
  49. goalns := d.Nanoseconds()
  50. prevIters := int64(b.N)
  51. prevns := b.duration.Nanoseconds()
  52. if prevns <= 0 {
  53. //四舍五入,预防除0
  54. prevns = 1
  55. }
  56. n = goalns * prevIters / prevns
  57. // 避免增长的太快,先按1.2倍增长,最少增加一次
  58. n += n / 5
  59. n = min(n, 100*last)
  60. n = max(n, last+1)
  61. // 最多执行1e9次
  62. n = min(n, 1e9)
  63. b.runN(int(n))
  64. }

以上就是go怎么通过benchmark对代码进行性能测试的详细内容,更多关于go怎么通过benchmark对代码进行性能测试的资料请关注九品源码其它相关文章!