初衷
TCMalloc的速度比glibc 2.3 malloc和其他我测试过的malloc速度快。ptmalloc在2.8GHz的P4处理器上执行一对分配和释放(小对象)内存的操作需要约300纳秒。TCMalloc的做同样的操作仅需要约50纳秒。对于一个内存分配实现来讲,速度非常重要,因为如果malloc不够快,应用开发者倾向于在malloc之上开发自定义的freelist。者可能导致额外的复杂度和更多的内存失业,除非应用开发者非常谨慎的适当调整freelist的大小并从freelist清除空闲的对象。
TCMalloc对于多线程程序也会减少锁竞争。对于小对象,几乎是0竞争。对于大对象,TCMalloc试图尝试使用粒度和高效的自旋锁。ptmalloc2通过使用每个线程的arenas来减少锁的使用。但是最大的问题在于ptmalloc2对基于线程的arenas的使用。在ptmalloc2内存永远不会从一个arena移动到另外一个arena中。这会导致巨大的空间浪费。例如,在Google应用程序中,第一个阶段将为数据结构分配约300MB内存。当第一阶段结束后,第二阶段将在同样的地址空间上被启动。这第二阶段被分配到一个不同的arena,这个阶段不会服用第一阶段留下的任何内存,并将增加300MB的内存占用。这样的问题也发生在其他应用程序中。
TCMalloc的另外一个好处是小对象空间的高效使用。例如:8N字节的对象可能被分配一个8N*1.01字节大小的内存。仅仅有1%的超额占用,ptmalloc2给每个对象分配一个4字节的的头,将大小四舍五入为一个8字节的倍数,最后使用16N个字节。
使用
想使用TCMalloc,只需要使用-ltcmalloc链接标志来链接应用程序。
你可以不用亲自编译就失业tcmalloc,只需要设置环境变量
export LD_PRELOAD="/user/lib/libtcmalloc.so"
使用这个环境变量有点tricky,我们不推荐这么用。
TCMalloc还包含一个heap checker和heap profile。
如果您希望链接不包含堆分析器和检查器的TCMalloc版本(可能会减小静态二进制文件的二进制大小),则可以改用libtcmalloc_minimal进行链接。
总览
TCMalloc给每个线程分配一个thread-local cache。小对象的分配通过这个thread-local cache来完成。根据需要,对象从central cache的数据结构中移动到thread-local cache中。周期性的垃圾回收被用于从thread-local cache迁移内存回到central cache中。
TCMalloc区别对待<=32K的小对象和大对象。大对象直接从central heap中使用页面级别的分配器来分配。例如:一个大对象总是页对齐的并且占用整数个页。
可以将一页划分为一系列小对象,每个小对象大小均相等。例如,一页客服分为32个128字节的小对象。
小对象的分配
每个小对象的大小映射到约170个大小等级中。例如,所有961-1024字节的对象都被取值为1024,大小等级的被划分为8字节,16字节,32字节等等等。最多有256个等级。
一个thread cache包含单个相同大小级别对象的链表。

当分配内存给一个小对象时:
1)把小对象的大小跟大小等级中的做映射找到相应的大小级别
2)在thread-cache的对应的空闲链表寻找
3) 如果空闲链表不为空,我们移除其第一个元素 并将其返回。
如果遵循的是上面的快速路径,tcmalloc根本不需要锁。这显著的提高的内存分配的效率,因为lock/unlock需要耗费约100纳秒。
如果链表是空的,我们从central cache中的空闲list找对应大小级别的的内存。2)把对应的内存放到thread-cache中,返回这个内存对象给应用程序。
如果central cache的空闲链表都是空的,我们从central page allocator页中分配页,把页划分成这个级别大小的对象的集合。
将新的对象集合放到central cache的空闲链表中。4,像前面一样,吧这些对象移动进入thread-local的空闲列表。
大对象分配(Large Object Allocation)
大对象(大于32K)被向上取整数被的页大小(4K),其内存分配由Central page heap来处理。Central page heap也是一个空闲链表数组。数组第i(i<256)个元素是一个包含i页大小的空闲链表,第256个元素是长度大于等于256页空闲链表。

一个需要分配k页内存的请求可以通过第k个链表来满足分配,如果这个链表是空的,我们会查找下一个空闲链表。最终我们会找到最后一个空闲链表。如果这样还找不到,我们最终从系统获取内存(使用sbrk,mmap或通过映射/dev/mem)
如果k页的内存分配被满足时 长度大于k,这个运行会重新插回到原定的空闲列表中。
Spans
TCMalloc管理的堆包含一组页。连续页的运行由span对象来表示。
span可以被分配,页可以被释放。如果释放,span就是页堆链表的一个元素。如果被分配,他可以是一个已经移交给应用程序的大对象,也可是已分成多个小对象序列的一个序列。如果被分成小对象,对象的尺寸级别将会被记录在span中。
一个以页码为索引的中央数组可以被用于确定一个页属于哪个span。例如,下面的span属于2个页,spanb占了1页,span c占了5页,span d占了三页

一个32位地址空间可以容纳2^20 个4K页,所以,这个中央数组占用4MB的空间是可以被接受的。在64位机器上,我们使用一个3级的radix 树来取代数组来映射页码到相应span的指针。
反分配。
当一个对象被解除分配时,我们计算他的页码,并在中央数组里查到相应的span对象。span来告诉我们那个对象是否是small的,以及如果是small的尺寸级别是多少。如果对象是small,我们把它插入相应的当前线程cache的空闲链表中。如果线程cache超过了预先确定的大小,我们运行一个gc来将没有用的对象从线程缓存移除。
如果对象很大,span告诉我们页的范围被对象里的覆盖。假定这个范围是[p,q].我们也会去查到页码为p-1和p+1的span。如果这些邻居span是空闲的,我们把他们跟[p,q]合并到span。最终产生的span被插入到页面堆中的相应空闲列表中。
给小对象的中央空闲列表
正如之前所说,我们为每个尺寸级别都保持一个中央空闲列表。每个中央空闲列表被组织成一个2级数据结构,spans的集合和每个span的空闲对象的链表
一个对象从中央空闲列表分配处理的过程是通过移除span链表的的第一个元素来实现。一个对象返回到中央空闲列表是通过把它加会到容纳他的span的链表中来实现的。如果链表长度等于span中总的小对象树,这个span现在就在完全闲置的,被返回到页堆中。
Thread Caches的GC
当所有对象的组合大小超过2MB时,thread cache就会被GC。GC的阈值是随着线程数字段增加而降低的,这样我们不要在具有很多线程的的程序中浪费过多内存。
我们遍历cache中所有的空闲列表并把其中一些数量的对象从空闲列表移动到相应的中央列表中。
具体移动的数量是一个per-list low-water-mark记录的字上次gc以来的最小长度来决定。请注意,我们可以在上一次垃圾回收时将列表缩短L个对象,而无需对中央列表进行任何额外的访问。 我们将过去的历史用作将来访问的预测器,并将L / 2对象从线程高速缓存可用列表移至相应的中央可用列表。 该算法具有很好的属性,即如果某个线程停止使用特定大小,则该大小的所有对象将快速从线程缓存移到中央空闲列表,其他线程可以在该列表中使用它们。
网友评论