Go内存管理与编译器优化 | 青训营笔记

81 阅读3分钟

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

Go 内存管理及优化

Go 内存分配思想 - 分块

内存分块流程

  • 系统调用 mmap( ) 向 OS 申请一块足够大的内存
  • 将内存划分成大块,即 mspan
  • 将大块划分成特定大小的小块,用于对象分配
  • noscan mspan
    GC 不需要扫描
    • 原理:因为分配的是不包含指针的对象
  • scan mspan
    GC 需要扫描
    • 原理:因为分配的是包含指针的对象

Go 内存分配思想 - 缓存

线程缓存的 Malloc
G(goroutine) M(thread) P(processor) 模型

TCMalloc 原理

  • 每一个 P 包含一个 mcache 用于快速分配
  • mcache 是为绑定在 P 上的 G 分配对象
  • mcache 作为缓存区 管理一组 mspan
  • 当 mspan 中没有分配对象时,mspan 会被缓存在 mcentral 全局中心缓存中,并不会立即释放归还给 OS

Go 内存分配 - 优化策略

Go 实际开发中的背景

  • 对象分配非常高频率

    • 每秒分配 GB 级别的内存
    • 对象分配的函数是最频繁调用的函数之一
  • 小对象占比较高

  • 分配路径流程

    • G -> M -> P -> mcache -> mspan -> memory block -> return pointer
    • 路径过长

Balanced GC 优化策略

  • G(goroutine) A(allocation) B(buffer)

    • GAB 相当于是是一个大对象
    • 本质是将多次的小对象的分配合并成一次的大对象分配
  • 每个 G 绑定一大块内存

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

  • 三指针维护 G A B

    • base
    • end
    • top
  • 内存分配流程

    • 直接操作 top 指针移动指定长度,并返回,完成内存分配
	if top+size <= end {
		addr := top
		top += size // move pointer
		return addr // memory
	}
  • Balanced GC 性能缺陷

    • GAB 中 存活的小对象会使 GAB 整个被标记为存活的
    • 导致 GAB 整个被占用,造成内存延迟释放
  • Balanced GC 性能优化方案

    • 当多个 GAB 中存在部分存活对象,且所占内存总和超过了一定阈值,我们将这些 GAB 中的内存复制到另外分配的 GAB 中
    • 这样子原先的 GAB 就能释放掉

Go 编译器优化

Go 编译器现状

  • 采用的优化少
  • 为了缩短编译时间,Go没有进行较为复杂的代码分析和优化

编译器优化原因

  • 编译器优化能够使用户无感知,重新编译即可获得性能收益
  • 编译器优化属于通用性优化

编译器优化方向

函数内联

原理:

  • 直接将被调用的函数体的副本替换到调用位置上

优势:

  • 消除函数调用开销,因为不需要传递参数等
  • 将过程间分析转化为过程内分析,能够为逃逸分析优化提供便利
  • 函数 inline 后性能提升明显

缺点:

  • 函数体变大
  • 编译生成的 Go 镜像变大

逃逸分析

原理:

  • 指针在哪里可以被访问

逃逸表现

  • 指针作为参数传递给其他函数
  • 指针传递给全局变量
  • 指针传递给其他的 goroutine
  • 指针传递给已经逃逸的指针 所指向的对象