Go语言优化与落地实践 | 青训营笔记

162 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

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

背景

自动内存管理和Go的内存管理机制

内存分配

GO中内存布局如下
mheap管理着地址空间的一大段连续的内存,以8K为一页,多个页组成一个个span,多个span组成一个arena。每个span只存储一种大小的元素

image.png span的数据结构是mspan,span存储大小规格覆盖了小于等于32k的66种大小,对应的编号为1-66,记录再mspan的spanclass中。而大于32k的记录编号为0,会直接在mheap中分配。

type mspan struct {
    next *mspan
    prev *mspan
    ...
}

构成双向链表,使用runtime.mSpanList存储头结点和尾结点在mcache以及mcentral中使用 image.png

除此之外,还会按照元素是否含有指针来分类,存在spanclass中

  • 存在指针 -- scan
  • 不存在指针 -- no-scan 因此一共有134种

类型元数据的 _typeptrdata就是用于区分需要放在scan还是no-scan中,ptrdata=0的会放在no-scan中,ptrdata不为0的会分配到scan中,会被gc扫描。

  • 减少了gc耗时

go实现了全局和本地span缓存,防止每次进行堆空间分配的时候都要去申请一次

mheap.central记录了span的全局缓存,每一个mcentral代表了一种spanclass。并且将有空闲空间empty,没空闲空间nonempty分别管理

每个P都有一个mcache用于本地缓存,一样用spanclass区分,共134个mspan链表

image.png

  • 微对象 (0, 16B) — 先使用微型分配器,再依次尝试线程缓存、中心缓存和堆分配内存;
  • 小对象 [16B, 32KB] — 依次尝试使用线程缓存、中心缓存和堆分配内存;
  • 大对象 (32KB, +∞) — 直接在堆上分配内存;

每个处理器(P)都会分配一个线程缓存mcache用于处理微对象和小对象的分配,题目会持有不同规格的内存管理单元mspan

每个类型的内存管理单元都会管理特定大小的对象,(8B,16B,32B...32KB)当内存管理单元中不存在空闲对象时,它们会从 mcentral 中获取新的内存单元,中心缓存属于全局的堆结构体 mheapmheap会从操作系统中申请内存。

GC

目前go的gc使用的是无分代(对象没有代际之分)、不整理(回收过程中不对对象进行移动与整理)、并发(与用户程序并发执行)的三色标记清扫算法

  • 对象整理的优势是解决了内存碎片化问题以及“允许”使用顺序内存分配器
    • go运行时的分配算法基于tcmalloc,基本没有碎片问题
    • 顺序内存分配器在多线程场景下不适用
  • 分代假设没有带来直接的优势,且go的垃圾回收器与用户程序并发执行,使得STW的时间与对象的代际、对象的size没有关系
    • go编译器会通过逃逸分析将大部分新生对象存储在栈上,会直接回收。当goroutine死亡后栈也会被直接回收,不需要gc的参与

具体流程如下

  • GC在准备阶段会为每个P创建一个mark worker协程,即p.gcBgMarkWorker(现在好像变成了一个池),后台makr worker很快就进入休眠,等待到标记阶段进入执行
	// Pool of GC parked background workers. Entries are type
	// *gcBgMarkWorkerNode.
	gcBgMarkWorkerPool lfstack
  • 当第一次进入STW的时候,用于标记gc阶段的全局变量gcphrase= _GCMark,全局变量writeBarrier.enabled=true用于标记是否开启写屏障,全局变量gcBlackenEnabled=1用于标记是否允许进行gc mark工作
  • 当STW结束后,所有p知道写屏障已经开启,然后后台的mark worker协程开始调度开始gc mark工作
  • 当没有标记工作的时候,进入第二个STW,将gcphrase设置为 _GCMarkTermination,确认标记工作已经完成,并将gcBlackenEnabled标记为0
  • 接下来进入 _GCOff阶段,将gcphrase标记为 _GCOff,关闭写屏障,设置writeBarrier.enabled=false,Start The World,进入清扫流程,bgsweep开始调度
    • 在进入_GCOff之前新分配的对象为黑色,进入之后就是白色的
    • 执行清扫工作的协程由runtime.main在gcenable中创建,标记为bgsweep。对应的g指针存储在全局变量sweep中
    • 由于清扫工作也是增量进行的,因此在开始清扫前还要先保证完成上一次gc未完成的清扫工作

编译器优化基本问题和思路

  • Go中由于语言特性,例如interface,defer等,限制了函数内联
  • 内联策略非常保守

优化

内存管理优化 -- Balanced GC

为每个g都绑定一大块内存(1KB,也是相当于多个span的集合),称作goroutine allocation buffer(GAB)

  • GAB用于对no-scan类型的小对象分配(这里指<128B的)
  • 使用了三个指针维护GAB:base(指向开始),end(结束点),top(当前指针指向的地址,即用到了的地方)
  • Bump pointer(指针碰撞风格对象的分配)
    • 无须和其他分配请求互斥
    • 分配动作简单高效

分析

本质是对多个小对象的分配合并成了一次大对象的分配,在每一次分配适当大小的no-scan小对象时,不需要走冗长的内存分配路径,直接从g中分配即可

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

GAB不是存储在g中,对go内存管理来说就是一个对象,可以直接交由go自己的内存管理机制管理

image.png

编译器优化 -- Beast Mode

调整函数内联的策略,使得更多函数被内联

  • 降低函数调用的开销
  • 增加了其他的机会:逃逸分析

产生的主要开销为

  • Go镜像增加 约10%大小
  • 编译时间增加