第五届字节跳动青训营Class4笔记 | 青训营笔记

92 阅读6分钟

这是我参与「第五届青训营 」笔记创作活动的第4天

本节课将主要介绍关于高性能 Go 语言发行版优化的内存管理优化,分享自动内存管理与 Go 内存管理知识,提供可行性的优化建议。

1.自动内存管理

动态内存

程序在运行时根据需求动态分配的内存:malloc()

自动内存管理(垃圾回收):由程序语言运行时系统管理动态内存,保证了内存使用的正确性和安全性,避免如double-free problem和use-after-free problem

三个任务:为新对象分配空间,找到存活对象,回收死亡对象的内存空间。

Garbage collector(GC)

相关概念

  • Mutator: 业务线程(用户启动的),分配新对象,修改对象指向关系
  • Collector: GC 线程,找到存活对象,回收死亡对象的内存空间

image.png

  • Serial GC:只有一个Collector

image.png

  • Parallel GC: 并行 GC,支持多个Collector同时回收

image.png

  • Concurrent GC: 并发 GC,Mutator和Collector可以同时执行

image.png

Collectors必须感知对象指向关系的改变:在Concurrent GC过程中,标记存活对象的同时,被标记对象可能同时又多指向了一个新对象:

image.png

评价GC算法

  • 安全性:不能回收存货的对象
  • 吞吐率:1-GC时间/程序执行总时间(越高越好)
  • 暂停时间:stop the world(STW) 业务是否感知
  • 内存开销:GC元数据开销

追踪垃圾回收

对象被回收条件:指针指向关系不可达的对象

步骤:

①标记根对象:静态变量、全局变量、常量、线程栈

②标记:找到可达对象(求指针指向关系的传递闭包)

③清理:所有的不可达对象。有三种清理方式:

  • 存活对象复制到另外的内存空间(Copying GC);

image.png

  • 将死亡对象的内存标记为可分配;移动并整理存活对象(Mark-sweep GC)

image.png

  • 移动并整理存活对象(Mark-compact GC) (Compact GC(原地整理对象),即将存活变量移动到堆的头部,在移动后的位置的末尾开始做内存分配)

image.png

分代GC

分代假说:每个对象都有年龄(经历过GC的次数),很多对象分配出来后很快就不使用了,也即most objects die young

针对年轻和老年的对象,指定不同的GC策略,降低整体内存管理的开销

年轻代:常规的对象分配,由于存活对象很少,可以采用copying collection

image.png

老年代:对象趋于一直活着,反复复制的开销较大,可以使用mark-sweep collection

image.png

引用计数

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

优点:内存管理的操作被平摊到程序执行的过程中,内存管理不需要了解runtime的实现细节

eg. C++智能指针

缺点:

  • 维护开销较大,需要使用原子操作保证对引用计数操作的原子性和可见性
  • 无法回收环形数据结构
  • 内存开销:每个对象都引入的额外内存空间存储引用数目
  • 回收内存时仍然可能引发暂停

2.Go语言内存管理及优化

分块

目标:为对象在堆上分配内存

提前内存分块:

  • 调用系统调用mmap()向OS申请一大块内存,例如4MB
  • 先将内存划分为大块,例如8KB,称作mspan
  • 再将大块继续划分成为特定大小的小块,用于对象分配
  • noscan mspan:分配不包含指针的对象——GC不需要扫描
  • scan mspan:分配包含指针的对象——GC需要扫描

缓存

image.png 以g(goroutine)作为根节点,p节点上含有一个数据结构(mcache),mcache存放了一组mspan,等待快速分配,如果mcache中的maspan已满,则在mcentral中寻找一个符合大小要求的mspan。

Go内存管理优化

对象分配是非常高频的操作:每秒分配GB级别的内存,小对象的占比较高

Go的内存分配路径:

g -> m -> p -> mcache -> mspan -> memory block -> return pointer

优化方案 Balanced GC

每个G都绑定一大块内存(1KB),称作goroutine allocation buffer(GAB)

GAB用于noscan类型的小对象分配<128B

使用三个指针维护GAB:base, end, top

Bump pointer(指针碰撞)风格对象分配,无需和其他分配请求互斥,分配动作简单高效

image.png

if top + size <= end {
    addr := top
    top += size
    return addr
}

GAB对于Go内存管理来说是一个大对象

本质:将多个小对象的分配合并成一次大对象的分配

问题:GAB的对象分配方式会导致内存被延迟释放

方案:移动GAB中存活的对象

  • 当GAB总大小超过一定阈值时,将GAB中存活的对象复制到另外分配的GAB中
  • 原先GAB可以释放,避免内存泄露
  • 本质:用copying GC的算法管理小对象 image.png

3.编译器和静态分析

编译器的结构

image.png

静态分析

静态分析:不执行程序代码,推导程序的行为,分析程序的性质。

控制流(Control flow):程序执行的流程 image.png

数据流(Data flow):数据在控制流上的传递

image.png

如本过程,通过静态分析,得到了本函数数据流会返回4,则函数被优化为return 4

过程内分析和过程间分析

过程内分析:仅在过程内部进行分析

过程间分析:考虑过程调用时传递和返回值得数据流和控制流

image.png

只有通过数据流分析得知i的具体类型,才能够知道foo()是哪个类型的方法,根据i的类型可知调用了A.foo(),再进行控制流分析。————联合求解

4.Go编译器优化

编译优化的思路:

  • 场景:面向后端长期执行任务
  • 代价:用编译时间换取更高效的机器码 Beast mode:
  • 函数内联
  • 逃逸分析
  • 默认栈大小的调整
  • 边界检查消除
  • 循环展开
  • ...

函数内联(inlining)

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

优点:消除函数调用的开销,将过程间分析转化为过程内分析

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

逃逸分析

分析代码中指针的动态作用域:指针在何处可以被访问

大致思路:

  • 从对象分配处出发,沿着控制流,观察对象的数据流
  • 若发现指针p在当前作用域s:
    • 作为参数传递给其他函数
    • 传递给全局变量
    • 传递给其他的goroutine
    • 传递给已逃逸的指针指向的对象
  • 则指针p指向的对象逃逸出了s,反之则没有逃逸出s

优化:函数内联减少了逃逸对象的数目,未逃逸对象可以在栈上分配

总结

本节课程以GC作为引入,介绍了一部分GC的算法,然后代入到Go语言的GC机制中去,讨论了Go语言长GC链的优缺点,并且根据实际业务场景、编译器后端的处理逻辑,介绍了GAB和Beast Mode两个优化思路,扩展了对于Go语言底层的认识。