Go性能优化(2) Go的内存管理与优化 | 青训营笔记

292 阅读5分钟

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

如果有错误和其他意见,麻烦留言指正💖

本文是Go内存管理及优化的整理总结

image.png

TCMalloc

概述

Go的内存分配基于TCMalloc(Thread Cache Malloc)。虽然随着版本迭代,Go的内存管理与TCMalloc的差异不断扩大,但其核心思想是类似的,因此为了更好理解Go的内存管理,需要简单了解一下 TCMalloc。

TCMalloc 有两大特点分块缓存

就像它的名称一样,TCMalloc的核心就是为每个线程(Thread)分配一块缓存(Cache)

这样做有两点好处:

  1. 只在预分配缓存时进行系统调用,后续线程申请小内存时直接从缓存分配,不用经过系统调用。而系统调用的开销是很大的。

    为什么系统调用相对于普通函数调用的开销大?

    我的理解是系统调用涉及用户态和内核态切换,需要保存寄存器信息,还有页表切换,这其中还会导致cpu缓存失效等等影响。

    而普通函数调用只涉及压栈的操作。

  2. 我们知道多线程并发访问同一块内存区域时需要加锁,而TCMalloc为每个线程单独分配一块缓存,是不同的地址空间,因而就无需加锁,减少了锁的开销。

《详解Go语言的内存模型及堆的分配管理》这篇文章将快速分配内存分为三个层次:

  • 第一层次:引入虚拟内存,让内存的并发访问的粒度从多进程级别,降低到多线程级别。
  • 第二层次:预分配缓存,减少系统调用开销。
  • 第三层次:多个线程同时申请小内存时,从各自缓存分配,无需加锁,把内存并发访问的粒度进一步降低了。

个人感想:计算机体系中处处可以看到缓存的身影,我觉得这是种通过复用来提高性能的思想,像各种池,如连接池本质也是一种缓存。平时可以思考一下如何利用这种缓存的思想来提高程序的性能。

名词概念简介

这些数据结构的设计体现了分块的思想。

  • Page

    与操作系统一样,TCMalloc堆内存的管理也是以页为单位。但是两者的大小不一定相同。一般在x64下 Page 为8kB。

  • Span

    Span 是一组连续的 Page ,是TCMalloc 中内存管理的基本单位。

  • ThreadCache

    ThreadCache是每个线程各自的 Cache,一个 Cache 包含多个空闲内存块链表,每个链表连接的都是内存块,同一个链表上内存块的大小是相同的,也可以说按内存块大小,给内存块分了个类,这样可以根据申请的内存大小,快速从合适的链表选择空闲内存块。由于每个线程有自己的ThreadCache,所以ThreadCache访问是无锁的

  • CentralCache

    CentralCache是所有线程共享的缓存,也是保存的空闲内存块链表,链表的数量与ThreadCache中链表数量相同。

    当ThreadCache的内存块不足时,可以从CentralCache获取内存块;当ThreadCache内存块过多时,可以放回CentralCache。

    由于CentralCache是共享的,所以它的访问是要加锁的

  • PageHeap

    PageHeap是对堆内存的抽象,PageHeap存的也是若干链表,链表保存的是Span。

    当CentralCache的内存不足时,会从PageHeap获取空闲的内存Span,然后把1个Span拆成若干内存块,添加到对应大小的链表中并分配内存;当CentralCache的内存过多时,会把空闲的内存块放回PageHeap中。


Go 内存分配

概述

基于 TCMalloc,核心也是分块和缓存。

名词概念简述

  • Page

    与 TCMalloc 的 Page 相同

  • mspan

    与 TCMalloc 的 span 类似,它是golang内存管理中的基本单位。根据对象是否包含指针,将内存块分为scan 和 noscan两种。

  • mcache

    与 ThreadCache 类似,区别在于TCMalloc是每个线程拥有1个ThreadCache,Go是每个 P 拥有一个 mcache。因为在Go程序中,当前最多有GOMAXPROCS个线程在运行,所以最多需要GOMAXPROCS个mcache就可以保证各线程对mcache的无锁访问。

    P(processor)代表上下文,可以认为是cpu

    M(work thread)代表工作线程

    G(groutine)代表协程

  • mcentral

    与CentralCache类似,是线程共享的缓存。

    当 mcache 中的 mspan 分配完毕,向 mcentral申请带有未分配块的mspan。

    当 mspan中没有分配的对象,mspan 会被缓存在 mcentral中, 而不是立刻释放并归还给 OS。

  • mheap

    与PageHeap类似,是堆内存的抽象。

image.png


Go 内存管理的问题

  • 对于很多线上业务,对象分配是非常高频的操作:每秒GB级别

  • 小对象占比比较高

  • Go 内存分配比较耗时

    • 分配路径长: g ->m -> p -> mcache -> span -> memory block -> return pointer
    • 通过 pprof 工具观察到对象分配函数调用频繁。

Balanced GC

Balanced GC 是字节跳动针对上述问题的优化方案。

特点

  • goroutine allocation buffer(GBA):为每个 g 都绑定一大块内存(1 KB)
  • GAB 用于 nosan 类型的小对象(<128B)分配
  • bump-the-pointer :指针碰撞风格的对象分配,高效简单

基于之前提到的小对象频繁分配的问题,Balanced GC 做的本质就是将多个小对象的分配合并成一次大对象(GBA)的分配

问题

GAB 中只要有一个小对象存活,这个GAB就不会被内存释放,导致内存的延迟释放。

解决方案

使用copying GC 算法解决,当GAB 的大小超于一定阈值时,将GAB中存活的对象拷贝到另外分配的GAB中,然后释放原先的GAB。


参考及推荐阅读

《Go 语言原本》7.1内存分配设计原则 主要是源码分析

《详解Go语言的内存模型及堆的分配管理》 深入浅出非常详细得讲解了内存管理。