使用TCMalloc(Thread-Caching Malloc)当内存管理器
以下翻译自tcmalloc.html
动机
- 相对于glibc2.3 malloc, 在2.8GHz P4上,ptmalloc2需要大概300ns执行一个malloc/free 操作,TCMalloc只需要50ns
- 多线程时,可减少锁竞争. 小对象基本都是无锁,对于大对象,ptmalloc2也使用每个线程一个arena,但是在不同的arena上分配大对象时内存不会复用而会重新去申请内存
- TCMalloc在小对象上也是空间高效的,对N个8字节对象只需要N81.01bytes,而ptmalloc2每个对象使用4字节头,也就是12字节,对齐到8字节的倍数,从而使用了16N个字节
预览
TCMalloc为每个线程分配一个线程级别(thread-local)的cache.小的对象申请都可以直接从这个cache中获取,需要的话也可以将central的内存移到cache中,或者从cache中回收对象到central中
TCMalloc将小于32K的数据称为小对象(small objects).大对象直接通过页级别(page-level,以4k(41024byte)为单位对齐)的内存分配器从central heap中申请n页.
每页也可以切分成多个小对象,比如一页可切分为32个128字节的对象(32128byte)
小对象分配
小对象可分为大约170种size-class,最大间距为256个字节
申请流程如下:
- 根据对象的大小找到合适的size-class
- 从当前线程的cache的空闲列表(free list)中查找,如果找到了就从空闲列表中移除并返回,这里甚至都用不到锁,这里也能稍微加速申请,毕竟一次lock/unlock对在2.8GHz Xeon处理器上也需要大约100ns的时间
- 如果空闲列表为空列表,则从central的空闲列表中(这里都是切割好的,可能是以前cache中回收的)查找这个size-class(注意:central空闲列表是所有线程共享的),如果找到了,就放到cache的空闲列表中,并从刚获取到的这个数组中返回一个对象
- 如果central中空闲列表也为空,则直接通过mmap或将heap中切分等获取连续的页,切割成对应size-class的对象列表,放入central的空闲列表中,然后再将一部分送到cache的空闲列表中返回
大对象分配
对于超过32K的对象,会先将大小对齐到页大小(4k)的倍数,然后交由central page heap来处理,
不同于空闲列表,page heap并未将页切分成对象数组,而是直接将其分成了256中页class,n<=255,构成了n页的一个列表(页为单位),剩余的同意放到256这个页class中.
分配时直接找到能满足的n页类型空闲列表,如果找不到则直接n++,找稍微大一些的空闲列表,如果失败了则从系统中通过mmap获取,如果找到的页过大,则会进行切分,将剩余的页插入到合适的空闲列表中去
Spans
TCMalloc通过连续的页称用span来管理heap. span有已申请或者空闲两种状态.
如果空闲,则span可以看做是heap某个链表的入口,可以看做是多个页打包在一起
如果已申请,则可能是一个使用中的大对象,或者切分成一系列的小对象列表供小对象使用
一个根据页编号的数组(central array)查到一个页是属于哪个span
一个32位的地址空间可以容纳2的20次方个4k的页面,这个central array占用了4MB的空间,在64位机器上,使用的是3级基数树(3-level radix tree,类似于二叉树)来查找对应的页
内存回收(Deallocation)
当某个对象要回收时,我们计算它的页编号(page number),并从central array中找到对应的span,这个span中能知道这个对象是不是小对象还有小对象对应的size-class,如果是小对象,则将其插入到对应cache中的空闲列表中去,如果这个线程的cache超过了一个预设的值(默认是2MB),将会出发垃圾回收将未使用的对象移入central的空闲列表中去.
central小对象的空闲列表
我们保存了每个size-class的空闲列表,每个空闲列表都有两级数据结构:一系列的span,每个span一个空闲对象的链表.
对象的申请通过从空闲列表中移除一个并返回(如果没有空闲链表,则会先从page heap中去申请)
对象的回收通过将其插入到它对应span的空闲列表中,如果链表长度已经等于span的最大对象的数量,则改span全部空闲,会将其回收到page heap中去
cache的垃圾回收
当这个cache中所有对象的长度超过预设的值时(默认2MB,随着线程数的增多,会自动降低这个阀值以免浪费)
会遍历所有cache的空闲列表,然后将一定数量的对象回收到对应的central的空闲列表中去
被回收的数量会根据每个列表中的L(low-water-mark)决定,L记录着上次回收时的最小长度.根据L会预测未来的使用量,并将L/2个对象移入central的空闲列表中去。这个算法的好处是当某个线程停止使用某个特定sizeclass的对象时,快速的移入central的空闲列表中,由于central的空闲列表是共享的,可直接供其他线程使用(较PTMalloc的优点之一)
### 性能
- TCMalloc在大部分的申请大小上性能都较PTMaloc2更好,原因是在cache中的话会减少冲突
- 随着分配尺寸的增大,由于cache垃圾回收的限制,导致大量的垃圾回收,很少有对象能存储在cache中
- 当超过32k时性能下降变慢,这是由于32k之后算大对象,都是存储在page heap中了
申请大小和每秒操作数关系图:
线程数与每秒操作数关系图