这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
简介
性能
怎么评估代码性能
Go语言提供了基准性能测试的 benchmark 工具
go test -bench=. -benchmen
性能优化建议
slice 预分配内存
使用基准测试结果说明差距 执行时间三分之一 内存分配次数由8次减小为1次
原理: 底层数据结构:数组片段的描述
type slice struct {
array unsafe.Pointer // 底层数组的指针
len int // 长度
cap int // 容量
}
当切片中元素个数大于切片大小时会发生扩容操作。扩容操作花费一定时间,浪费性能,为了节省这部分性能开销,如果知道具体容量,就应该预先设置容量大小。
陷阱
切片中在创建小的切片,小的切片不会复制大切片的底层数组,而是选择公用一个底层数组,使用新的指针指向合适的位置。
但是当这样一个场景,已经存在大的切片 a := [1:10000]int,这时候使用 a[1:3] 创建一个小的切片,这个切片会引用大的切片的底层数组(该数组的大小10000),进行垃圾回收的时候,会分析引用关系,因为小的切片这2个int的空间,10000int的数组空间被引用着,不能释放。这就造成系统资源的极大浪费。
解决方案
使用 copy 代替 re-slice
// 使用re-slice
func GetLastBySlice(origin []int) []int {
return origin[len(origin)-2:]
}
// 使用copy
copy(新切片,原始数组要拷贝的部分)
func GetLastByCopy(origin []int) []int {
result := make([]int, 2)
copy(result, origin[len(origin)-2:])
return result
}
// test
func testGetLast(t *testing.T, f func([]int) []int) {
result := make([][]int, 0)
for k := 0; k < 100; k++ {
origin := generateWithCap(128 * 1024) //1M
result = append(result, f(origin)) //
}
printMem(t)
_ = result
}
// 使用 go test -run=. -v 命令查看结果
Map预分配内存
基准测试验证结论
底层
不断往Map中put元素,会引发扩容,如果提前分配,可以减少rehash、分配内存的开销。
字符串处理
三种拼接工具
循环中不使用 + 拼接字符串
- strings.Builder
- strings.Buffer
+拼接
原理 go语言中字符串都是不可变类型
使用➕进行拼接时,每次都会分配新的内存空间。 strings.Builder 与 strings.Buffer 底层都是byte数组,可以直接append操作,不需要每次分配内存空间。等到所有附加操作完成,生成stirng字符串时,分配一次内存空间就好。
strings.Builder 比 strings.buffer 性能更好的原因
// strings.Buffer 部分源码
func (b *Buffer) String() string{
if b == nil {
return "<nil>"
}
// 申请新的内存创建string
return string(b.buf[b.off:])
}
// strings.Builder 部分源码
func (b *Builder) String() string {
// 直接将byte数组所在的空间指针类型强制转换为stirng类型然后*Pointer取值返回
return *(*string)(unsafe.Pointer(&b.buf))
}
进一步优化 知道字符串拼接后的容量的话,可以在stirng.Builder / strings.Buffer 中预先给byte数组进行预分配大小,进一步提升性能。
使用空结构体节省内存
空结构体特点
- 不占用内存
struct {}
作用 节省资源 作为占位符
示例 空结构体作为Map的value的占位符来实现Set Set只用到key,用不到value。将value定义为结构体的话,就不会占用内存,从而节省空间。
如果没有空结构体,那么用其他类型,即使是布尔值,也会多占一个字节。
正确使用 atomic 包
选择
- sync.Mutex 应该用来保护一段逻辑
- atomic 用来保护单个变量
- 非数值操作,可以使用atomic.Value,能承载一个interface{}
atomic包的效率比加锁的效率好很多
原理
锁是通过操作系统实现的,属于系统调用。 atomic通过硬件实现,效率好。
总结
- 刚开始时,避开常见性能陷阱就好。
- 优化要保证程序的正确性、可靠性、简洁清晰
- 越高级的优化,越容易出问题
- 普通程序,用不到太过分的优化,不一定追求极致性能。
性能优化原则
- 依靠数据不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 业务的性能瓶颈出现后,再进行优化
- 不要过度优化
- 优化手段激进的调优手段更容易出现问题
性能优化工具
pprof
功能简介
排查实战
CPU
直接看任务管理器
结果采样(命令行)
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
top 命令
查看cpu性能分析报告
结果
解释
- flat 当前函数的本身的执行耗时
- flat% flat 占 cpu 总时间的比例
- sum% 上面每一行的 flat% 综合
- cum 指当前函数本身加上其调用函数的总耗时
- cum% cum 占 CPU 总时间的比例
Flat == Cum, 当前函数没有调用其他函数
Flat == 0, 函数中只有其他函数的调用
list命令 分析
list 正则表达式
根据正则表达式查找代码
示例
web命令 可视化
web
可视化 / 显示调用关系
可视化分析
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/cpu"
内存 **
可视化分析
go tool pprof "http://localhost:6060/debug/pprof/heap
四种内存
- alloc_objects 程序累计申请的对象数
- alloc_space 程序累计申请的内存大小
- inuse_objects 程序当前持有的对象数
- inuse_objects 程序当前占用的内存大小
阻塞
go tool pprof "http://localhost:6060/debug/pprof/block
协程
go tool pprof "http://localhost:6060/debug/pprof/goroutine
适合火焰图查看
火焰图
- 从上到下代表调用顺序
- 一个小块代表一个函数,小块的长度代表占用cpu的时间长短。
- 图是动态的,可以点击小块进行分析
锁
go tool pprof "http://localhost:6060/debug/pprof/mutex
通用分析流程
- 流程图里分析找到占用最大资源的函数
- 通过source找到具体行
- 注释掉具体行后运行。
采样过程与原理
性能评估工具
Go基准测试
示例
package locks1
import "testing"
func BenchmarkFib(b *testing.B) {
for n := 0; n < b.N; n++ {
Fib(10)
}
}
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) + Fib(n-2)
}
命令行中使用 go test -bench=. -benchmem 命令可以查看基准测试报告。
示例
goos: windows
goarch: amd64
pkg: locks1
cpu: Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz
BenchmarkFib-8 4702662 261.0 ns/op 0 B/op 0
// 测试函数名-cpu核心数 //执行总次数(b.N的值) 每次执行花费的时间 每次执行申请的内存大小 每次执行申请内存次数
allocs/op
PASS
ok locks1 1.553s
总结
优先看内存、协程、cpu调优部分 了解常用的Top、调用图、火焰图、源码图等等视图 以及网页端的排查方式
原理讲解
CPU采样
采样对象: 函数调用和他们调用的时间。
采样率:100次/秒,固定值
采样时间:从手动启动到手动结束
采样流程:系统通过设定信号处理函数,并开启计时器,每10ms记录一次CPU信息,每100ms将信息写入到写缓存中,到达指定时间后,关闭计数器,取消信号处理函数。
堆内存采样
采集对象:采集内存分配器在堆上分配和释放的内存。
采样率:每分配512K记录一次
采样时间: 从程序运行开始到采样时
采样指标:分配的内存总大小,分配的总对象数、当前持有的内存大小、当前持有的对象数
协程与线程创建采样
协程采样对象:记录了所有用户发起的,运行中的Goroutine的调用栈信息。
线程采样对象:程序创建的系统线程信息。
采样时间:Stop The World
协程采样流程: Stop The World ——> 遍历allg切片 ———> 输出创建g的堆栈信息 ——> Start The World
线程采样流程: Stop The World ——> 遍历allm链表 ———> 输出创建m的堆栈信息 ——> Start The World
阻塞采样
采样对象:阻塞以及耗时
采样率:阻塞耗时超过指定阈值时记录
采样流程: 阻塞操作 ——> 上报调用栈和消耗时间到分析器 ——> 时间到达阈值后记录采样信息 ——> 最后统计阻塞次数以及耗时
锁采样
采样对象:锁竞争操作的调用栈和耗时
采样率:只记录固定比例的锁操作
采样流程: 锁竞争操作 ——> 上报调用栈和消耗时间到分析器 ——> 是固定比例中的才记录采样信息 ——> 统计锁竞争次数和耗时。
性能优化实践
业务服务优化
基础概念
服务
依赖
调用链路
基础库