TCMalloc详细图解

在讲述TCMalloc的实现的大致过程之前,我们先来看一下TCMalloc的整体图解。

TCMalloc的整体图解

在这里插入图片描述
在 TcMalloc中由三个部分组成,分别是ThreadCacheCentralCachePageheap。我们接下来来简单介绍一下TCMalloc和这三个部分的结构实现过程

简介

TCMalloc 是Google 开发的内存分配器,在不少项目中都有使用,例如在Golang中就是用了类似的算法进行内存分配。它具有现代化内存分配器的基本特征:对抗内存碎片、在多核处理器能够scale。并且它的内存分配速度是glibc2.3中实现的malloc的数倍。
注: glibc是gnu发布的libc库,也即c运行库。glibc是linux系统中最底层的api(应用程序开发接口)

TCMalloc的著名 是由于它的性能是在是太高了,在高并发(多线程方面)实现较好的内存管理。而且TCMalloc是优化C++写的多线程应用(并发的情况更要比glibc2.3快)。

这篇文章主要就是以看图为主,主要是图解

如何分配定长的记录

在这里插入图片描述
首先是基本问题,如何分配定长的记录?例如,我们有一个Page的内存,大小为4kb(4*1024字节),现在要以N字节为单位进行分配。为了简化问题,就以16字节作为单位进行分配。

解法有很多,比如, bitmap。 4kb / 16 /8 = 32, 用32字节做bitmap即可, 实现也相当简单。

但是处于最大化内存利用率的目的,我们是用另一种经典的方式,freelist。将4kb的内存划分为16字节的单元,每一个单元的前8个字节作为节点指针,指向下一个单元。初始化的时候把所有的指针都指向下一个单元;分配时,从链表头分配一个对象出去;释放时,插入到链表(头插)。

由于链表指针直接分配在待分配内存中,因此不需要额外的内存开销,而且分配速度也是相当快。

如何分配变长记录?

定长记录的问题很简单,但如何分配变长记录的。对此,我们把问题划归成多种定长记录的分配问题

在这里插入图片描述
我们把所有的变长记录进行“取整”,例如分配7字节,就分配到8字节,31字节分配到32字节,得到多种规格的定长记录。这里带来了内部内存碎片化的问题,即分配出去的空间不会被完全被利用,有一定的浪费。为了减少内部碎片,分配规则按照8,16,31,48,64,80这样子来。注意到,这里并不是简单的使用2的幂级数,内存碎片化会相当的严重,分配65字节,实际会分配到128字节,接近50%的内存碎片。而按照这里的分配规则,只会分配80字节,一定程度上的减轻了问题。

扫描二维码关注公众号,回复: 8749572 查看本文章

大的对象如何分配

上面讲的是基于Page,分配小于Page的对象,但是如果分配的对象大于一个Page,我们就需要用多个Page来分配了

在这里插入图片描述

这里就提出了Span的概念,也就是多个连续的Page会组成一个Span,在Span中记录起始的Page的编号,以及Page的数量。

分配对象时,大的对象直接分配给Span,小的对象从Span中分配。

Span如何分配?

对于Span的管理,我们可以如法炮制:

图8

还是使用多种定长的Page来实现变长的Page的分配,初始时只有128Page的Span,如果要分配一个Page的Span,就要把这个Span分裂成两个,1+127, 再把127再记录下来。对于Span的回收,需要考虑Span的合并问题,否则再分配回收多次之后,就只剩很小的Span了,也就带来了外部碎片的问题。

为此,释放Span时,需要将前后空闲的Span进行合并,当然合并的前提是他们的Page要连续。
问题来了 , 如何知道前后的Span在哪里?

从Page到Span

在这里插入图片描述
最简单的一种方式,用一个数组记录每一个Page所属的Span,而数组的索引就是Page ID。这种方式虽然简介明了,但是在Page比较少的时候会有很大的空间浪费。

为此,我么可以使用RadixTree这种数据结构,用较少的空间开销,和不错的速度来完成这件事:
在这里插入图片描述
乍一看可能有点懵,这个跟 RadixTree 能扯上关系吗?可以把 RadixTree 理解成压缩过的前缀树(trie),所谓压缩,就是在一条路径上的节点都只有一个子节点,就把这条路径合并到父节点去,因此内部节点最少会有 Radix 个字节点。具体的分析可以参考一下 wikipedia 。

实现时,可以通过一定的空间换来时间,也就是减少层数,比如说3层。每层都是一个数组,用一个地址的前 1/3 的bit 索引数组,剩下的 bit 对下一层进行寻址。实际的寻址也可以非常快。

PageHeap

在这里,我们已经实现了PageHeap, 对所有的Page进行管理:
在这里插入图片描述

全局对象分配

既然有了基于Page的对象分配,和Page本身的管理,我们把他们串起来就可以得到一个简单的内存分配器了:

在这里插入图片描述
按照我们之前设计的,每一种规格的对象,都有不同的Span进行分配;每种规则的对象都有一个独立的内存分配单元:CentralCache。在一个CentralCache内存,我们用链表把所有的Span组织起来,每次需要分配时就找一个Span从中分配一个Object;当没有空闲的Span时,就从PageHeap申请Span。

看起来基本满足功能,但是这里有一个严重的问题,在多线程的场景下,所有的线程都从CentralCache分配的话,竞争可能相当的激烈。

ThreadCache

到这里ThreadCache便呼之欲出了:
在这里插入图片描述
每一个线程都有一个线程局部的ThreadCache, 按照不同的规格,维护了对象的链表;如果ThreadCache的对象不够了,就从CentralCache进行批量分配;如果CentralCache依然没有,就从PageHeap中申请Span;如果PageHeap没有合适的Page可以申请Span,就只能从操作系统中申请了。

在释放内存的时候,ThreadCache依然遵循批量释放的策略,对象积累到一定的程度就释放给CentralCache; CentralCache发现一个Span的内存完全释放了,就可以把这个Span归还给PageHeap;PageHeap发现一批连续的Page都释放了,就可以归还给操作系统了。

至此, TCMalloc的大体结构都呈现在我们眼前了。

总结

这里是用图解的方式简单的讲述了TCMalloc的基本结构,如何减少内部碎片,如何减少外部碎片,如何用伙伴算法进行内存合并,如何使用单链表进行内存分配,如何通过线程局部的方式提高拓展性。
图解的方式更便于我们去理解TCMalloc的结构设计的意义,一层层的拨开分析,更容易让我们去理解为什么TCMalloc可以支持高并发下内存的管理。
当然这篇文章也是参照知乎上面的一篇,再稍加一点自己的补充 ^ _ ^

发布了55 篇原创文章 · 获赞 12 · 访问量 5254

猜你喜欢

转载自blog.csdn.net/weixin_43939593/article/details/103427074