Go-Benchmark入门-进阶篇(下)

news2024/7/6 20:47:41

接上文:Go-Benchmark入门-基础篇(上)

引言

本篇是进阶篇,围绕最佳实践,介绍项目中可能会用得上的一些技巧和科普更多的 benchmark 知识,也是对本人半个多月实践的一次总结和备忘。

go版本:

go1.19.4 darwin/arm64

最佳实践

提升准确度:-benchtime

默认情况下,benchmark只运行1秒钟,不过我们可以通过如下2个命令控制运行时长和测试轮次,以提高准确度:

-benchtime t
    Run enough iterations of each benchmark to take t, specified
    as a time.Duration (for example, -benchtime 1h30s).
    The default is 1 second (1s).
    The special syntax Nx means to run the benchmark N times
    (for example, -benchtime 100x).

-count n
    Run each test, benchmark, and fuzz seed n times (default 1).
    If -cpu is set, run n times for each GOMAXPROCS value.
    Examples are always run once. -count does not apply to
    fuzz tests matched by -fuzz.

根据 官方文档 ,我们看到 -benchtime 是 time.Duration 格式,小时分可以缩写为“h”、”m”、”s”单个字母,以下代表运行测试1小时10分10秒:

$ go test -bench="BenchmarkFibV2$" -benchtime=1h10m10s

另外,如果以x结尾,可以精确控制测试运行N次:

$ go test -bench="BenchmarkFibV2$" -benchtime=10x

goos: darwin
goarch: arm64
pkg: goexample/13_benchmark
BenchmarkFibV2-10             10               116.6 ns/op
PASS
ok      goexample/13_benchmark  0.414s

搭配 -count命令,我们可以精确控制测试运行多少时间(或多少次),以及运行几轮测试:

$ go test -run=none -bench="BenchmarkFibV2$" -benchtime=2s -count=3
goos: darwin
goarch: arm64
pkg: goexample/13_benchmark
BenchmarkFibV2-10       238259484                9.972 ns/op
BenchmarkFibV2-10       240662946                9.968 ns/op
BenchmarkFibV2-10       240829356                9.969 ns/op
PASS
ok      goexample/13_benchmark  10.420s

如上,我们每次测试运行2秒钟,总共运行了3次,我们看到平均每次耗时在 9.9 ns 左右,通过 -benchtime-count 这2个命令,我们可以提升测试的准确定,避免性能毛刺或抖动。

内存分配情况:-benchmem

我们可以使用 -benchmem 来观察代码的内存分配情况:

goos: darwin
goarch: arm64
pkg: server/msg-dispatcher/internal/dao
BenchmarkPushGroup_Create-10     855    1270851 ns/op    6138 B/op    142 allocs/op
PASS
ok      server/msg-dispatcher/internal/dao      1.563s
  • 142 allocs/op:表示每次函数操作会涉及 142 次内存分配,因为这里使用的 ent orm框架,这个数字看起来也不算夸张。
  • 6138 B/op:表示1次函数调用会分配 6138 Bytes内存。

我们还可以结合 -memprofile 命令导出性能文件,然后使用 pprof 工具来进行精确分析内存分配和占用情况。

  1. 生成 pprof 文件(文件名和后缀无要求)
$ go test -run=none -bench="BenchmarkPushGroup_Create$" -benchmem -memprofile="out.profile"
  1. 使用 go tool 中的pprof工具加载,并输出 top 10的内存占用情况(更多功能输入help查看):
(pprof) top 10
Showing nodes accounting for 9227.88kB, 58.09% of 15884.30kB total
Showing top 10 nodes out of 126
      flat  flat%   sum%        cum   cum%
3075.38kB 19.36% 19.36%  3075.38kB 19.36%  runtime.allocm
1024.08kB  6.45% 25.81%  1024.08kB  6.45%  server/msg-dispatcher/internal/dao/ent.(*BizPushGroupCreate).createSpec
1024.08kB  6.45% 32.26%  1536.17kB  9.67%  github.com/go-sql-driver/mysql.(*mysqlStmt).query
1024.04kB  6.45% 38.70%  1024.04kB  6.45%  server/msg-dispatcher/internal/dao/ent.(*BizPushGroupQuery).querySpec
  516.64kB  3.25% 41.95%   516.64kB  3.25%  google.golang.org/protobuf/reflect/protoregistry.(*Types).register
     514kB  3.24% 45.19%      514kB  3.24%  bufio.NewReaderSize
  513.31kB  3.23% 48.42%   513.31kB  3.23%  regexp.onePassCopy
  512.17kB  3.22% 51.65%   512.17kB  3.22%  entgo.io/ent/dialect/sql.Select
  512.09kB  3.22% 54.87%   512.09kB  3.22%  github.com/go-sql-driver/mysql.(*mysqlConn).readColumns
  512.09kB  3.22% 58.09%   512.09kB  3.22%  google.golang.org/protobuf/types/descriptorpb.init
  1. 可以通过 png 导出成图片:
(pprof) png
Generating report in profile001.png

看下图最左侧 pushGroup.Create的树形图,分配情况一目了然:
在这里插入图片描述

测试不同的输入(时间复杂度)

以 斐波拉契数列 为例,当我们需要评估算法是O(1)、O(N)、O(n^2)还是其他时间复杂度时,如果看代码不好判断(比如递归版本的解法):

func Fib(n int) int {
   if n == 0 || n == 1 {
      return n
   }
   return Fib(n-2) + Fib(n-1)
}

这种情况下,我们可以通过测试不同输入的函数耗时,然后放到折线图中判断。在下面的代码中,我们通过b.Run运行子测试的功能,一次性测试计算第10、30和40位的斐波拉契数列:

func BenchmarkFib_Table(b *testing.B) {
   var table = []int{10, 30, 40}
   for i := 0; i < len(table); i++ {
      num := table[i]
      name := fmt.Sprintf("%s_%d", "BenchmarkFib", num)
      b.Run(name, func(b *testing.B) {
         benchFib(b, num)
      })
   }
}

func benchFib(b *testing.B, num int) {
   for n := 0; n < b.N; n++ {
      Fib(num)
   }
}

输出:

goos: darwin
goarch: arm64
pkg: goexample/13_benchmark
BenchmarkFib_Table/BenchmarkFib_10-10            4750002               250.3 ns/op
BenchmarkFib_Table/BenchmarkFib_30-10                314           3787747 ns/op
BenchmarkFib_Table/BenchmarkFib_40-10                  3         467186889 ns/op
PASS
ok      goexample/13_benchmark  6.110s

转换成折线图(1-30):
在这里插入图片描述
我们可以直观看到这个线的走势,从20左右开始,折线就急剧爬升。到了30后面,几乎就是90度的走向了,所以从这根线的走势可以看出,递归版本的 斐波拉契 解法的时间复杂度,肯定不是 O(n),至少是 O(n^2) 级别,关于该递归解法的时间复杂度分析可以参考这篇文章:递归求Fibonacci数列的时间复杂度到底是多少?

跳过单元测试

如果你省略 -run 指令,其实也会运行 同一个package 下的所有单测:

$ go test -bench="BenchmarkFib_40$"
goos: darwin
goarch: arm64
pkg: goexample/13_benchmark
BenchmarkFib_40-10             3         469632750 ns/op
PASS
ok      goexample/13_benchmark  3.235s

从上面的输出看不出啥,那是因为你的单测全部 PAAS,不相信的话,我们可以加个 -v 把执行的单测详情输出看看:

$ go test -bench="BenchmarkFib_40$" -v
=== RUN   TestFib
--- PASS: TestFib (0.00s)
=== RUN   TestFibIterator
--- PASS: TestFibIterator (0.00s)
goos: darwin
goarch: arm64
pkg: goexample/13_benchmark
BenchmarkFib_40
BenchmarkFib_40-10             3         468810389 ns/op
PASS
ok      goexample/13_benchmark  2.999s

所以,如果我们需要显示跳过单测,一定别忘记给 -run 指令指定一个不匹配任何单测的正则,即可跳过单测,只运行 benchmark测试。

$ go test -run="none" -bench="BenchmarkFib"

对比benchmark结果

golang.org/x/perf 中提供了一组工具,可以对 benchmark 结果进行分析,其中一个很有用的工具就是 benchstat,可以对2次benchmark结果进行A/B比较,让我们以 斐波拉契数列 为例来演示一下。

  1. 准备 benchmark 测试代码
func BenchmarkFib_Table(b *testing.B) {
   nums := []int{10, 20, 30}
   for i := 0; i < len(nums); i++ {
      num := nums[i]
      name := fmt.Sprintf("%d", num)
      b.Run(name, func(b *testing.B) {
         for n := 0; n < b.N; n++ {
            Fib(num)              // (1)
            // FibIterator(num)   // (2)
         }
      })
   }
}
  1. 执行 递归算法版本 的测试并保存文件
$ go test -bench="BenchmarkFib$" -run=none -count 2 | tee old.txt
* `-count`:会执行2遍测试
* `tee`:会把测试结果保存成文件,同时把结果输出到控制台。
  1. 执行 迭代版本解法 的测试并保存文件
# 注释(1)位置的代码,取消(2)位置代码的注释然后运行
$ go test -bench="BenchmarkFib$" -run=none -count 2 | tee new.txt
  1. 安装 benchstat 工具
$ go install golang.org/x/perf/cmd/benchstat@latest
  1. 对比结果
$ benchstat old.txt new.txt

输出:

goos: darwin
goarch: arm64
pkg: goexample/13_benchmark
                │      old.txt       │                new.txt                │
                │       sec/op       │    sec/op     vs base                 │
Fib_Table/10-10       238.550n ± ∞ ¹   3.743n ± ∞ ¹        ~ (p=0.333 n=2) ²
Fib_Table/20-10     29431.000n ± ∞ ¹   6.889n ± ∞ ¹        ~ (p=0.333 n=2) ²
Fib_Table/30-10   3637274.000n ± ∞ ¹   9.976n ± ∞ ¹        ~ (p=0.333 n=2) ²
geomean                 29.45µ         6.360n        -99.98%
* `sec/op`:代表每次运行的平均耗时
* `geomean`:递归版本平均耗时 `30.84 us`,迭代版本解法平均 `6.6 ns`,性能提升了 `99.98%`

更多关于 bench stat工具可以参考:官方文档

结合pprof进行函数耗时分析

通过指定 -cpuprofile 可以输出cpu信息到文件中,然后就可以使用pprof来进行更深入的分析了。

举一个实际案例,假设要分析某个函数的耗时:

$ go test -run="none" -bench="BenchmarkPushDevice_BindOrUpdate" -cpuprofile=out.cpu

输出:

goos: darwin
goarch: arm64
pkg: internal/biz
BenchmarkPushDevice_BindOrUpdate-10           18          66413139 ns/op
PASS
ok      internal/biz      3.678s

我们发现这个 BindOrUpdate 1秒执行了18次,平均66ms执行一次。

这个性能比预期低,让我们使用 go tool中的 pprof 工具,调查一下到底是哪里比较耗时(微服务中一般使用trace分析函数耗时)。

使用 pprof 加载文件:

$ go tool pprof out.cpu

Type: cpu
Time: Feb 16, 2023 at 4:47pm (CST)
Duration: 2.86s, Total samples = 120ms ( 4.19%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

输入 top:

(pprof) top

Showing nodes accounting for 120ms, 100% of 120ms total
Showing top 10 nodes out of 112
      flat  flat%   sum%        cum   cum%
      40ms 33.33% 33.33%       40ms 33.33%  syscall.syscall
      20ms 16.67% 50.00%       20ms 16.67%  runtime.kevent
      20ms 16.67% 66.67%       20ms 16.67%  runtime.pthread_cond_signal
      10ms  8.33% 75.00%       10ms  8.33%  internal/dao/ent.newBizPushUserDeviceMutation
      10ms  8.33% 83.33%       10ms  8.33%  runtime.(*limiterEvent).stop
      10ms  8.33% 91.67%       10ms  8.33%  runtime.(*spanSet).push
      10ms  8.33%   100%       10ms  8.33%  runtime.step
         0     0%   100%       20ms 16.67%  bufio.(*Reader).ReadSlice
         0     0%   100%       20ms 16.67%  bufio.(*Reader).fill
         0     0%   100%       10ms  8.33%  context.WithDeadline
(pprof)

好像看不出啥,我们可以继续使用 list newBizPushUserDeviceMutation 挨个查看详情,考虑到篇幅这里就不再过多介绍了。

输入 png 导出(help查看更多pprof命令):

(pprof) png
Generating report in profile001.png

打开 png,我们看到下面3个红框的位置,分辨有3个操作每个耗时10ms(mysql操作),他们是顺序执行,所以会占用30ms,总计40ms耗时左右,1秒钟也就执行25次,和压测结果接近,至此,我们结合pprof找到了函数耗时的点,可以做针对性的性能优化了,是不是很方便?
在这里插入图片描述

ResetTimer & StartTimer & StopTimer

我们可以通过 ResetTimer 忽略初始化需要的时间,通过 StartTimerStopTimer 忽略某一段代码的耗时,如下是一个示例:

func BenchmarkNewPushDevice(b *testing.B) {
   entClient := newEntClient(b)
   redisClient := unittest.NewRedis(b)
   member := NewPushGroupMemberDao(entClient, redisClient)
   group := NewPushGroupDao(entClient, redisClient, member)

   // 忽略 redis,mysql 等初始化占用的时间
   b.ResetTimer()

   b.SetParallelism(4)
   b.RunParallel(func(pb *testing.PB) {
      for pb.Next() {
         // 忽略这段代码的耗时
         b.StopTimer()
         _ = group.Create(context.Background(), rand.Int()%100000)
         b.StartTimer()

         err := member.Add(context.Background(), rand.Int())
         assert.NoError(b, err)
      }
   })
}

总结
本文罗列了实战中可能会用到的各种小技巧,包括:

  • 提升准确度:-benchtime 和 -count 的使用
  • 内存分配情况:-benchmem 和 -memprofile 的使用
  • 测试不同的输入:b.Run运行子测试,验证时间复杂度
  • 跳过单元测试:可以通过 -run 指定一个不匹配任何单测的正则来跳过
  • 对比benchmark结果:使用 bench stat 工具,对比2次测试结果
  • 结合pprof进行函数耗时分析:-cpuprofile 指令的使用,并演示如何结合pprofile工具进行可视化分析。
  • ResetTimer & StartTimer & StopTimer 的使用

附录

benchmark常用命令如下(完整命令):

-cpu 1,2,4
    Specify a list of GOMAXPROCS values for which the tests, benchmarks or
    fuzz tests should be executed. The default is the current value
    of GOMAXPROCS. -cpu does not apply to fuzz tests matched by -fuzz.

-benchtime t
    Run enough iterations of each benchmark to take t, specified
    as a time.Duration (for example, -benchtime 1h30s).
    The default is 1 second (1s).
    The special syntax Nx means to run the benchmark N times
    (for example, -benchtime 100x).

-benchmem
    Print memory allocation statistics for benchmarks.

-count n
    Run each test, benchmark, and fuzz seed n times (default 1).
    If -cpu is set, run n times for each GOMAXPROCS value.
    Examples are always run once. -count does not apply to
    fuzz tests matched by -fuzz.

-cpuprofile cpu.out
    Write a CPU profile to the specified file before exiting.
    Writes test binary as -c would.

-memprofile mem.out
    Write an allocation profile to the file after all tests have passed.
    Writes test binary as -c would.

关于作者

作者简介:一线Gopher,公众号《Go和分布式IM》运营者,开源项目: CoffeeChat 、interview-golang 发起人 & 核心开发者,终身程序员。


参考:

  • benchmark 基准测试
  • https://pkg.go.dev/time#Duration
  • Benchmarking in Golang: Improving function performance
  • benchstat

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/532078.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CBA 总决赛激战正酣,线上观赛体验如何保证?

近日&#xff0c;许多球迷都在关注着 CBA 总决赛的激烈赛况。浙江队在征战 CBA 28 年后首次闯入总决赛&#xff0c;并将决赛主场放在了即将承办杭州亚运会正式比赛的奥体中心场馆&#xff0c;使得这轮系列赛成为了浙江篮球史上浓墨重彩的一笔。也难怪球迷现场观赛热情高涨&…

孙鑫VC++第四章 2.简单绘图-绘制

目录 1. 按键消息 2. 绘制线条 2.1 利用SDK全局函数实现画线功能 2.2 利用MFC的CDC类实现画线功能 2.3 利用MFC的CClientDC类实现画线功能 2.4 利用MFC的CWindowDC类实现画线功能 3. 在桌面窗口中画线 3.1 绘制彩色线条 4. 使用画刷绘图 4.1 简单画刷 4.2 位图画刷 …

vue+elementui+nodejs医院疫情防控管理系统3935y

语言 node.js 框架&#xff1a;Express 前端:Vue.js 数据库&#xff1a;mysql 数据库工具&#xff1a;Navicat 开发软件&#xff1a;VScode 互联网的飞速发展&#xff0c;使人们的生活离不开网络&#xff0c;医院的发展也同样是。现代医院中&#xff0c;不论大小都有各自的管…

想要快速开发东南亚市场?附见效快的方法

外贸人开发东南亚市场的超全指南&#xff0c;觉得有用的话&#xff0c;转发给身边的朋友哦&#xff5e; 一、8大开发东南亚客户的渠道 01 外贸客户开发系统 这个是目前主流的开发客户的渠道&#xff0c;也是效果最好的。可以根据产品关键字、HS编码、SCI编码通过在线数据库…

使用eclipse运行DBeaver源码

一、安装eclipse(rcp) 官网地址&#xff1a;https://www.eclipse.org/ 1、进入官网后点击downloads 2、点击Download Packages 3、选择版本&#xff1a;Eclipse IDE for RCP and RAP Developers 4、选择合适的镜像进行下载 5、找到下载的文件进行解压 6、找到解压后的文件…

Netty实战(一)

Nett的概念及体系结构 第一章 Java网络编程1.1 Java NIO1.2 选择器 第二章 Netty是什么2.1 Netty简介2.2 Netty的特性2.2.1 设计2.2.2 易于使用2.2.3 性能2.2.4 健壮性2.2.5 安全性2.2.6 社区驱动 2.3 Netty的使用者2.4 异步和事件驱动2.4.1 异步2.4.2 异步和伸缩性 第三章 Net…

企业缺少成本票的三个解决方式!

业务是流程&#xff0c;财税是结果&#xff0c;税收问题千千万&#xff0c;关注《税算盘》来帮你找答案。 成本票指的是计入要素费用的票据&#xff0c;主要是指直接或是间接计入生产成本或制造费用等多方面的发票和票据。简单来说成本票&#xff0c;就是企业销售商品后提供服…

MyBatis Plus 插件

一、分页插件 多个插件的情况下分页插件需要放到最后执行 1.1、增加分页插件配置 package com.xx.config;import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplu…

IT项目管理工具推荐:选择最适合你的优秀工具

国际数据公司的一份报告指出&#xff0c;25%的IT项目彻底失败&#xff0c;这还不包括需要返工的50%的项目&#xff0c;也不包括20-25%未能提供投资回报的项目。众所周知&#xff0c;管理IT项目有很多挑战。以下是最常见的挑战&#xff1a; 1.学习曲线大 挑战&#xff1a; 由于I…

c#与NPOI

简介 操作指南 某些API参考 视频讲解 完整代码 1、简介 NPOI是源于一个用于读取xls,doc,ppt文档的POI 项目&#xff0c;POI是Java项目&#xff0c;后面因为有.Net的市场&#xff0c;于是将POI移植到.Net上。Npoi 可以在没有安装Office 的情况写 对 Word 或 Excel 文档进行读写…

《如何向ChatGPT提问并获得高质量的答案》—Prompt技术的完整指南

本教程收集于:AIGC从入门到精通教程 目录 第1章:提示工程技术简介 第2章:说明提示技术(Instructions Prompt Technique)

OpenPCDet系列 | 5.3 PointPillars算法——BaseBEVBackbone伪图像特征提取模块

文章目录 BaseBEVBackbone模块1. BaseBEVBackbone初始化2. BaseBEVBackbone前向传播 OpenPCDet的整个结构图&#xff1a; BaseBEVBackbone模块 在进行了bev视图的特征转换后&#xff0c;随后进行backbone2d模块进行进一步的特征处理&#xff0c;在PointPillars中选择的是Bas…

AI绘画小程序开发流程

最近市场上出现了很多AI绘画的小程序和APP&#xff0c;这些小程序使用起来非常方便&#xff0c;用户体验也不错&#xff0c;可以解决一部分用户的实际需求。今天和大家分享如何开发一个AI类的绘画小程序以及要注意的问题&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公…

电脑蓝屏怎么U盘重装系统详细图文教学

电脑蓝屏怎么U盘重装系统详细图文教学。很多用户遇到电脑蓝屏的问题之后&#xff0c;不懂怎么去进行问题的解决。蓝屏问题其实自己制作一起启动盘就可以轻松的解决问题了。一起来看看以下的详细处理方法分享吧。 准备工作&#xff1a; 1、U盘一个&#xff08;尽量使用8G以上的U…

如何获取和制作免费的icon图标素材

icon 图标在界面设计中虽然占比不大&#xff0c;但却是不可缺少的设计元素之一。设计师通过 icon 图标&#xff0c;将抽象的概念通俗化&#xff0c;降低用户理解某个操作的难度。而设计师也会通过改变 icon 图标的样式来展现整体界面的视觉效果。icon 图标的风格有很多&#xf…

Linux gdb调试

使用gdb进行调试 我们将使用GNU的调试器gdb调试这个程序。gdb是一个功能很强大的调试器&#xff0c;它是一个自由软件&#xff0c;能够用在许多UNIX平台上。它同时也是Linux系统中的默认调试器。gdb已被移植到许多其他的计算机平台上&#xff0c;并且能够用于调试嵌入式实时系统…

(转载)从0开始学matlab(第6天)—数据文件与运算

1数据文件 有许多的方法用于加载和保存 MATLAB 的数据文件&#xff0c;这些方法后面进行介绍。在这里我们只向大家介绍最简单的 save 和 load 命令。 save 命令用于保存当前 MATLAB 工作区内的数据到一个硬盘文件。这个命令的基本形式如下 save filename var1 var2 var3 …

《拥抱AGI时代,电信日前省级电信公司的一次“非电信”培训》

数字信息基础设施是数字中国的关键底座&#xff0c;打通数字基础设施大动脉&#xff0c;建设数字中国成为推进中国式现代化的重要引擎。作为算力时代的探险家&#xff0c;出海浪潮的逐浪人&#xff0c;中国电信正全面融入数字经济建设大局&#xff0c;展现出新发展新格局下央企…

飞凌嵌入式技术帖——i.MX9352的GPIO怎么用?

之前小编为大家介绍过在飞凌嵌入式i.MX6ULL开发板上操作GPIO的方法。本期&#xff0c;让我们一起走近i.MX9352处理器&#xff0c;深入了解这位i.MX系列新成员的GPIO该如何操作&#xff0c;以及它与前辈i.MX6ULL处理器又有哪些异同。 01 硬件原理分析 以点灯和按键为例&#xf…

也谈零基础学编程,需要学什么

也谈零基础学编程&#xff0c;需要学什么 零基础人群语法1、注释2、变量2.1 变量命名2.2 数据类型2.3 类型强度 3、 运算符4、流程控制与代码块5、函数和方法6、类 算法学会使用第三方库小结 零基础人群 现在开发语言各种各样&#xff0c;越来越多&#xff0c;尤其是 python &…