欢迎光临
我们一直在努力

Go 内存分配器详解:从 mspan 布局看三级缓存如何减少锁竞争压力

Go语言以其高效的并发能力闻名,但其高性能的基础之一是极其高效的内存分配器。Go的内存分配器基于Google的TCMalloc(Thread-Caching Malloc)思想,采用了精妙的三级缓存结构,极大地减少了分配过程中的锁竞争压力。

本文将深入探讨Go内存分配的核心单元 mspan,并解释它如何在三级缓存(mcache、mcentral、堆)中协同工作,实现几乎无锁的小对象分配。

1. 为什么需要三级缓存?

传统的内存分配器通常使用全局锁来保护共享内存池,在高并发场景下,这会导致严重的性能瓶颈。Go的目标是让大多数小对象分配在不获取任何锁的情况下完成,从而支持数以万计的Goroutines并行运行。

Go通过将内存管理工作分解为三个层次来实现这一目标:

2. 第一级:mcache (Goroutine/P 局部缓存)

mcache 是分配速度最快、最核心的组件。每个操作系统线程(对应一个Go运行时处理器 P)都拥有一个私有的 mcache 实例。这个实例存储着预先分配好的、按照不同大小分类的内存块(即 mspan)。

关键特性: Goroutine 可以在其关联的 P 上分配小对象时,直接从 mcache 中获取内存,无需任何锁保护。

// 抽象概念:mcache 结构 (每个 P 私有)
type mcache struct {
    // alloc 数组存储着不同大小类别(SizeClass)的 mspan 列表
    alloc [NumSizeClasses]*mspan
    // ... 其他本地统计信息
}

// 流程:分配一个小于 32KB 的对象时:
// 1. 确定对象所属的 SizeClass。
// 2. 从 mcache.alloc[SizeClass] 中取出 span,直接分配。

如果 mcache 中某个 SizeClass 的 mspan 耗尽了,它才会向上一级缓存请求新的内存。

3. 第二级:mcentral (中央缓存)

mcentral 是共享的资源池,用于在不同的 mcache 之间平衡和分发内存。系统中每种大小类别(SizeClass)都有一个对应的 mcentral 实例。

关键特性: mcentral 负责管理所有该 SizeClass 的 mspan 列表。当 mcache 缺页时,它会从 mcentral 索取一个带有空闲对象的 mspan

虽然 mcentral 是共享的,因此需要加锁保护,但由于只有在 mcache 耗尽时才会访问它,所以锁竞争的频率极低。

// 抽象概念:mcentral 结构 (每种 SizeClass 共享一个)
type mcentral struct {
    lock    mutex
    sizeclass int32
    // 拥有空闲对象的 mspan 列表
    nonempty mspanList 
    // 已被清空或等待 GC 的 mspan 列表
    empty    mspanList 
}

4. mspan:内存管理的基本单元

mspan 是 Go 内存分配器的核心抽象。它代表了一块连续的、已被切分成固定大小对象的内存页

一个 mspan 总是只服务于一个特定的 SizeClass。例如,一个 mspan 可能被切割成许多 64字节的对象,另一个 mspan 可能被切割成许多 1KB 的对象。

// mspan 结构简化版
type mspan struct {
    next, prev *mspan    // 用于连接 mspanList (如 mcentral 中的 nonempty/empty)
    startAddr  uintptr   // span 起始地址
    npages     uintptr   // span 占用的页数
    // ... 还有许多字段用于追踪哪些对象已被分配
}

mcachemcentral 拿走一个 mspan 时,它实际上是拿走了一个已经被结构化、准备好分配的内存块。一旦该 mspan 被添加到 mcache 中,mcache 对其的操作就是无锁的。

5. 第三级:堆 (Heap)

如果 mcentral 发现自己的 nonempty 列表中没有可用的 mspan,它会向 Go 运行时堆(mheap)请求一批新的操作系统页,然后将这些页切分、初始化为一个或多个新的 mspan,并将其添加到自己的 nonempty 列表中。

大对象处理: 任何大于 32KB 的对象,由于无法放入任何 SizeClass 的 mspan 中,会直接向 mheap 请求连续的页来分配。这些大对象分配需要获取全局锁,但它们占总分配量的比例很小。

总结

Go通过 mcachemcentralmspan 的巧妙配合,实现了高效的内存分配:

  1. 最小化锁竞争: 绝大多数(98%以上)的小对象分配都在本地 mcache 上完成,无需任何锁。
  2. 效率和均衡: mspan 保证了每次从中央缓存获取的都是一大批可以立即使用的、同类型的小对象,减少了与中央锁交互的次数。
  3. 大对象管理: 大对象直接从堆中分配,避开 mcachemcentral 的小对象路径,保持了分配路径的简洁性。
【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Go 内存分配器详解:从 mspan 布局看三级缓存如何减少锁竞争压力
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址