前面提到,在jemalloc,每次分配,都是以4M为基准,而且内存块地址也是以4M对齐,这导致经常要先分配8M-PAGESIZE大小的内存,再回收未被使用的内存。见函数chunk_alloc_mmap_slow, alloc_size = size + alignment - PAGE ,约等于8M。tcmalloc要好点,是以系统页对齐就可以了,这个保证成功率要高很多,只要有空闲内存,基本都能成功。
但不管怎么说,由于内存地址对齐,所以,根据对齐参数,能够直接得到chunk的地址,作为索引。对于tcmalloc来说,由于不是以4M对齐,所以要稍微复杂一点,得到的是Span地址,根据Span再推演到整个PAGE。总而言之,我们能够根据任何一个内存地址,通过对齐算法,得到一个PageID,那么如何得到这个Page的描述信息呢?不论是jemalloc还是tcmalloc,都是使用基数树。
在tcmalloc中,对应于32位机器,硬性使用二层基数树,对于64位机器,使用的是三层基数树。根据定义typedef MapSelector<kAddressBits>::Type PageMap;其中kAddressBits = 8 * sizeof(void *) 在64位下,等于64。那么在template <int BITS> class TCMalloc_PageMap3中,BITS = 64 - kPageShift = 64 - 13 = 51。其中每层需要有INTERIOR_BITS = (BITS + 2) / 3 = 17个比特,也就说基数树的节点需要有sizeof(void *) << INTERIOR_BITS,在64位机器上,需要分配1M内存。
在jemalloc在,对层数的计算要科学一点。首先限定节点的总字节数,
#if (LG_SIZEOF_PTR == 2) # define RTREE_NODESIZE (1U << 14) #else # define RTREE_NODESIZE CACHELINE #endif
bits_per_level = ffs(pow2_ceil((RTREE_NODESIZE / sizeof(void *)))) - 1;pow2_ceil(size)返回n,其中size = 2^n ; ffs(size)返回高位为1的比特位置。也就说,依据RTREE_NODESIZE算出基数树每层的比特数。这样,每次分配的时候,总是能控制住节点的字节数。
关于jemalloc和tcmalloc的这种方式孰优孰劣,其实很难定论。32位下,都差不多。但是64位下tcmalloc的层数要低,64位占的空间较多,但查找要快。jemalloc层数要高,占空间要少,查找要慢。不过,由于jemalloc的chunk描述是跟chunk的数据区放在一起,基本是不需要查找,反而在这个地方更快了。
另外一点,需要注意,基数树的节点,在分配之后,都没有被释放的。虽然很奇怪,但想想,却很正常。如果需要释放,那么插入、删除、查找,都需要互斥来保护内存边界。
在jemalloc中,chunks_rtree作为全局变量,管理所有的chunk。而tcmalloc则是 通过静态变量
static PageHeap* pageheap() { return pageheap_; }
和
static CentralFreeListPadded* central_cache() { return central_cache_; }
供全局使用的。