TCMalloc:线程缓存Malloc

Posted Wang-Junchao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCMalloc:线程缓存Malloc相关的知识,希望对你有一定的参考价值。

【博文目录>>>】 【项目地址>>>】


TCMalloc:线程缓存Malloc

本文翻译了TCMalloc : Thread-Caching Malloc最重要的部分,TCMalloc是go语言内存分配的基石,go内存分配置就是从TCMalloc演化而来。其余部分内容可参见原文。

动机

内存分配速度快。TCMalloc比我测试过的glibc 2.3 malloc(可称为ptmalloc2的独立库)和其他malloc更快。ptmalloc2在2.8 GHz P4上(用于小型对象)执行malloc / free对大约需要300纳秒。对于同一操作对,TCMalloc实现大约需要50纳秒。速度对于malloc实现很重要,因为如果malloc不够快,应用程序编写者倾向于在malloc之上编写自己的自定义空闲列表。除非应用程序编写者非常小心地适当调整可用列表的大小并从空闲列表中清除空闲对象,否则这可能导致额外的复杂性和更多的内存使用。

TCMalloc还减少了多线程程序的锁争用。对于小对象,竞争几乎为零。对于大对象,TCMalloc尝试使用细粒度且高效的自旋锁。ptmalloc2还通过使用每个线程的竞技场来减少锁争用,但是ptmalloc2使用每个线程的竞技场存在一个大问题。在ptmalloc2中,内存永远无法从一个领域移动到另一个领域。这会导致大量的空间浪费。例如,在一个Google应用程序中,第一阶段将为其数据结构分配大约300MB的内存。当第一阶段完成时,第二阶段将在相同的地址空间中开始。如果为第二阶段分配的舞台不同于第一阶段使用的舞台,此阶段将不重用第一阶段之后剩余的任何内存,并将在地址空间中再增加300MB。在其他应用程序中也发现了类似的内存爆炸问题。

TCMalloc的另一个好处是小对象的空间高效表示。例如,可以在使用大约8N * 1.01字节的空间时分配N个8字节的对象。即,空间开销为百分之一。ptmalloc2为每个对象使用一个四字节的标头,并且(我认为)将大小四舍五入为8个字节的倍数,最后使用16N字节结束。

##总览
TCMalloc为每个线程分配一个线程本地缓存。线程本地缓存满足小分配。根据需要将对象从中央数据结构移动到线程本地缓存中,并使用定期垃圾回收将内存从线程本地缓存迁移回中心数据结构中。

TCMalloc将大小小于等于32KB的对象(“小”对象)与大对象区别对待。使用页级分配器(一页就是4KB对齐内存区域)直接从中央堆分配大对象。即,大对象始终是页对齐的,并且占据整数页。

可以将一页纸拆分成一系列小对象,每个大小均相等。例如,一页(4K)可以分成32个对象,每个对象大小为128字节。

小对象分配

每个小对象大小映射到大约170个可分配大小类别之一。例如,在961到1024字节范围内的所有分配都被四舍五入为1024。大小类别是空间化的,以使小尺寸被8字节分隔,大尺寸被16字节分隔,更大尺寸被32字节分隔,依此类推。 。最大空间(对于大小> =〜2K)为256个字节。

线程缓存包含每个大小类别的空闲对象的单链列表。

当分配一个小对象时:

  • (1)我们将其大小映射到相应的大小类别。
  • (2)在线程高速缓存中的相应空闲列表中查找当前线程。
  • (3)如果空闲列表不为空,则从列表中删除第一个对象并返回它。当遵循此快速路径时,TCMalloc根本不会获得任何锁。因为在2.8 GHz Xeon上,锁定/解锁对大约花费100纳秒,所以这大大有助于加速分配。

如果空闲列表为空:

  • (1)我们从该大小类别的中央空闲列表中获取一堆对象(中央空闲列表由所有线程共享)。
  • (2)将它们放在线程本地空闲列表中。
  • (3)将新获取的对象之一返回给应用程序。

如果中央空闲列表也为空:

  • (1)我们从中央页面分配器分配“大量的页面”(a run of pages)。
  • (2)将“大量的页面”(the run)分成一组该大小级别的对象。
  • (3)将新对象放置在中央空闲列表上。
  • (4)和以前一样,将其中一些对象移动到线程本地空闲列表中。

大对象分配

大对象大小(> 32K)被四舍五入为页面大小(4K,即4K的整数倍),并由中央页面堆处理。中央页面堆也是一组空闲列表。对于i<256,第k条目是包含k个页面的运行的空闲列表。第256项是长度>=256个页面的免费空闲列表:

通过在k空闲列表中查找,可以满足分配k个页面要求。如果该空闲列表为空,则我们查找下一个空闲列表,依此类推。最终,如有必要,我们将查找最后一个空闲列表。如果失败,我们从系统中获取内存(使用sbrk,mmap或通过映射/dev/mem的一部分)。

如果k个页面的分配由长度大于k的大量页面满足(例如:需要4KB的页,但是分配了大量8KB的页),则将大量的其余部分重新插入到页面堆中的相应空闲列表中(即将8KB的空闲页面放到对应的8KB空闲列表)。

跨度(Span)

TCMalloc管理的堆由一组页面组成。大量的连续页由一个Span对象表示。Span要么是已分配的,要么是空闲的。如果是空闲的,则spna是页面堆链表(页面堆链表:page heap linked-list)中的条目之一。如果已分配,则它要么是已移交给应用程序的大对象,要么是已分成多个小对象序列的页面。如果拆分为小对象,则将对象的大小级别记录在跨度中。

由页码索引的中央数组可用于查找页面所属的跨度。例如,下面的跨度a占2页,跨度b占1页,跨度c占5页,跨度d占3页。

32位地址空间可容纳2^20 4K页,因此此中央数组占用4MB的空间,这似乎可以接受。在64位计算机上,我们使用3级基数树而不是数组来将页码映射到相应的跨度指针。

解除分配

释放对象后,我们将计算其页码并在中央数组中查找以找到相应的跨度对象。跨度告诉我们对象是否小,如果大小小则告诉我们大小级别。如果对象较小,则将其插入当前线程的线程缓存中的相应空闲列表中。如果线程缓存现在超过预定大小(默认为2MB),我们将运行垃圾回收器,将未使用的对象从线程缓存移至中央空闲列表。

如果对象很大,则跨度会告诉我们该对象包含的页面范围。假设范围是[p,q]。我们还将查找页面p-1和的跨度q+1。如果这些相邻跨度中的任意一个是空闲的,则将它们与[p,q]跨度合并。将结果跨度插入到页面堆中相应的空闲列表中。

小对象中央空闲列表

如前所述,我们为每个大小级别保留一个中央空闲列表。每个中央空闲列表被组织为两级数据结构:一组span,以及每个span的空闲对象的链表。

通过从某个span的链接列表中删除第一个条目,可以从中央空闲列表中分配一个对象。(如果所有span都具有空链接列表,则首先从中央页面堆中分配一个适当大小的范围。)

通过将对象添加到其包含span的链接列表中,可以将对象返还到中央空闲列表。如果链接列表的长度现在等于span围中小对象的总数,则该span现在是完全空闲的并返回到页面堆。

线程缓存的垃圾回收

当高速缓存中所有对象的总大小超过2MB时,便会垃圾回收线程高速缓存。随着线程数量的增加,垃圾回收阈值会自动降低,这样我们就不会在具有很多线程的程序中浪费过多的内存。

我们遍历缓存中的所有空闲列表,然后将一些对象从空闲列表移到相应的中央列表。

使用每个列表的低水位标记L确定要从空闲列表中移动的对象的数量。L记录自上次垃圾回收以来列表的最小长度。请注意,我们可以在上一个垃圾回收时通过对象L将列表缩短,而无需对中央列表进行任何额外的访问。我们使用过去的历史作为将来访问的预测,并将L/2对象从线程高速缓存可用列表移动到相应的中央空闲列表。该算法具有很好的属性,即如果某个线程停止使用特定大小,则该大小的所有对象将快速从线程缓存移至中央空闲列表,其他线程可以在该列表中使用它们。

以上是关于TCMalloc:线程缓存Malloc的主要内容,如果未能解决你的问题,请参考以下文章

自我实现tcmalloc的项目简化版本

高并发内存池的介绍

高并发内存池的介绍

高并发内存池的介绍

Go内存管理

使用tcmalloc替换系统的malloc