TCMalloc:线程缓存Malloc以及tcmalloc与ptmalloc性能对比

目录

动机

用法

总览

小对象分配

大对象分配

跨度

解除分配

小型物品中央free清单

线程缓存的垃圾收集

性能说明

PTMalloc2单元测试

注意事项

推荐阅读


Paul Menage的Sanjay Ghemawat <opensource@google.com>

 

动机


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,只需通过“ -ltcmalloc”链接器标志将tcmalloc链接到您的应用程序中。

您可以通过使用LD_PRELOAD在未自行编译的应用程序中使用tcmalloc:

$LD_PRELOAD ="/usr/lib/libtcmalloc.so"

LD_PRELOAD很棘手,我们不一定推荐这种使用方式。

TCMalloc还包括堆检查器 和堆分析器

如果您希望链接一个不包含堆分析器和检查器的TCMalloc版本(也许可以减小静态二进制文件的二进制大小),则可以libtcmalloc_minimal 改为链接。

 

总览


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

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

可以将一页纸雕刻成一系列小物件,每个物件大小均相等。例如,一页(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)我们从中央页面分配器分配一系列页面。(2)将行程分成一组该大小级别的对象。(3)将新对象放置在中央空闲列表上。(4)和以前一样,将其中一些对象移动到线程本地空闲列表中。

 

大对象分配


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

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

如果k长度为>的页面运行满足页面分配k,则将运行的其余部分重新插入到页面堆中的相应空闲列表中。

 

跨度


TCMalloc管理的堆由一组页面组成。连续页面的运行由一个Span对象表示。范围可以分配,也可以免费。如果为空,则跨度是页面堆链接列表中的条目之一。如果已分配,则它要么是已移交给应用程序的大对象,要么是已分成多个小对象序列的一系列页面。如果拆分为小对象,则将对象的大小级别记录在跨度中。

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

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

 

解除分配


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

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

 

小型物品中央free清单


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

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

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

 

线程缓存的垃圾收集


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

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

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

 

性能说明


PTMalloc2单元测试


PTMalloc2软件包(现在是glibc的一部分)包含一个单元测试程序t-test1.c。这会派生多个线程,并在每个线程中执行一系列分配和释放。线程仅通过内存分配器中的同步进行通信。

t-test1(包含在google-perftools / tests / tcmalloc中,并编译为ptmalloc_unittest1)在不同数量的线程(1-20)和最大分配大小(64字节-32KB)下运行。这些测试使用RedHat 9的Linux glibc-2.3.2在启用超线程的2.4GHz双Xeon系统上运行,每个测试中每个线程执行一百万次操作。在每种情况下,测试均正常运行一次,并使用LD_PRELOAD = libtcmalloc.so运行一次。

下图显示了针对几种不同指标的TCMalloc与PTMalloc2的性能。首先,对于不同数量的线程,每秒经过的总操作数(百万)与最大分配大小之比。t-test1.times.txt中提供了用于生成这些图形的原始数据(“时间”实用程序的输出)。

  • TCMalloc比PTMalloc2具有更一致的可扩展性-对于所有线程数大于1的情况,小分配可达到约7-9百万个操作/秒,大分配可降低至约200万个操作/秒。单线程的情况显然是异常的,因为它只能使单个处理器保持繁忙,因此可以实现更少的运算/秒。PTMalloc2的每秒操作数差异更大-对于较小的分配,峰值约为400万次操作/秒,对于较大的分配,则降至<100万次/秒。
  • 在大多数情况下,尤其是对于小型分配,TCMalloc比PTMalloc2快。在TCMalloc中,线程之间的争用问题不大。
  • 随着分配大小的增加,TCMalloc的性能会下降。这是因为当每个线程高速缓存达到阈值(默认为2MB)时,将对其进行垃圾回收。使用较大的分配大小,在垃圾被垃圾回收之前,可以将较少的对象存储在缓存中。
  • 最大分配大小为〜32K时,TCMalloc性能显着下降。较大尺寸时,性能下降的速度较慢。这是由于每个线程缓存中对象的最大大小为32K。对于大于此tcmalloc的对象,将从中央页堆分配。

接下来,CPU的每秒操作数(百万)与线程数的关系(最大分配大小为64字节-128 KB)。

在这里,我们再次看到,TCMalloc比PTMalloc2更一致,更高效。对于最大分配大小小于32K的情况,TCMalloc通常使用大量线程来实现每秒约2-250万个操作,而PTMalloc通常可以实现每秒0.5-1百万个操作,在很多情况下可以实现很多小于这个数字。超过32K最大分配大小后,TCMalloc的CPU时间下降到每秒1-150万,而对于大量线程,PTMalloc的下降几乎为零(即,使用PTMalloc时,大量的CPU时间被消耗掉,等待大量的锁定时间)多线程情况)。

 

注意事项


对于某些系统,TCMalloc可能无法在未与libpthread.so(或OS上的等效文件)链接的应用程序上正常工作。它应该可以在使用glibc 2.3的Linux上运行,但是尚未测试其他OS / libc组合。

TCMalloc可能比其他malloc占用更多的内存,尽管它往往没有其他malloc可能发生的大量崩溃。特别是,在启动时,TCMalloc分配大约6 MB的内存。推出一个专门的版本,以牺牲一点点速度来提高空间效率,这很容易。

TCMalloc当前不向系统返回任何内存。

不要尝试将TCMalloc加载到正在运行的二进制文件中(例如,在Java程序中使用JNI)。二进制文件将使用系统malloc分配一些对象,并可能尝试将它们传递给TCMalloc进行释放。TCMalloc将无法处理此类对象。

 

推荐阅读


jemalloc简介

图解tcmalloc内存分配器

TCMalloc:线程缓存Malloc以及tcmalloc与ptmalloc性能对比

ptmalloc、tcmalloc与jemalloc内存分配器对比分析

使用jemalloc在Go中进行手动内存管理

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值