Dawn's Blogs

分享技术 记录成长

0%

GO语言杂谈 (12) 内存管理和编译器优化

内存管理

Go 中,内存分配主要有两个思想:

  • 分块
  • 缓存

分块

分块的思路为:

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

缓存

缓存的基本思路是:

  • 每一个 P 包含一个 mcache 用于快速分配,mcache 管理一组 mspan
  • 当 mcache 中的 mspan 分配完毕,向 mcentral 申请带有未分配块的 mspan。
  • 当 mspan 中没有分配的对象,mspan 会被缓存在 mcentral 中,而不是立刻释放归还给 OS。

1655263401798

优化 - Balanced GC

字节跳动有自己的 Go 语言内存管理优化方案,即 Balanced GC,其思路如下:

每个 G 都绑定一大块内存(1 KB),称为 Goroutine Allocation Buffer(GAB)

  • GAB 用于 noscan 类型的小对象(小于 128B)的分配。
  • GAB 使用三个指针进行维护:base、end、top。使用指针碰撞风格进行对象分配。

1655263925528

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

GAB 有一个问题:会导致内存被延迟释放,GAB 中即使只有一个很小的对象存活,Go 内存管理也不会回收其余空闲空间。

解决方法

当 GAB 中存活对象大小少于一定阈值时,将 GAB 中存活的对象复制到另外分配的 GAB(Survivor GAB)中,原先的 GAB 可以释放。

1655264117801

编译器优化

函数内联

函数内联是指,将被调用函数的函数体副本替换调用位置上,同时重写代码以反映参数的绑定。

优点

  • 消除函数调用的开销,如传递参数、保存寄存器等。
  • 扩展了函数边界,更多对象不逃逸分析。

缺点

  • 函数体变大。
  • 编译生成的可执行文件变大。

逃逸分析

逃逸分析步骤

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

对于未逃逸的对象,在上分配;逃逸对象,在上分配。