发行版及优化课程笔记 | 青训营笔记

72 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第四篇笔记。

1 自动内存管理

1.1 相关概念

Mutator:业务线程,分配新对象,修改对象指向关系
Collector: GC线程,找到存活对象,回收死亡对象的内存空间
Serial GC: 只- collector
Parallel GC:支持多个 collectors同时回收的GC算法
Concurrent GC: mutator(s) 和 collector(s) 可以同时执行

image-20220517151144-cstbwhj.png

1.2 追踪垃圾回收

  • 对象被回收的条件:指针指向关系不可达的对象
  • 标记根对象
    静态变量、全局变量、常量、线程栈等
  • 标记:找到可达对象
    求指针指向关系的传递闭包:从根对象出发,找到所有可达对象
  • 清理:所有不可达对象
    将存活对象复制到另外的内存空间(Copying GC)
    将死亡对象的内存标记为“可分配" (Mark-sweep GC)
    移动并整理存活对象(Mark-compact GC)
  • 根据对象的生命周期,使用不同的标记和清理策略

1.3 分代GC(Generational GC)

每个对象都有年龄:经历过GC的次数,很多对象在分配出来后很快就不再使用了,针对年轻和老年的对象,制定不同的GC策略,降低整体内存管理的开销

年轻代存活对象少,可以使用copying collection,老年代趋于一直存活,反复复制开销大,可以采用mark-sweep collection

1.4 引用计数

每个对象都有一个与之关联的引用数目
对象存活的条件:当且仅当引用数大于0

优点

内存管理的操作被平摊到程序执行过程中
内存管理不需要了解runtime的实现细节: C++智能指针(smart pointer)

缺点

维护引用计数的开销较大:通过原子操作保证对引用计数操作的原子性和可见性
无法回收环形数据结构——weak reference
内存开销:每个对象都引入的额外内存空间存储引用数目
回收内存时依然可能引发暂停(如大型结构)

2 Go内存管理及优化

2.1 内存分配

为对象在heap上分配内存

分块

提前将内存分块,先申请一大块内存,然后把内存分成大块mspan,再将大块分成特定大小的小块,然后分配不包含指针的对象npscan mspan和包含指针的对象scan mspan

缓存

mcache管理一组mspan,有缓存

2.2 Go内存管理优化

  • 对象分配是高频服务,因此优化很关键
  • 小对象占比高
  • 内存分配比较耗时

2.3 优化方案 Balance GC

每个g都绑定一大块内存(1 KB),称作 goroutine allocation buffer (GAB)
GAB 用于noscan类型的小对象分配:< 128B
使用三个指针维护GAB:base, end, top
Bump pointer (指针碰撞)风格对象分配,无须和其他分配请求互斥,分配动作简单高效

3 编译器和静态分析

3.1 编译器的结构

image-20220517161159-yds2no8.png

3.2 静态分析

静态分析:不执行程序代码,推导程序的行为,分析程序的性质。
控制流(Control flow):程序执行的流程
数据流(Data flow):数据在控制流上的传递

3.3 过程内分析和过程间分析

过程内分析(Intra-procedural analysis):仅在函数内部进行分析
过程间分析(Inter-procedural analysis):考虑函数调用时参数传递和返回值的数据流和控制流

4 Go编译器优化

优点:用户无感知且通用

  • 函数内联
  • 逃逸分析
  • 默认栈大小调整
  • 边界检查消除
  • 循环展开

4.1 函数内联

将被调用函数的函数体(callee)的副本替换到调用位置(caller)上,同时重写代码以反映参数的绑定

优点
消除函数调用开销,例如传递参数、保存寄存器等
将过程间分析转化为过程内分析,帮助其他优化,例如逃逸分析

缺点
函数体变大,instruction cache (icache) 不友好
编译生成的Go镜像变大

大多数情况下函数内联都是正向优化