这是我参与「第五届青训营」笔记创作活动的第5天
一、本堂课重点内容
- 自动内存管理
- Go 内存管理及优化
- 编译器和静态分析
- Go 编译器优化
二、详细知识点介绍
自动内存管理
追踪垃圾回收
- 被回收的条件:不可达对象
- 过程
- 标记根对象 (GC roots): 静态变量、全局变量、常量、线程栈等
- 标记:找到所有可达对象
- 清理:回收所有不可达对象占据的内存空间
- Copying GC: 将存活对象从一块内存空间复制到另外一块内存空间,原先的空间可以直接进行对象分配
- Mark-sweep GC: 将死亡对象所在内存块标记为可分配,使用 free list 管理可分配的空间
- Mark-compact GC: 将存活对象复制到同一块内存区域的开头
引用计数
- 每个对象都有一个与之关联的引用数目
- 对象存活的条件:当且仅当引用数大于 0
- 优点
- 内存管理的操作被平摊到程序运行中:指针传递的过程中进行引用计数的增减
- 不需要了解 runtime 的细节:因为不需要标记 GC roots,因此不需要知道哪里是全局变量、线程栈等
- 缺点
- 开销大,因为对象可能会被多线程访问,对引用计数的修改需要原子****操作保证原子性和可见性
- 无法回收环形数据结构
- 每个对象都引入额外存储空间存储引用计数
- 虽然引用计数的操作被平摊到程序运行过程中,但是回收大的数据结构依然可能引发暂停
- 说明
- 以上我们所讲述的技术的缺点并非是无法解决的问题。学术界和工业界在一直在致力于解决自动内存管理技术的不足之处。例如,最新的 PLDI'22 的文章Low-Latency, High-Throughput Garbage Collection。
Go 编译器优化
函数内联
- 定义:将被调用函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定
- 优点
- 消除调用开销
- 将过程间分析的问题转换为过程内分析,帮助其他分析
- 缺点
- 函数体变大
- 编译生成的 Go 镜像文件变大
- 函数内联在大多数情况下是正向优化,即多内联,会提升性能
- 采取一定的策略决定是否内联
- 调用和被调用函数的规模
- Go 内联的限制
- 语言特性:interface, defer 等等,限制了内联优化
- 内联策略非常保守
- 字节跳动的优化方案
- 修改了内联策略,让更多函数被内联
- 增加了其他优化的机会:逃逸分析
- 开销
- Go 镜像大小略有增加
- 编译时间增加
- 运行时栈扩展开销增加
逃逸分析
- 定义:分析代码中指针的动态作用域,即指针在何处可以被访问
- 大致思路
- 从对象分配处出发,沿着控制流,观察数据流。若发现指针 p 在当前作用域 s:
- 作为参数传递给其他函数;
- 传递给全局变量;
- 传递给其他的 goroutine;
- 传递给已逃逸的指针指向的对象;
- 则指针 p 逃逸出 s,反之则没有逃逸出 s.
- 从对象分配处出发,沿着控制流,观察数据流。若发现指针 p 在当前作用域 s:
- 优化:未逃逸出当前函数的指针指向的对象可以在栈上分配
- 对象在栈上分配和回收很快:移动 sp 即可完成内存的分配和回收;
- 减少在堆上分配对象,降低 GC 负担。
三、实践练习例子
Balanced GC
- 核心:将 noscan 对象在 per-g allocation buffer (GAB) 上分配,并使用移动对象 GC 管理这部分内存,提高对象分配和回收效率
- 每个 g 会附加一个较大的 allocation buffer (例如 1 KB) 用来分配小于 128 B 的 noscan 小对象
- bump pointer 风格的对象分配,示意如下。
if g.ab.end - g.ab.top < size {
// Allocate a new allocation buffer
}
addr := g.ab.top
g.ab.top += size
return addr
- 分配对象时,根据对象大小移动 top 指针并返回,快速完成一次对象分配
- 同原先调用 mallocgc() 进行对象分配的方式相比,balanced GC 缩短了对象分配的路径,减少了对象分配执行的指令数目,降低 CPU 使用
- 从 Go runtime 内存管理模块的角度看,一个 allocation buffer 其实是一个大对象。本质上 balanced GC 是将多次小对象的分配合并成一次大对象的分配。因此,当 GAB 中哪怕只有一个小对象存活时,Go runtime 也会认为整个大对象(即 GAB)存活。为此,balanced GC 会根据 GC 策略,将 GAB 中存活的对象移动到另外的 GAB 中,从而压缩并清理 GAB 的内存空间,原先的 GAB 空间由于不再有存活对象,可以全部释放。