这是我参与「第五届青训营」伴学笔记创作活动的第 5 天
入门 Go 语言-高质量编程与性能调优(二)
性能调优实战
性能调优简介
简介
- 性能优化的前提是满足正确可靠、简洁清晰等质量因素
- 性能优化是综合评估,有时候时间效率和空间效率可能对文
- 针对Go语言特性,介绍Go相关的性能优化建议
性能优化建议-Benchmark
如何使用
- 性能表现需要实际数据衡量
- Go语言提供了支持基准性能测试的benchmark工具
go test -bench. -benchmemfib.go
func Fib(n int) int {
if n < 2 {
fmt.Print(n)
return n
}
return Fib(n-1) + Fib(n-2)
}
fib_test.go
func BenchmarkFib10(b *testing.B) {
for i := 0; i < b.N; i++ {
Fib(10)
}
}
slice预分配内存
- 尽可能在使用
make()初始化切片时提供容量信息
func NoPreAlloc(size int) {
data := make([]int, 0)
for i := 0; i < size; i++ {
data = append(data, i)
}
}
func PreAlloc(size int) {
data := make([]int, 0, size)
for i := 0; i < size; i++ {
data = append(data, i)
}
}
BenchmarkNoPreAlloc-12 5632498 205.5 ns/opBenchmarkPreAlloc-12 25896112 46.65 ns/op- 切片本质是一个数组片段的描述
- 包括数组指针
- 片段的长度
- 片段的容量(不改变内存分配情况下的最大长度)。切片操作并不复制切片指向的元素
- 创建一个新的切片会复用原来切片的底层数组
另一个陷阱:大内存未释放
- 在已有切片基础上创建切片,不会创建新的底层数组
- 场景
- 原切片较大,代码在原切片基础上新建小切片
- 原底层数组在内存中有引用,得不到释放
- 可使用
copy替代re-slice
func GetLastBySlice(origin []int) []int {
return origin[len(origin)-2:]
}
func GetLastByCopy(origin []int) []int {
result := make([]int, 2)
copy(result, origin[len(origin)-2:])
return result
}
map预分配内存
- 不断向map中添加元素的操作会触发map的扩容
- 提前分配好空间可以减少内存拷贝和 Rehash的消耗
- 建议根据实际需求提前预估好需要的空间
func NoPreAlloc(size int) {
data := make(map[int]int)
for i := 0; i < size; i++ {
data[i] = 1
}
}
func PreAlloc(size int) {
data := make(map[int]int, size)
for i := 0; i < size; i++ {
data[i] = 1
}
}
test
func BenchmarkNoPreAlloc(b *testing.B) {
for i := 0; i < b.N; i++ {
NoPreAlloc(30)
}
}
func BenchmarkPreAlloc(b *testing.B) {
for i := 0; i < b.N; i++ {
PreAlloc(30)
}
}
BenchmarkNoPreAlloc-12 527054 2160 ns/op 2218 B/op 6 allocs/opBenchmarkPreAlloc-12 1280247 896.8 ns/op 1166 B/op 1 allocs/op
性能优化建议-字符串处理
- 常见的字符串拼接方式
func Plus(n int, srt string) string { s := "" for i := 0; i < n; i++ { s += srt } return s } func StrBuilder(n int, srt string) string { var builder strings.Builder for i := 0; i < n; i++ { builder.WriteString(srt) } return builder.String() } func ByteBuffer(n int, srt string) string { buf:=new(bytes.Buffer) for i := 0; i < n; i++ { buf.WriteString(srt) } return buf.String() } BenchmarkPlus-12 160256 8157 ns/op 15992 B/op 99 allocs/opBenchmarkStrBuilder-12 1951412 591.1 ns/op 1016 B/op 7 allocs/opBenchmarkByteBuffer-12 1000000 1010 ns/op 1280 B/op 5 allocs/op
使用strings.Builder
- 使用+拼接性能最差,strings.Builder,bytes.Buffer相近,strings.Buffer 更快
- 分析
- 字符串在Go语言中是不可变类型,占用内存大小是固定的
- 使用+每次都会重新分配内存
- strings.Builder,bytes.Buffer底层都是[]byte 数组
- 内存扩容策略,不需要每次拼接重新分配内存
- bytes.Buffer转化为字符串时重新申请了一块空间
- strings.Builder直接将底层的[]byte转换成了字符串类型返回
func PreStrBuilder(n int, srt string) string {
var builder strings.Builder
builder.Grow(n * len(srt))
for i := 0; i < n; i++ {
builder.WriteString(srt)
}
return builder.String()
}
func PreByteBuffer(n int, srt string) string {
buf := new(bytes.Buffer)
buf.Grow(n * len(srt))
for i := 0; i < n; i++ {
buf.WriteString(srt)
}
return buf.String()
}
BenchmarkPlus-12 2275 487942 ns/op 1602943 B/op 999 allocs/opBenchmarkStrBuilder-12 259966 4420 ns/op 8440 B/op 11 allocs/opBenchmarkByteBuffer-12 127496 8870 ns/op 11200 B/op 8 allocs/opBenchmarkPreStrBuilder-12 296757 4454 ns/op 3072 B/op 1 allocs/opBenchmarkPreByteBuffer-12 183508 6612 ns/op 6144 B/op 2 allocs/op
性能优化建议-空结构体
使用空结构体节省内存
- 空结构体struct 实例不占据任何的内存空间
- 可作为各种场景下的占位符使用
- 节省资源
- 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
func EmptyStructMap(n int) {
m := make(map[int]struct{})
for i := 0; i < n; i++ {
m[i] = struct{}{}
}
}
func BoolMap(n int) {
m := make(map[int]bool)
for i := 0; i < n; i++ {
m[i] = false
}
}
BenchmarkEmptyStructMap-12 35041 34217 ns/op 23887 B/op 37 allocs/opBenchmarkBoolMap-12 33301 35685 ns/op 26643 B/op 48 allocs/op- 实现Set,可以考虑用map来代替
- 对于这个场景,只需要用到map的键,而不需要值
- 即使是将map 的值设置为bool类型,也会多占据1个字节空间
性能优化建议-atomic包
如何使用atomic包
- 锁的实现是通过操作系统来实现,属于系统调用
- atomic操作是通过硬件实现,效率比锁高
- sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值操作,可以使用atomic.Value,能承载一个interface0
type atomicCounter struct {
i int32
}
func AtomicAddOne(c *atomicCounter) {
atomic.AddInt32(&c.i,1)
}
type mutexCounter struct {
i int32
m sync.Mutex
}
func MutexAddOne(c *mutexCounter) {
c.m.Lock()
c.i++
c.m.Unlock()
}
BenchmarkAtomicAddOne-12 75900723 14.74 ns/op 4 B/op 1 allocs/opBenchmarkMutexAddOne-12 36162004 29.30 ns/op 16 B/op 1 allocs/op
性能分析工具 pprof 实战
性能调优原则
- 要依靠数据不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
说明
- 希望知道应用在什么地方耗费了多少 CPU、Memory
- pprof是用于可视化和分析性能分析数据的工具
- pprof 功能简介
- pprof 排查实战
搭建pprof 实践项目
- GitHub(来自Wolfogre)
- 项目提前埋入了一些炸弹代码,产生可观测的性能问题
- pprof 的采样过程和原理
浏览器查看指标
CPU
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
Saved profile in C:\Users\yayuke\pprof\pprof.samples.cpu.001.pb.gz
Type: cpu
Time: Jan 17, 2023 at 10:18pm (CST)
Duration: 10.18s, Total samples = 2.75s (27.01%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
命令:topN-查看占用资源最多的函数
flat当前函数本身的执行耗时flat%flat占CPU总时间的比例sum%上面每一行的flat%总和cum指当前函数本身加上其调用函数的总耗时cum%cum占CPU总时间的比例- Flat == Cum,函数中没有调用其他函数
- Flat == 0,函数中只有其他函数的调用
命令:list-根据指定的正则表达式查找代码行命令:web-调用关系可视化
Heap-堆内存
go tool pprof -http=:8080 "http://127.0.0.1:6060/debug/pprof/heap"
- alloc_objects:程序累计申请的对象数
- inuse_objec:程序当前持有的对象数
- alloc_space:程序累计申请的内存大小
- inuse_space:程序当前占用的内存大小
goroutine-协程
- goroutine泄露也会导致内存泄露
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
- 由上到下表示调用顺序
- 每一块代表一个函数,越长代表占用CPU的时间更长
- 火焰图是动态的,支持点击块进行分析
mutex-锁
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"block-阻塞go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"