这是我参与「第五届青训营 」笔记创作活动的第3天
性能优化及自动内存管理
为什么要做性能优化?降低成本,提高效率
软件基本结构
-
业务代码
- 比较容易获得较大的性能收益
- 依靠pprof数据驱动,首先优化最大的瓶颈
-
SDK
- Go SDK 语言运行时优化
- 全公司都要用,要考虑更多的场景
-
基础库
-
语言运行时
-
OS
软件优化
- 质量非常重要
- 保证接口稳定
- 测试用例:尽可能覆盖更多的场景,方便回归
- 文档:做了什么,没做什么,能达到怎样的效果
- 隔离:通过选项控制是否开启优化
- 可观测:必要的日志输出
自动内存管理
由程序运行时系统管理动态内存
避免手动内存管理,专注于实现业务逻辑,保证内存使用的正确性和安全性
-
Mutator 业务线程 分配新对象,改变对象指向关系
-
Collector GC线程 找到存活对象,回收死亡对象内存空间
-
Serial GC:只有一个Collector
- 会有STW
-
Parallel GC 支持多个collecors 同时回收的GC算法
-
Concurrent GC:mutator和collector可以同时执行
评价GC算法
- 安全性
- 吞吐率 1 - GC时间/程序执行总时间
- 暂停时间:STW 业务上是否感知
- 内存开销GC 元数据开销
追踪垃圾回收
- 标记根对象
- 找到可达对象
- 清理所有不可达对象
根据对象不同周期,选择不同的策略
分代GC
假设:大部分对象很快就回收了
对象的年龄:对象经过GC的次数
对象分为Young Generation 和 Old Generation
Young:采用复制的方法清理,很快就死了,复制到一块连续的内存去
Old:倾向于一直活着,采用mark sweep 机制清理
引用计数
引用计数清零,内存释放
C++智能指针
缺点:
- 开销大,需要原子操作引用计数
- 没法对付环形结构
- 额外内存开销
- 回收的时候又可能引发暂停
Go内存管理
在head上分配内存
提前将内存分块
-
系统调用mmap()向OS申请一块大内存,例如4MB
-
内存分为大块,每块8KB 称为mspan
- mspan分为特定大小的小块 8B 16B 24B 用于对象分配
-
noscan mspan 不包含指针的对象 GC不需要扫描
-
scan mspan 包含指针的对象 GC需要扫描
-
根据对象大小,选择最合适的块返回
缓存
每个P包含一个mcache用于快速分配,为绑定于p的g分配对象
mcache管理一组mspan
mcache的mspan分配完毕,向mcentral申请带有未分配块的mspan
mspan没有分配的对象,mspan会缓存在mcentral中,而不是立即释放还给OS
优化方案
对象分配是非常高频的操作,每秒GB级别
小对象占比较高
Go内存分配比较耗时 g->m->p-> mcache-> mspan -> memory block -> return pointer
字节方案 Balanced GC
小对象使用GAB分配,采用指针碰撞的方式,1KB的GAB,分配8B,TOP指针移动8B即可,有start,end两个指针来定界
缺点:一个8B小对象就能让整个GAB存活
方案:当GAB超过上限时,采用copy机制把两个GAB中的小对象copy到一个GAB里紧密排列
编译器和静态分析
编译器
-
词法分析
-
语法分析 抽象语法树
-
语义分析
-
中间表示
- 主要集中优化后端:以下亮点
-
代码优化
-
代码生成 目标代码
静态分析
- 控制流
- 数据流
过程内分析:只在函数内
过程间分析:
Go编译器优化
函数内联
将被调用的函数体的副本替换到调用位置上,重写代码反映参数的绑定
优点:
- 不用传递参数,保存寄存器
- 过程间分析转化为过程内分析,有利于逃逸分析
缺点:
- 函数体变大
- 编译生成的Go镜像变大
Beast Mode
Go的interface和defer不太好内联
调整内联策略,使得更多函数被内联
降低开销,有助于逃逸分析
逃逸分析
若发现指针p在当前作用域s:
- 作为参数传递给其他的函数
- 传递给全局变量
- 传递给其他goroutine
- 传递给已逃逸指针指向的对象
则说明p指向的对象逃逸出s
BeastMode:函数内联拓展了逃逸边界,更多对象不逃逸
优化:
- 未逃逸对象栈上分配,移动sp指针就行
- 减少在heap上分配,降低GC负担