这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天
本堂课重点内容
- 性能优化建议
- 性能优化工具pprof
1 性能优化建议
1.1 Benchmark
Go语言下支持基准性能测试的工具
e.g.
命令行指令:go test -bench=. -benchmem
参数解释
- bench=. 表示对当前目录下的所有基准测试代码进行测试
- 注意:windows下需要对点加双引号,即-bench="."
- -benchmem 表示统计内存信息
执行结果:
说明:
- 第一列是基准测试函数名
- -16表示线程数
- 第二列是函数执行的次数,即b.N的值
- 第三列是每次执行花费的时间
- 第四列是每次操作申请的内存大小
- 第五列是每次执行申请内存的次数
1.2 Slice
优化建议:
- 尽可能在使用 make() 初始化切片时提供容量信息
- s := make(tp, length, size)
- 原理:当追加的内容大小与原切片大小之和超过容量时,Slice会将原数据拷贝到新的更大内存空间,因此提供容量信息能减少内存拷贝的次数,从而有效降低执行时间
- 陷阱:大内存未释放
- 场景:在已有切片基础使用
[]上创建新的切片 - 原理:虽然新的切片仅使用了原切片的部分内容,但是由于浅拷贝,原来切片底层的数组仍然被占用,无法释放,因此原切片剩余部分的内容仍然处于占用状态。
- 解决方法:使用copy代替
[],copy会对原切片的部分内容进行深拷贝,故原内存不会处于占用状态,得以释放。
- 场景:在已有切片基础使用
1.3 Map
优化建议:
- 预分配内存:原理同slice优化建议第一条
1.4 字符串处理
优化建议:
- 使用strings.Builder.WriteString代替
+进行拼接- strings.Builder底层是
[]byte数组。WriteString方法往同一片内存空间写入字符 - 原理:字符串在Go语言中是不可变类型,占用内存大小固定,每次使用
+会重新分配内存。strings.Builder,bytes.Buffer 底层都是[]byte数组。根据内存扩容策略,不需要每次拼接重新分配内存 - 性能比较:使用 + 拼接性能最差strings.Builder, bytes.Buffer 相近,strings.Buffer 更快
- bytes.Buffer 转化为字符串时重新申请了一块空间
- strings.Builder 直接将底层的
[]byte转换成了字符串类型返回
- 由于底层为数组,因此根据上面的优化建议,同样可以为该数组进行预分配从而达到减少内存拷贝次数
builder.Grow(length); buf.Grow(length)
- strings.Builder底层是
1.5 空结构体
优化建议:
- 使用空结构体节省内存
- 空结构体 struct{} 实例不占据任何的内存空间可作为各种场景下的占位符使用节省资源。空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
- 考虑使用map代替set,只需使用到map的键,值可以使用空结构体代替
1.6 atomic包
介绍
- 锁的实现是通过操作系统来实现,属于系统调用
- atomic 操作是通过硬件实现,效率比锁高
- sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值操作,可以使用 atomic.Value,能承载一个 interface{}
2 性能调优实战
2.1 原则
- 要依靠数据不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
2.2 性能分析工具pprof
2.2.1 功能简介
pprof由4个功能组成:分析Profile, 工具Tool, 展示View, 采样Sample
- 分析Profile
- 网页
- 可视化终端
- 工具Tool
- runtime/pprof
- net/http/pprof
- 展示View
- Top
- 调用图Graph
- 火焰图FlameGraph
- Peek
- 源码Source
- 反汇编Disassemble
- 采样Sample
- CPU
- 堆内存Heap
- 协程Goroutine
- 锁Mutex
- 阻塞Block
- 线程创建ThreadCreate
2.2.2 pprof-praticle
项目埋入了一些炸弹代码,可观测性能问题,用于学习pprof的功能
2.2.2.1 运行项目
运行项目后,打开http://localhost:16060/debug/pprof。出现以下页面
注:原项目端口为6060,由于我的6060端口占用了,故该为16060
展示了程序运行的采样数据,分别是
- allocs:内存分配情况
- blocks:阻寒操作情况
- cmdline:程序启动命令
- goroutine:当前所有goroutine的堆栈信息
- heap:堆上内存使用情况(同alloc)
- mutex:锁竞争操作情况
- profile: CPU占用情况
- threadcreate:当前所有创建的系统线程的堆栈信息
- trace:程序运行跟踪信息
该程序在CPU,堆内存,goroutine,锁竞争和阻塞上埋下了炸弹,下面逐条分析
2.2.2.2 CPU
-
打开一个新的终端输入
go tool pprof http://localhost:16060/debug/pprof/profile?seconds=10
使用pprof工具对程序运行时占用CPU情况采样10秒 -
进入pprof工具终端,输入
top或者top N可以获取采样时间内程序各部分占用CPU情况
该图显示tiger.Eat占用了最多的CPU时间
字段说明:
- Flat: 当前函数的占用
- Flat%; Fat占总量的比例
- Sum%: 上面所有行的Flat%总和
- Cum (Cumulative) :当前函数加上其调用函数的总占用时间
- Cum%: Cum占总量的比例
注意:当函数没有调用其他函数时Flat=Cum,当函数只调用其他函数时Flat=0
- 输入
list Eat可以查看该函数的源码以及每条语句所占用的时间
如图所示,for循环占用了2.46s
- 输入
web可以打开可视化界面,展示函数调用链
如图所示 tiger.Eat箭头最粗,时间最长
- 将该段代码进行优化,这里先将该循环进行注释
可以看到目前问题已经“解决”
2.2.2.3 heap堆内存
- 输入
go tool pprof -http=:8080 http://localhost:16060/debug/pprof/heap
-http=:port会在端口打开一个可视化的web界面提供操作
可以看到Mouse.Steal“偷”走了最多的堆空间
- top视图和source视图
- 将上述代码进行注释后,可以看到堆内存问题已解决(了吗?)
注意:在内存采样中,默认展示的是inuse_space视图,只展示当前持有的内存,如果有的内存已经释放,这时inuse采样就不会展示了。我们切换到alloc_space指标,后分析下分配的内存问题
- 解决alloc_space问题
可以看到该代码申请了16MB的内存空间,但是没有使用,说明是无意义的
- 注释后,此时堆内存问题才真正解决了
2.2.2.4 协程goroutine
- 输入
go tool pprof -http=:8080 http://localhost:16060/debug/pprof/goroutine - 火焰图
说明
- 图中,自顶向下展示了各个调用,表示各个函数调用之间的层级关系
- 每一行中,条形越长代表消耗的资源占比越多
- “又平又长”的节点是占用资源多的节点
如图所示,wolf.Drink开启了90%+的goroutine
-
在source中搜索wolf,可以定位
这里程序开启了10条无意义的goroutine,导致内存泄露,生产下如果出现这种情况,进程占用了内存会越来越大,最终被os kill掉
-
注释掉代码后
2.2.2.5 锁mutex
- 输入
go tool pprof -http=:8080 http://localhost:16060/debug/pprof/mutex
这里等待了1s后才解锁,造成了阻塞
2.2.2.6 阻塞block
-
输入
go tool pprof -http=:8080 http://localhost:16060/debug/pprof/block -
使用命令行,发现有4个node被丢掉了,原因是累计时间太短