这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记
优化要求
- 保证接口稳定的前提下改进具体实现;
- 拥有测试用例;
- 可以选择是否开启优化,并让不开启优化的用户不受影响;
- 拥有文档(优化了什么?达到的什么效果?);
- 可观测优化结果。
自动内存管理
基础概念
Mutator:业务线程,分配新的对象,修改对象的指向关系
Colletor: GC 线程,找到存活的对象,回收死亡对象的内存空间
Serial GC: 只有一个 Collector,需要暂停(先执行 Mutator,在需要执行 Collector 时 Mutator 必须暂停)
Parallel GC:可以同时有多个 Collector 和 Mutator
Concurrent GC: Mutator 和 Collector 可以同时执行
评价指标
安全性(Safety):不能回收存活的对象
吞吐率(Throughput):
\
暂停时间(Pause time):Stop the world,即在进行 GC 时业务需要暂停的时间
内存开销(Space overhead):GC 元数据开销
追踪垃圾回收(Tracing garbage collection)
引用计数(Reference counting)
《The Garbage Collection Handbooks》
追踪垃圾回收
-
标记根对象:静态变量、全局变量、常量、线程栈等,在之后的程序运行过程中仍有可能用到,即便不可达也应标记为存活
-
标记可达对象:求指针指向关系的传递闭包:从根对象出发,找到所有可达对象
-
清除不可达对象(根据对象的生命周期选择策略):
- Copying GC: 复制存活对象到另外的内存空间
- Mark-sweep GC: 将死亡对象内存标记为可分配
- Mark-compact GC: 移动并整理存活对象
分代 GC (Generation GC)
分代假说:变量的生命周期短,很多对象分配后很快就不使用了
为对象设置年龄:经历过GC的次数
- Young Generation:常规内存对象分配。由于存活对象少,所以可以采用 Copying GC
- Old Generation:对象趋向于一直存活,可以采用 Mark-sweep GC
Go 内存管理及优化
内存分配
目标:在 Heap 上分配内存
策略:提前将内存分块
- 先试用
mmap()向 OS 申请一大块内存 - 将内存划分成大块
- 再将大块划分成特定大小的小块,用于对象分配
- noscan mspan 用于分配不包含指针的对象,GC无需扫描它
- scan mspan 分配包含指针的对象,GC 需要扫描它,找到指针指向的对象
Go 编译器优化
编译器优化:用户没有感知,重新编译即可获得性能收益。通用性优化。
思路:用编译时间换取高效机器码
Beast Mode:函数内联、逃逸分析、...
函数内联
把被调用的函数体(callee)的副本替换到调用位置(caller)上,同时重写代码以反映参数的绑定
类似于 C++ 中显式指定的 inline
优点:
- 消除函数调用开销
- 将过程间分析转化为过程间分析
缺点:
- 函数体变大,instruction cache 不友好
- 编译后Go 镜像变大
函数内联大部分情况是正向优化,可以使用内联策略判断什么情况下需要内联
Beast Mode
interface,defer等功能限制了 Go 的函数内联- 调整了函数内联的策略,使更多函数被内联
- Go 镜像大小增加、编译时间增加
逃逸分析
分析指针的动态作用域,指针在何处可以被访问
逃逸情况:
- 作为参数传递给了其他函数
- 传递给了全局变量
- 传递给了其他 goroutine
- 传递给了已经逃逸的指针指向的对象
Beast Mode
- 对象在栈上分配和回收很快
- 减少在 heap 上的分配,降低 GC 负担
\