高性能 Go 语言发行版优化与实践 | 青训营笔记

104 阅读3分钟

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

优化要求

  1. 保证接口稳定的前提下改进具体实现;
  2. 拥有测试用例;
  3. 可以选择是否开启优化,并让不开启优化的用户不受影响;
  4. 拥有文档(优化了什么?达到的什么效果?);
  5. 可观测优化结果。

自动内存管理

基础概念

Mutator:业务线程,分配新的对象,修改对象的指向关系

Colletor: GC 线程,找到存活的对象,回收死亡对象的内存空间

Serial GC: 只有一个 Collector,需要暂停(先执行 Mutator,在需要执行 Collector 时 Mutator 必须暂停)

Parallel GC:可以同时有多个 Collector 和 Mutator

Concurrent GC: Mutator 和 Collector 可以同时执行

评价指标

安全性(Safety):不能回收存活的对象

吞吐率(Throughput):

1-\frac{Time_{GC}}{Time_{total}}\

\

暂停时间(Pause time):Stop the world,即在进行 GC 时业务需要暂停的时间

内存开销(Space overhead):GC 元数据开销

追踪垃圾回收(Tracing garbage collection)

引用计数(Reference counting)

《The Garbage Collection Handbooks》

追踪垃圾回收

  1. 标记根对象:静态变量、全局变量、常量、线程栈等,在之后的程序运行过程中仍有可能用到,即便不可达也应标记为存活

  2. 标记可达对象:求指针指向关系的传递闭包:从根对象出发,找到所有可达对象

  3. 清除不可达对象(根据对象的生命周期选择策略):

    • 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 负担

\