这是我参加[第五届青训营]伴学笔记创作活动的第 4 天
零、 前言
本文整理了一些关于性能调优的知识点,撰写本文的目的一是为了参加伴学笔记创作活动,而是方便自己未来查缺补漏。
一、本堂课重点内容
-
性能调优
- 性能优化指南
- 性能优化分析工具
- 性能调优实战案例
二、详细知识点回顾
2.1 性能优化指南
简介:
1.性能优化的前提是代码满足正确可靠、简介清晰的基本要求
2.性能优化是综合评估,有时候时间效率和空间效率会对立
Benchmark
如下形式的函数
func BenchmarkXxx(*testing.B)
被认为是基准测试,需要通过go test命令执行,其基本形式为:
go test [build/test flags] [packages] [build/test flags & test binary flags]
基准函数测试样例:
// from fib.go
func Fib(n int) int {
if n <= 2 {
return n
}
return Fib(n-1) + Fib(n-2)
}
// from fib_test.go
```
func BenchmarkFib10(b *testing.B) {
for n := 0; n < b.N; n++ {
Fib(10)
}
}
```
执行go test命令,它会在它会在 *_test.go 中寻找 test 测试、benchmark 基准 和 examples 示例 函数,其中基准函数必须以Benchmarkxxx的形式命名。
命令go test -bench=. -benchmem
运行结果
Slice
Slice数据结构
type Slice struct {
array unsafe.Pointer
len int
cap int
}
尽可能使用make()初始化切片时提供容量信息,这样会为Slice预分配内存,在Slic容量足够的情况下,对其追加元素不会引起扩容,而扩容涉及内存操作,需要消耗一定时间
考虑以下场景
- 原切片较大,代码在原切片基础上创建了小切片
- 原底层数组在内存有引用,得不到释放
func f(origin []int) []int {
return origin[len(origin)-2 :]
}
在该实例函数中返回origin的切片,origin底层数组在源程序中有引用,导致origin底层数组不会被垃圾回收机制释放,占用大量内存(假设origin很大)
func f(origin []int) []int {
result := make([]int, 2)
copy(result, origin[len(origin)-2:])
return result
}
改用copy函数可以完美解决这个问题,这样origin会在该函数完成后被释放
Map
预分配内存,根据实际需求分配长度,可以对比Slice,两者大同小异
字符串处理
使用+拼接字符串 VS 使用strings.Builder VS 使用ByteBuffer
结果:使用+拼接字符串性能最差,使用strings.Builder和使用ByteBuffer相差不大,strings.Builder稍快一些,这是由于bytes.Buffer转化为字符串是重新申请了一片内存空间
在预知字符串长度的时候,可以使用Grown方法对字符串预分配内存,提高性能
struct空结构体
空结构体struct{}不占用任何内存,可作为占位符使用,最常用的方式是与Map一起实现set,如map[int]struct{}
atomic包
使用atomic包 VS 使用加锁
锁的实现依赖操作系统,调用成本偏高,锁的使用因该用来保护一段逻辑,而不是保护一个数据项。
2.2 性能优化工具pprof
pprof是用于可视化和分析性能分析数据的工具,可以用pprof检查你的Go程序在什么地方占用的Cpu或memory最多
性能调优实战案例
准备:首先克隆我们需要用到的项目代码,使用Git bashgit clone https://github.com/wolfogre/go-pprof-practice,保证项目可以编译运行
项目main包:
package main
import (
"log"
"net/http"
_ "net/http/pprof"
"os"
"runtime"
"time"
"github.com/wolfogre/go-pprof-practice/animal"
)
func main() {
log.SetFlags(log.Lshortfile | log.LstdFlags)
log.SetOutput(os.Stdout)
runtime.GOMAXPROCS(1)
runtime.SetMutexProfileFraction(1)
runtime.SetBlockProfileRate(1)
go func() {
if err := http.ListenAndServe(":6060", nil); err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
for {
for _, v := range animal.AllAnimals {
v.Live()
}
time.Sleep(time.Second)
}
}
项目运行时,使用浏览器打开http://localhost:6060/debug/pprof/
注意保证你的项目处于运行状态!
- CPU
打开终端运行命令go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
输入top
flat 当前函数的执行耗时
flat% flat占CPU总时间的比例
sum% 上面每一项的flat%总和
cum 指的是本函数加上其调用函数的总耗时
cum% cum占CPU总时间的比例
细心的你思考一下,什么时候flat == cum?,什么时候flat == 0?
当前函数没有调用其他函数 flat == cum
当前函数只调用其他函数,而本身并不占用CPU时间 flat == 0
可以看到函数Eat占用了最多的CPU时间,命令list通过正则式查找代码行
其中第24行的for循环占用的绝大部分CPU时间,把这段代码注释掉(做性能优化)
命令web可以使调用关系可视化:
优化CPU之后,我们转而优化内存占用
- Heap-堆内存
我们将优化CPU后的程序重新运行,打开终端输入命令
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
pprof自动使用图形化界面展示占用内存的比例,可以看到包mouse中的函数steal占用绝大部分内存,转而排查该函数:
alloc_objects 程序累计申请对象数 alloc_space 程序当前持有对象数 inuse_objects 程序累计申请内存大小 inuse_space 程序当前占用的内存大小
func (m *Mouse) Steal() {
log.Println(m.Name(), "steal")
max := constant.Gi
for len(m.buffer)*constant.Mi < max {
m.buffer = append(m.buffer, [constant.Mi]byte{})
}
}
可以看到该函数持续的申请内存,将这段代码注释掉(优化),重新运行,可以看到内存占用问题已经大大改善了。
- goroutine
运行程序,终端输入命令go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
可以点击页面viem -> flame Graph转而观察程序的火焰图,Drink占用了绝大部分协程,转向source视图,搜索Drink看看它到底做了什么
可以看到它在routine中等待30s才退出,将相关代码注释掉(优化)
- block
运行程序,终端输入命令go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
然后排查发生block的两个函数,基本步骤和上面类似
- 总结