Golang 技术分享|我和 Dave Cheney 有个约会

高性能 Go 编程:CPU 与内存性能分析工具的实用案例

文章目录

Start

今天上午参加了 Go 语言项目开发成员 Dave Cheney 的分享。

签到

分享的主题是《High Performance Go: Two tools, three types of profiling in 45 minutes》

High Performance Go

分享的内容来自 https://dave.cheney.net/high-performance-go-workshop/gopherchina-2019.html 非常有干货的分享!

两个工具分别是 pprof 和 trace,三种类型的分析例子 cpu 性能分析,内存性能分析,trace 性能分析

Dave 首先感谢了 Tencent Tarscloud,估计是 Tars 项目的同事邀请他来的。

CPU 性能分析工具:pprof

通过一个统计文件内容中单词的个数的 go 程序和 linux 自带的 wc 的性能对比来展示 go 的性能分析工具 pprof 的用法。

原始程序代码如下:

Golang 统计文件内容中单词的个数

编译运行后统计《Moby Dick》白鲸记这篇小说的单词个数。(moby dick 直译是大雕???《白鲸记》为赫尔曼·梅尔维尔发表于 1851 年的小说,被认为是美国最伟大的长篇小说之一。是一部以海上捕鲸业为题材的小说,一位名叫亚哈的“裴廓德号”捕鲸船船长带领全体船员,追捕一条叫做“莫比·迪克”的大白鲸的历险过程。https://www.gutenberg.org/files/2701/2701-h/2701-h.htm

该程序大约 2 秒统计完成,而 wc 只要 0.012 秒,性能差距太大,经过分析优化后,性能接近 wc。

于是在代码中开始 CPU 性能分析,添加 profile 代码:

profile CPU 性能分析

go 默认的 pprof 有两个包: net/http/pprofruntime/pprof,前者是通过后者实现的。这里的 pkg/profile 是第三方包,也是通过 runtime/pprof 实现的,更加好用。

使用 go run 运行后会生成 cpu.pprof 文件,接下来就可以通过执行命令 go tool pprof /path/to/cpu.pprof 来展示分析结果,执行 top 命令:

pprof 分析结果

可以通过在上面的命令中添加 -http=:PORT 参数来启动 http 服务,就会自动打开浏览器在里面展示 dot 图,里面可以交互可视化的查看 top,火焰图,调用图,源码信息,很强大,需要先安装 Graphviz,注意添加 bin 目录到 Path 中才行。

我这里测试是 windows 本地创建了一个有 320 万个英文字符文件,字符数量太少效果不太明显,调用图如下(和*nix 上的输出有所不同):

pprof 分析结果

pprof 分析结果

上面的 graph 可以看到 cpu 90+% 是在做 syscall.Syscall,因为 readbyte 直接对文件对象进行读取操作,每次读一个单词,于是这个程序有多少个单词就会调用多少次 syscall,系统调用通常都是比较昂贵的操作,大量的 syscall 就占用了 cpu,导致程序性能下降。

优化:因为 readbyte 接收的是 io.Reader 的 interface,所以可以把文件对象 f 通过 bufio 缓存起来就不用每次去读取文件导致 syscall 了。bufio 实现了 io.Reader 和 io.Writer。

bufio

性能分析数据

Go 性能分析数据

Go 性能分析数据

本地测试文件优化以前有大量 syscall 大概要 62 秒,优化后没有产生 syscall,只需要 860 毫秒左右,主要耗时在 readbyte 里创建 buf 对象。

总结:通过 pprof 工具可以分析出 cpu 瓶颈所在。频繁的文件操作会是 syscall 大量占用 cpu,可以使用 bufio 避免 syscall 大幅优化性能。

内存性能分析

profile.Start 默认分析 cpu,其他指标需要显示指定参数,内存分析 rate 设置为 1 表示搜集所有的内存分配信息

Go 内存性能分析

Go 内存性能分析

可以看到程序内存主要分配在 readbyte 这里,这里 windows 上的测试结果和 dave 分享的结果不太一样。

性能分析示例:Mandelbrot 图像生成

项目地址: https://github.com/campoy/mandelbro

运行这个程序会生成一张图片,大约花了 1.6 秒。如何评估它的性能好坏?是否可能让它更快?

首先还是通过 pprof 分析 cpu 性能

pprof 分析 cpu 性能

pprof 分析 cpu 性能

可以看到 cpu 消耗在 seqFillg 中调用 fillPixel 函数上

pprof 分析 cpu 性能

在代码中加入 trace 分析

Go代码中加入trace分析

编译成可执行文件,然后执行,会生成 trace.out 文件,现在可以使用 trace 工具分析该文件 go tool trace trace.out 会自动打开浏览器,页面上有查看 trace,gorutine 分析,网络阻塞分析,同步阻塞分析,系统调用阻塞分析,调度耗时分析等

Go代码中加入trace分析

页面显示只利用了一个 cpu

使用 gorutine 来利用多个 cpu

oneToOne 方法

使用gorutine来利用多个cpu

每一次的 fillpixel 都开一个 gorutine,运行时间并没有缩短,在 trace 页面放大看,虽然使用了多个 cpu,到 cpu 并不是连续的,全部是断开的,因为 fillpixel 做的事太少,花费了大量的时间在 gorutine 的调度上。

onePerRow 方法

使用gorutine来利用多个cpu

将一行像素的填充作为一个 gorutine,运行耗时大幅减少,再看 trace 页面,cpu 也利用的不错,gorutine 数量也不多。

使用gorutine来利用多个cpu

最后还有一个 nworker 方法

使用gorutine来利用多个cpu

执行耗时会最慢,因为它使用的是无缓冲 channel,每一次都得等。修改为有缓冲的 channel 就会大幅提升性能。

trace 信息也可以通过 http 接口获取 curl -o trace.out https://127.0.0.1:8080/debug/pprof/trace?seconds=5

End

接下来是 QA 环节,同学们纷纷用流利的英语和 Dave 交流 :)

最后是 Tars 团队和 Dave 一起坐上讲台开始交流,我就先溜了。


也可以看看