这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记
性能优化基本问题
- 性能优化:提升软件系统处理能力,减少消耗,发掘算力
- 为什么要做性能优化
- 用户体验
- 资源高效利用:降低成本提高效率(很小的优化乘海量的机器也会带来很大收益)
性能优化的两个层面(业务、语言)
- 业务层优化
- 针对特定场景具体问题具体分析
- 容易得到大收益
- 语言运行时优化——对SDK进行优化
- 解决更通用的性能问题
- 考虑更多场景
- Tradeoffs:需要做权衡
- 数据驱动
- pprof
- 依靠数据而非猜测
- 首先优化最大瓶颈
性能优化与软件质量(可维护性)
- 保证接口稳定的情况下改进具体实现
- 测试用例:覆盖尽可能多的场景,方便回归。(测试样例都跑一遍)
- 文档:做了、没做、取得什么效果
- 隔离:用选项控制是否开启优化,保证行为的一致性
- 可观测:必要的日志输出
1、自动内存管理
背景
- 动态内存:malloc()
- 自动内存管理(垃圾回收):
- 避免手动内存,专注业务逻辑
- 保证内存使用的正确性和安全性:double free problem、use after free problem
- 三个核心任务
- 新对象分配空间
- 找到存活对象
- 回收死亡对象的内存空间
相关概念
-
Mutator:业务线程,分配新对象,修改对象指向关系
-
Colloctor:GC线程,找到存活对象,回收死亡对象的内存空间
-
Serial GC:只有一个collector
-
Parallel GC: 支持多个collectors同时回收的GC算法
-
Concurrent GC:Mutator和collector可以同时执行,一边垃圾回收一边用户线程执行
- collectors必须感知对象指向关系的改变
- collectors必须感知对象指向关系的改变
-
GC算法
- 安全性:不能回收存活对象
- 吞吐率:花在GC上的时间(吞吐量越高越好)
- 暂停时间:业务是否感知(时间越短越好)
- 内存开销:GC元数据开销(越低越好)
方法一:追踪垃圾回收(Tracing GC)
- 对象回收条件:指针不可达
- 标记根对象
- 静态、全局变量、常量、线程栈
- 标记可达对象:从跟对象出发找到所有可达对象
- 清理:所有不可达对象
- copying GC 新复制旧需要新空间
- copying GC 新复制旧需要新空间
-
- Mark-sweep GC使用free list管理空闲内存
- Mark-sweep GC使用free list管理空闲内存
-
- Mark-compact-GC
原地整理
- Mark-compact-GC
原地整理
根据生命周期、选择不同标记和清理策略
GC策略:分代GC
-
分代假说:most objects die young 很多对象分配出来就不再使用
-
对象年龄:经历过GC的次数
-
目的:不同年龄不同Gc策略降低整体内存管理开销
-
不同年龄的对象处于heap的不同区域
-
年轻代
- 常规对象分配
- 由于存活对象少,可以采用copy GC
- 吞吐量高
-
老年代
- 对象趋于活着,反复复制开销大
- 采用其他两种方式
方法二:引用计数
- 对象存活条件:当且仅当引用数大于0
- 优点:
- 内存管理的操作平摊到程序执行过程中
- 内存管理不选要了解runtime的实现细节:C++智能指针
- 缺点:
- 维护起来开销大:通过原子操作保证原子性和可见性
- 无法回收环形数据结构
- 内存开销:引入额外内存空间存储引用数目
- 回收时依然可能引发暂停
2、Go内存管理以及优化
Go内存分配
分块
- 目标:为对象在heap上分配内存
缓存
每类大小的mspans都会对应相同大小的mcentral
go内存管理优化
- 对象分配是高频操作:每秒分配GB级别内存
- 小对象占比高
- go内存分配比较耗时
- 分配路径长
- pprof:对象分配的函数是最频繁调用的函数之一
- 分配路径长
优化方案:Balanced GC
感觉核心是对noscan类型的对象缩短分配路径。
技术细节
-
GAB对Go内存管理来说是一个大对象
-
本质上:将对多个小对象的分配合并成一次大对象的分配
-
问题:GAB的对象分配会导致内存被延迟释放
-
方案:移动GAB中存活的对象
- GAB的大小超过阈值时,将存活的对象复制到另外分配的GAB中
- 原先GAV释放,避免内存泄漏
- 本质:用copying GC管理算法的小对象,Survivior GAB不属于任一个Goroutine,由go进行内存块的管理
性能收益:利用率下降4.6%
3、编译器和静态分析
编译器的结构
- IR是机器无关的
编译器后端优化:静态分析
- 不去执行代码,推导程序的行为、分析程序的性质
- 控制流:程序执行的流程
- 数据流:数据在控制流上的传递
- 通过分析数据流和控制流,可以知道更多关于程序的性质
- 根据性质优化代码
过程内分析和过程间分析
- 过程内分析:函数内部分析
- 过程间分析:函数调用时的参数传递和返回值的数据流和控制流
4、go编译器的优化
背景
- why?
- 用户无感知,重新编译就有收益
- 通用性优化
- 现状
- 采用的优化少
- 编译时间段,没有复杂代码分析和优化
- 编译优化的思路
- 场景: 面向后端长期执行任务
- Tradeoff:用编译时间换取更高效的机器码
- Beast mode:
- 函数内联
- 逃逸分析
- 。。。
函数内联
- 内联:将被调用函数的函数体副本替换导调用位置上,重新代码以反映参数的绑定
- 优点:
- 消除函数调用的开销,如传参、保持寄存器
- 将过程间分析转换为过程内分析,帮助其他优化,如逃逸分析
- 性能提升内联后提升4.58倍
- 使用micro-benchmark快速验证和对比性能结果
- 缺点:
- 函数体变大,icache不友好
- 编译生成的go镜像变大
Beast Mode
- go函数内联受到的限制较多
- 语言特性
- 内联策略保守,很多函数不会内联
- Beast mode: 调整函数内联的策略,使更多函数被内联
- 降低系统调用开销
- 增加其他的优化机会
- 开销
- go镜像增加10%
- 编译时间增加
逃逸分析:
- 分析代码中指针的动态作用域,指针在何处可以被访问
- 核心是:p能不能在s外访问到
- Beast mode:内联扩展函数边界,更多对象不逃逸
- 优化:未逃逸对象可以在栈上分配
- 对象在栈上分配和回收很快:移动sp
- 减少在heap上的分配,降低GC负担