Loading... # 0x01 最近曹大推荐了一篇高质量的 Go 性能分析的文章 [High Performance Go Workshop](https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html#discussion) 本来想写一篇翻译文的,但是发现已经有一篇质量还算比较高的翻译文 [(翻译)Go 高性能研讨讲座](https://blog.zeromake.com/pages/high-performance-go-workshop/#benchmarking),这里就不赘述。就节选一点个人认为重要的东西加上一些个人平时的总结记录下来~ # Benchmark ## 基本的测试/Benchmark代码框架 ```go // 文件名 xxx_test.go 且需要 main module 即 go mod init import "testing" // 函数名需为 TestXXXX func TestName(t *testing.T) { // 逻辑 } // 函数名需为 BenchmarkXXXX func BenchmarkName(b *testing.B) { for i := 0; i < b.N; i++ { // 逻辑 } } ``` ## go test 命令参数 执行 `go test` 默认不会执行 benchmark 仅会执行 `TestXXX` 命名规则的**所有**函数,可以通过设置 `-run=xxx` 指定需要测试样例,支持**正则表达式**。比如 `go test -run=^TestHello$` ### -bench 使用 `go test -bench=.` 仅会执行 `BenchmarkXXX` 命名规则的函数,不会进行标准测试 `Testxxxx` 。并且`-bench` 同样支持正则表达式。 `go test -bench=.` 默认会使用等同于 `GOMAXPROCS` 的 System-Thread 数来执行 benchmark,可以通过例如 `-cpu=1,2,4`来指定使用的 System-Thread 数执行 benchmark。结果会同时打印: ```bash $ go test -bench=BenchmarkHello -cpu=1,2,4 BenchmarkHello 1000000000 0.242 ns/op BenchmarkHello-2 1000000000 0.242 ns/op BenchmarkHello-4 1000000000 0.240 ns/op ``` ### b.N 的次数 `b.N` 值是不固定的,如果一次 benchmark 能在 1s 内执行完毕,则 `b.N` 会从 1 开始,以近似 1, 2, 3, 5, 10, 20, 30, 50, 100 的顺序增长。`b.N` 的值是由测试框架智能维护的,如果较小的 `b.N` 值相对较快地完成,它将更快地增加迭代次数,最终结果为总循环次数的平均时间。 ### 手动开启/关闭基准测试计时器 某些 benchmark 场景需要初始化或者处理一些与测试本身无关的逻辑,这部分不需要参与 benchmark。就可以使用 `b.ResetTimer()`、`b.StopTimer()` 和 `b.StartTimer()` 处理。 - `b.ResetTimer()` 忽略该条语句之前的执行时间 - `b.StopTimer()` 暂停 benchmark 计时器 - `b.startTimer()` 开启 stop 状态的 benchmark 计时器 ```go func BenchmarkExpensive(b *testing.B) { // 不需要计时的逻辑 boringAndExpensiveSetup() b.ResetTimer() // (1) for n := 0; n < b.N; n++ { // function under test } } ``` ```go func BenchmarkComplicated(b *testing.B) { for n := 0; n < b.N; n++ { // 因为循环会有多次,使用 Reset 会丢失之前的计时进度 b.StopTimer() // (1) // 不需要计时的逻辑 complicatedSetup() b.StartTimer() // (2) // function under test } } ``` ### 生成 pprof 分析文件 * `-cpuprofile=$FILE` 将 `CPU` 分析写入 `$FILE`. * `-memprofile=$FILE`, 将内存分析写入 `$FILE`, `-memprofilerate=N` 将配置文件速率调整为 `1/N`. * `-blockprofile=$FILE`, 将块分析写入 `$FILE`. ```bash $ go test -bench=BenchmarkHello -cpuprofile=c.p ``` # pprof > 本节的 How Why What 部分基本摘抄煎鱼大佬的 [9.1 Go 大杀器之性能剖析 PProf](https://eddycjy.gitbook.io/golang/di-9-ke-gong-ju/go-tool-pprof) ,用词确实没有办法做到比大佬更精练,只能出此下策了 pprof 是一个 Go tool 自带的性能优化工具,主要分为以下两种: * runtime/pprof:采集程序(非 Server)的运行数据进行分析 * net/http/pprof:采集 HTTP Server 的运行时数据进行分析 ## 支持什么使用模式 * Report generation:报告生成 * Interactive terminal use:交互式终端使用 * Web interface:Web 界面 ## 可以做什么 * CPU Profiling:CPU 分析,按照一定的频率采集所监听的应用程序 CPU(含寄存器)的使用情况,可确定应用程序在主动消耗 CPU 周期时花费时间的位置 * Memory Profiling:内存分析,在应用程序进行堆分配时记录堆栈跟踪,用于监视当前和历史内存使用情况,以及检查内存泄漏 * Block Profiling:阻塞分析,记录 goroutine 阻塞等待同步(包括定时器通道)的位置 * Mutex Profiling:互斥锁分析,报告互斥锁的竞争情况 ## pprof 使用 除了一下两种生成 pprof 的模式,还可以用 benchmark 的形式生成,详见 benchmark 一节的最后一小节。 ### runtime/pprof ```go package main import ( "flag" "fmt" "log" "os" "runtime/pprof" ) var ( cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file.") ) func main() { log.Println("begin") flag.Parse() if *cpuprofile != "" { f, err := os.Create(*cpuprofile) if err != nil { log.Fatal(err) } pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() } for i := 0; i < 30; i++ { nums := fibonacci(i) fmt.Println(nums) } } func fibonacci(num int) int { if num < 2 { return 1 } return fibonacci(num-1) + fibonacci(num-2) } ``` ```bash // 生成 proflie 文件 $ go run fib.go --cpuprofile=c.p ``` ### net/http/pprof ```go package main import ( "fmt" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "net/http" // 必须导入这个包 _ "net/http/pprof" ) func hello(w http.ResponseWriter, r *http.Request) { _, err := w.Write([]byte("Hello world!")) if err != nil { return } } func main() { r := chi.NewRouter() r.Use(middleware.Logger) r.Get("/", hello) go func() { // 正常的 web 程序,使用 goroutine 启动 err := http.ListenAndServe(":8000", r) if err != nil { fmt.Println(err.Error()) return } }() // 只有在传入的 handler 为 nil 时才会开启 pprof http.ListenAndServe(":8001", nil) } ``` 然后我们就可以在 `http:127.0.0.1:8001/debug/pprof` 打开分析页面了,如果需要用命令行模式,可以使用 - cpu 模式`go tool pprof http://127.0.0.1:8001/debug/pprof/profile?seconds=60` - mem 模式 `go tool pprof http://127.0.0.1:8001/debug/pprof/heap` - block 模式 `go tool pprof http://127.0.0.1:8001/debug/pprof/mutex`  ### 可视化图形界面 Web 模式可以点击页面中的 `profile` 链接下载或者使用 `wegt http://127.0.0.1:8001/debug/pprof/profile?seconds=60 -O filename` 。 然后使用 `go tool pprof -http=:8002 filename` 可视化的分析。 > 使用前请确保已经安装了 graphviz > > * Linux: `[sudo] apt-get install graphviz` > * OSX: > * MacPorts: `sudo port install graphviz` > * Homebrew: `brew install graphviz` > * [Windows](https://graphviz.gitlab.io/download/#Windows) 具体怎么使用这个可视化界面,建议看 煎鱼大佬的 pprof 使用教程的 [这一节](https://eddycjy.gitbook.io/golang/di-9-ke-gong-ju/go-tool-pprof#san-pprof-ke-shi-hua-jie-mian) # 逃逸分析 我们都知道函数的形参如果是指针,那么它就会逃逸到堆上。使用 `go build -gcflags` 可以进行逃逸分析。 ```go package main import "fmt" type circle struct { r float64 } func area(c *circle) float64 { return c.r * c.r * 3.14 } func main() { c := &circle{2} fmt.Println(area(c)) } ``` ```bash $ go build -gcflags=-m main.go # command-line-arguments .\main.go:14:6: can inline area .\main.go:21:21: inlining call to area .\main.go:21:16: inlining call to fmt.Println .\main.go:14:11: c does not escape .\main.go:20:10: &circle literal does not escape .\main.go:21:21: area(c) escapes to heap .\main.go:21:16: []interface {} literal does not escape <autogenerated>:1: .this does not escape <autogenerated>:1: .this does not escape ``` 我们就能很明显的看出 `area(c)` 逃逸到了堆上。 如果输出的信息不够清晰,可以多加几个 `-m` 打印更多的信息。 ```bash $ go build -gcflags="-m -m" main.go # command-line-arguments .\main.go:14:6: can inline area as: func(*circle) float64 { return c.r * c.r * 3.14 } .\main.go:18:6: cannot inline main: function too complex: cost 94 exceeds budget 80 .\main.go:21:21: inlining call to area func(*circle) float64 { return c.r * c.r * 3.14 } .\main.go:21:16: inlining call to fmt.Println func(...interface {}) (int, error) { var fmt..autotmp_3 int; fmt..autotmp_3 = <N>; var fmt..autotmp_4 error; fmt..autotmp_4 = <N>; fmt..autotmp_3, fmt..autotmp_4 = fmt.Fprintln(io.Writer(os.Stdout), fmt.a...); return fmt..autotmp_3, fmt..autotmp_4 } .\main.go:14:11: c does not escape .\main.go:21:21: area(c) escapes to heap: .\main.go:21:21: flow: ~arg0 = &{storage for area(c)}: .\main.go:21:21: from area(c) (spill) at .\main.go:21:21 .\main.go:21:21: from ~arg0 = <N> (assign-pair) at .\main.go:21:16 .\main.go:21:21: flow: {storage for []interface {} literal} = ~arg0: .\main.go:21:21: from []interface {} literal (slice-literal-element) at .\main.go:21:16 .\main.go:21:21: flow: fmt.a = &{storage for []interface {} literal}: .\main.go:21:21: from []interface {} literal (spill) at .\main.go:21:16 .\main.go:21:21: from fmt.a = []interface {} literal (assign) at .\main.go:21:16 .\main.go:21:21: flow: {heap} = *fmt.a: .\main.go:21:21: from fmt.Fprintln(io.Writer(os.Stdout), fmt.a...) (call parameter) at .\main.go:21:16 .\main.go:20:10: &circle literal does not escape .\main.go:21:21: area(c) escapes to heap .\main.go:21:16: []interface {} literal does not escape <autogenerated>:1: .this does not escape <autogenerated>:1: .this does not escape ``` # 内联优化 如果不清楚什么是内联优化请看我的这篇文章: <div class="preview"> <div class="post-inser post box-shadow-wrap-normal"> <a href="http://gvoidy.cn/index.php/archives/306/" target="_blank" class="post_inser_a no-external-link no-underline-link"> <div class="inner-image bg" style="background-image: url(https://gvoidy.cn/usr/uploads/2021/07/1305760700.gif);background-size: cover;"></div> <div class="inner-content" > <p class="inser-title">通过游戏解释内联优化</p> <div class="inster-summary text-muted"> 什么是内联优化内联是一个基本的编译器优化,他会将一些简单的函数展开放入程序主题中,减少调用函数本身的开销。内联优化... </div> </div> </a> <!-- .inner-content #####--> </div> <!-- .post-inser ####--> </div> 程序优化往往只有两种方式,一种是用时间换空间,另一种是用空间换时间。内联优化就是用空间换时间的案例,不过他是用代码量来换 CPU 执行的时间,即内联优化级别越高,build 出来的二进制文件越大。 ## 内联优化级别 编译阶段 Go 编译器会在解析 AST 时计算函数走过的节点数,如果节点超过了阈值则不会进行内联优化。说人话就是函数如果越复杂越不容易进行内联优化。可以通过调整内联级别来调整这个阈值,build 的时候使用 `-gcflags = -l`调整内联级别。 * 函数前加上 `//go:noinline` 注释强制不启动内联 * 不使用 `-gcflags` 参数,不调节内联级别 * `-gcflags=-l`, 禁用内联 * `-gcflags='-l -l'` 内联级别 2,更具激进,可能更快,可能会生成更大的二进制文件 * `-gcflags='-l -l -l'` 内联 3 级,再次更加激进,二进制文件肯定更大,也许更快,**网上的文章都说可能会有 bug** * `-gcflags=-l=4 (4个-l)`在 Go 1.11 以后支持实验性的[中间栈内联优化](https://github.com/golang/go/issues/19348)。可能会有更多的 bug # 几种常见的优化场景 ## 内存泄露 内存泄露最常见的场景就是忘记关闭打开的文件资源等,最好的办法就是在打开文件的代码后立刻加一个 `defer` 关闭文件,这样就不容易忘记了。 > 解锁 mutex 的时候也是同理,但是一定要注意如果函数生命周期内不是一直需要这个锁,使用 defer 可能会带来性能损耗 ## 常驻大对象一直在被 GC 扫 Go 的 GC 有个很大的坑点就是他没有 **分!代!**,在每一次 GC 的时候他都会对堆内内存全局扫描。如果我们有一个常驻大对象,GC 可能会相当的耗性能。比如下图用一个 `sync.Map` 当本地缓存使用的情况:  > 正常情况下 GC 的 CPU 占用会被约束在 25%,超过 25% 的话,应用协程经常被征用去做 mark assist 这种情况,只能想办法减少 GC 频率或者降低参与 GC 的对象数量 ### 减少 GC 频率 这应该算是邪道方案了,GC 会在堆上分配的内存翻倍或是定时触发。所以,我们堆上分配对象程线性/指数增长时,可以通过预先分配一大块不需要用到的内存降低 GC 触发的频率。**这种方法只适合内存不紧张的时候。** ```go package main func main() { _ = make([]int, 100000) // DO SOMETHING } ``` ### 减少对象数量 最好的当然就是自己优化代码,将一堆小对象合并成大对象之类的。还可以视情况将 `map` 替换成 `slice` , `map` 结构会比 `slice` 复杂的多内部的对象也多很多。下面举几个特殊的优化例子。 #### 使用 `sync.Pool` `sync.Pool` 采用池化技术,可以保存和复用临时对象,减少内存分配,降低 GC 压力。 ```go package main import ( "fmt" "sync" ) var pool *sync.Pool type Person struct { Name string } func initPool() { pool = &sync.Pool{ New: func() interface{} { fmt.Println("Creating a new Person") return new(Person) }, } } func main() { initPool() p := pool.Get().(*Person) fmt.Println("首次从 pool 里获取:", p) p.Name = "first" fmt.Printf("设置 p.Name = %s\n", p.Name) pool.Put(p) fmt.Println("Pool 里已有一个对象:&{first},调用 Get: ", pool.Get().(*Person)) fmt.Println("Pool 没有对象了,调用 Get: ", pool.Get().(*Person)) } ``` #### 使用定长数组代替 slice 回到刚刚用`sync.Map` 当本地缓存的情况,我们的 `map` key 的类型是 `[]bytes`,但存的值是定长的。我们就可以把 `[]bytes` 优化成 `[12]bytes` ,将其从指针类型转化为值类型,降低堆上分配的对象数。  ### GC 优化总结 - 减少堆上对象分配 - sync.Pool 进行堆对象重用 - Map -> slice - 指针 -> 非指针对象 - 多个小对象 -> 合并为一个大对象 - 降低 GC 频率 - 修改 GOGC - Make 全局大 slice # 相关文章推荐 [跟煎鱼学Go - 9.1 Go 大杀器之性能剖析 PProf](https://eddycjy.gitbook.io/golang/di-9-ke-gong-ju/go-tool-pprof) [(翻译)Go 高性能研讨讲座](https://blog.zeromake.com/pages/high-performance-go-workshop/#benchmarking) [High Performance Go Workshop](https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html#discussion) [如何读懂火焰图?](https://www.ruanyifeng.com/blog/2017/09/flame-graph.html) [golang pprof 实战](https://blog.wolfogre.com/posts/go-ppof-practice/) 最后修改:2021 年 08 月 16 日 10 : 21 PM © 允许规范转载 赞赏 如果觉得我的文章对你有用,请随意赞赏 赞赏作者 支付宝微信
1 条评论
牛逼!