性能优化
- 前提是满足正确可靠、简洁清晰等质量因素
- 综合评估,时间和空间效率对立
- 根据go语言特性,提出go相关的优化建议
Benchmark-性能测试工具: Go test -bench=. -benchmem
执行结果
- BenchmarkFib10-8:测试函数名,-8标识GOMAXPROCS(CPU核数)值为8
- 1866870:表示一共执行1855870次
- 602.5 ns/op 每次执行花费时间
- 0.8 B/op 每次执行申请多大内存
- 0 allocs/op 每次执行申请几次内存
Slice预分配内存:
尽可能在使用make()初始化切片时提供容量信息
扩容过程会耗时
大内存未释放(在已有的切片基础上创建切片,不会创建新的底层数组) 1.根据原始数组创建新的切片 2.创建新的切片,将原始数组内容拷贝
使用copy代替re-slice
Map预分配内存(添加元素时会导致扩容,因此提前分配空间可以减少rehash的消耗)
字符串处理
- +,拼接//最差(会开辟新空间)
- String.Builder//耗时最短
- Byte.Buffer//
字符串是不可变类型,内存大小固定. 每次使用+会重新分配内存. String.Builder和bytes.Buffer底层都是[]bytes数组. 内存扩容策略,拼接时不需要重新分配内存.
String.Builder:直接将底层的byte数组转为字符串类型返回 bytes.Buffer:将bytebuffer转为字符串时重新申请了一块空间
空结构体不占据任何内存空间
Atomic包(优于加锁)
- 锁实现通过系统调用实现
- Atomic通过硬件实现,效率比锁高
- Sync.Mutex应该用来保护一段逻辑而不仅仅是一个变量
- 对非数值操作,可以使用atomic.Value,能承载一个interface()
Go语言优化
- 性能优化:减少组件中不必要的性能消耗
- 业务层优化:针对特定场景,具体问题具体分析
- 语言运行时优化
- 解决通用性能问题
数据驱动
- 采用pprof,定位优化问题
- 依靠数据而非猜测
- 优先优化最大瓶颈
- 保证接口稳定前提下改进具体实现
- 更多测试
- 撰写文档
- 隔离性:选项控制是否开启优化
- 可观测:必要的日志输出
自动内存管理
- 对象:动态内存(运行时根据需求分配的内存)‘
- 自动内存管理(垃圾回收):由程序余元运行时系统管理动态内存(避免手动管理,保证正确性和安全性)
- Double-free:两次释放内存
- Use-after-free:释放之后再次使用
核心任务:
- 微信对象分配空间
- 找到存活空间
- 回收死亡对象的内存空间
相关概念:
- Mutator:业务线程,分配新对象,修改对象指向关系
- Collector:GC线程,找到存货对象,回收死亡对象的内存空间
Serial GC:只有一个collector/会有pause
Parallel GC:支持多个collectors同时回收GC算法/会有pause
Concurrent GC:业务线程和GC线程可以同时执行,/Collector必须感知对象指向关系的改变
在gc过程中产生了新的引用应该如何标记?因此该过程中必须感知到指向关系的改变如何解决?
Go v1.3(serial 和parallel GC)(同Mark and sweep法)
- 将所有业务逻辑暂停并找到所有可达和不可达的对象
- 启动collector开始标记所有可达的对象
- 清除所有未标记的对象
- 结束暂停
Go v1.5
三色标记法
- 白色:垃圾对象
- 灰色:被标记,但其对象下的属性未被完全标记
- 黑色:被标记,且对象下的属性也被完全标记
步骤:
- 只要创建新对象,默认都标记为“白色”
- 每次GC开始回收,从根节点遍历所有对象,将遍历到的对象从“白色”标记为“灰色”
- 遍历灰色对象,将灰色对象引用的对象全部标记为灰色,再将该灰色对象标记为黑色,重复操作直到没有灰色对象
- 回收所有白色对象
Bug:当一个灰色对象C和一个白色对象D断开引用(D应该被回收)此时一个黑色对象A(已经被标记扫描后,其引用的应该全为黑)再被扫描之后再次引用了D,此时D本应该不会被回收,但是它错过了A的扫描时间,因此还是会被回收,这样就出现了bug。
解决方法:
- 强三色不变式:破坏条件一、不允许黑色对象引用白色对象
- 弱三色不变式:破坏条件二、黑色可以引用白色对象,但必须保证其必须存在灰色对象的引用(这样才能保证其能被再次扫描)
屏障机制:(只能用于堆)
插入屏障:在对象别引用时将被引用对象强制变成灰色(强三色不变,黑色不会引用白色)
删除屏障:断开引用时将被断开的标记为灰色(弱三色不变式)
Go v1.8
- GC开始时将栈上对象全部扫描并标记为黑色,之后不在进行重复扫描
- GC期间,在栈上创建任何对象均标记为黑色
- 被删除的对象标记为灰色
- 被添加的对象标记为灰色
Go v1.3版本,普通的标记清除法,整体需要STW,效率较低;
Go v1.5版本,三色标记法,堆空间启用屏障,栈空间不启用,全部扫描后,需要重新扫描一次栈(需要STW),效率普通;
Go v1.8版本后,三色标记法+混合写屏障,栈空间不启动屏障,堆空间启动,几乎不需要STW,整体效率较高;
GC算法评价
- Safety:不能回收存活的对象
- Throughout:1-GC时间/程序执行总时间
- Pause time:STW 业务感知到暂停
- 内存开销:GC的内存开心
Tracing garbage collection:追踪垃圾回收
- 标记根对象(静态变量、全局变量、常量、线程栈)
- 找到可达对象
- 清理所有不可达对象
清理策略
- copying GC:将存活对象复制到另外的内存空间
- mark-sweep;将死亡对象的内存标记为可分配
- mark-compact(原地整理对象):移动并整理存活对象
分代GC
对象年龄:经过GC的次数
将年轻的对象和老年对象制定不同的GC策略,降低内存管理的开销(不同年龄对象处于heap不同区域)
Young generation()
- 常规的对象分配
- 存活对象较少
- GC吞吐量很高
- 数量少,可以采用copy
- Old generation
- 对象一直活着,反复复制开销大
- 可以采用mark-sweep方法
引用计数
每个对象都有一个与之关联的引用计数,存活条件:当且仅当引用计数大于0
优点:
- 管理操作被平摊到程序执行过程
- 不需要了解runtime细节
缺点
- 维护引用计数开销大
- 无法回收环形数据结构(weak reference)
- 内存开销,引入额外内存空间
- 回收仍然会引发暂停(回收大数据结构)
性能调优实战
- 要依靠数据而不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
Pprof:耗费cpu和内存
找cpu占用的原因,定位:
Top命令:
- Falt:当前函数本身耗时
- Falt% flat占CPU总时间比例
- Sum% 上面每一行的flat%总和
- Cum 指当前函数本身加上其调用函数的总耗时
- Cum cum占cpu总时间比例
- Flat==cum:当前函数没有调用函数
- Falt==0 :只调用了其他函数
Heap-堆内存
- Go too pprof -http=:8080
- 显示各个方法占用内存比例
- Top视图
- Source视图(类似list)
Alloc_obejects:程序累计申请对象数 Alloc_space:程序累计申请内存大小 Inuse_objects:程序当前持有的对象数 Inuse_space:程序当前占用的内存大小
Goroutine
Go tool pprof -http-:8080
火焰图(从上到下表示对应函数,一块代表一个函数,越长表示cpu越长) 搜素定位到对应函数进行注释
Mutex-锁
- 修改命令后缀为mutex
- 在流程图找方法
- 在source中找有问题的代码
- 进行修改
业务服务优化:
- 建立服务性能评估手段
- 分析性能数据,定位性能瓶颈
- 重点优化项改造
- 优化效果验证