作者:Sanjay Ghemawat, Paul Menage
翻译:ShiningRay
动机TCMalloc要比glibc 2.3的 TCMalloc也减少了多线程程序中的锁争用情况。对于小对象,几乎已经达到了零争用。对于大对象,TCMalloc尝试使用粒度较好和有效的自旋锁。 ptmalloc同样是通过使用每线程各自的场地来减少锁争用,但是ptmalloc2使用每线程场地有一个很大的问题。在ptmalloc2中,内存可 能会从一个场地移动到另一个。这有可能导致大量空间被浪费。例如,在一个Google的应用中,第一阶段可能会为其URL标准化的数据结构分配大约 300MB内存。当第一阶段结束后,第二阶段将从同样的地址空间开始。如果第二个阶段被安排到了一个与第一阶段什?用的场地不同的场地,这个阶段不会复用 任何第一阶段留下的的内存,并会给地址空间添加另外一个300MB。类似的内存爆炸问题也可以在其他的应用中看到。 TCMalloc的另一个好处是小对象的空间最优表现形式。例如,分配N个8字节对象可能要使用大约 使用要使用TCMalloc,只要将tcmalloc通过“-ltcmalloc”链接器标志接入你的应用即可。 你也可以通过使用LD_PRELOAD在不是你自己编译的应用中使用tcmalloc: $ LD_PRELOAD="/usr/lib/libtcmalloc.so" LD_PRELOAD比较讨巧,我们也不十分推荐这种用法。 如果你更想链接不包含堆测量器和检查器的TCMalloc版本(比如可能为了减少静态二进制文件的大小),你可以接入 概览TCMalloc给每个线程分配了一个线程局部缓存。小分配可以直接由线程局部缓存来满足。需要的话,会将对象从中央数据结构移动到线程局部缓存中,同时定期的垃圾收集将用于把内存从线程局部缓存迁移回中央数据结构中。 TCMalloc将尺寸小于<= 连续的一些页面可以被分割为一系列小对象,而他们的大小都相同。例如,一个连续的页面(4K)可以被划分为32个128字节的对象。 小对象的分配每个小对象的大小都会被映射到170个可分配的尺寸类别中的一个。例如,在分配961到1024字节时,都会归整为1024字节。尺寸类别这样隔 开:较小的尺寸相差8字节,较大的尺寸相差16字节,再大一点的尺寸差32字节,如此类推。最大的间隔(对于尺寸 >= ~2K的)是256字节。 一个线程缓存对每个尺寸类都包含了一个自由对象的单向链表。 当分配一个小对象时:
如果自由列表为空:
如果中央自由列表也为空:(1) 我们从中央页分配器分配了一连串页面。(2) 将他们分割成该尺寸类的一系列对象。(4) 像前面一样,将部分对象移入线程局部的自由列表中。 大对象的分配一个大对象的尺寸(> 32K)会被除以一个页面尺寸(4K)并取整(大于结果的最小整数),同时是由中央页面堆来处理的。中央页面堆又是一个自由列表的阵列。对于
如果 跨度(Span)TCMalloc管理的堆由一系列页面组成。连续的页面由一个“跨度”( 由页面号索引的中央数组可以用于找到某个页面所属的跨度。例如,下面的跨度a占据了2个页面,跨度b占据了1个页面,跨度c占据了5个页面最后跨度d占据了3个页面。 在一个32位的地址空间中,中央阵列由一个2层的基数树来表示,其中根包含了32个条目,每个叶包含了 215个条目(一个32为地址空间包含了 220个 4K 页面,所以这里树的第一层则是用25整除220个页面)。这就导致了中央阵列的初始内存使用需要128KB空间(215*4字节),看上去还是可以接受的。 在64位机器上,我们将使用一个3层的基数树。 解除分配当一个对象被解除分配时,我们先计算他的页面号并在中央阵列中查找对应的跨度对象。该跨度会告诉我们该对象是大是小,如果它是小对象的话尺寸类别是 什么。如果是小对象的话,我们将其插入到当前线程的线程缓存中对应的自由列表中。如果线程缓存现在超过了某个预定的大小(默认为2MB),我们便运行垃圾 收集器将未使用的对象从线程缓存中移入中央自由列表。 如果该对象是大对象的话,跨度会告诉我们该对象覆盖的页面的范围。假设该范围是 小对象的中央自由列表就像前面提过的一样,我们为每一个尺寸类别设置了一个中央自由列表。每个中央自由列表由两层数据结构来组成:一系列跨度和每个跨度一个自由对象的链表。 通过从某个跨度中移除第一个条目来从中央自由列表分配一个对象。(如果所有的跨度里只有空链表,那么首先从中央页面堆中分配一个尺寸合适的跨度。) 一个对象可以通过将其添加到他包含的跨度的链表中来返回到中央自由列表中。如果链表长度现在等于跨度中所有小对象的数量,那么该跨度就是完全自由的了,就会被返回到页面堆中。 线程缓存的垃圾收集某个线程缓存当缓存中所有对象的总共大小超过2MB的时候,会对他进行垃圾收集。垃圾收集阈值会自动根据线程数量的增加而减少,这样就不会因为程序有大量线程而过度浪费内存。 我们会遍历缓存中所有的自由列表并且将一定数量的对象从自由列表移到对于得中央列表中。 从某个自由列表中移除的对象的数量是通过使用一个每列表的低水位线 性能备注PTMalloc2单元测试PTMalloc2包(现在已经是glibc的一部分了)包含了一个单元测试程序
下面的图像显示了TCMalloc对比PTMalloc2在不同的衡量指标下的性能。首先,现实每秒全部操作(百万)以及最大分配尺寸,针对不同数量的线程。用来生产这些图像的原始数据(
下面,CPU时间的每秒操作数(百万)以及线程数量的图像,最大分配尺寸64B~128KB。 这次我们再一次看到TCMalloc要比PTMalloc2更连续也更高效。对于<32K的最大分配尺寸,TCMalloc在大线程数的情况 下典型地达到了CPU时间每秒约0.5~1百万操作,同时PTMalloc通常达到了CPU时间每秒约0.5~1百万,还有很多情况下要比这个数字小很 多。在32K最大分配尺寸之上,TCMalloc下降到了每CPU时间秒1~1.5百万操作,同时PTMalloc对于大线程数降到几乎只有零(也就是, 使用PTMalloc,在高度多线程的情况下,很多CPU时间被浪费在轮流等待锁定上了)。 注意对于某些系统,TCMalloc可能无法与没有链接 TCMalloc可能要比其他malloc版本在某种程度上更吃内存,(但是倾向于不会有其他malloc版本中可能出现的爆发性增长。)尤其是在启动时TCMalloc会分配大约240KB的内部内存。 不要试图将TCMalloc载入到一个运行中的二进制程序中(例如,在Java中使用JNI)。二进制程序已经使用系统malloc分配了一些对象,并会尝试将它们传递到TCMalloc进行解除分配。TCMalloc是无法处理这种对象的。 |
|