本来打算花一天时间看看tcmalloc就算结束了。但是在网上找博客的时候发现,100个人有101钟不同的解释,完全没有公论。一怒之下,剖源码!
tcmalloc是对ptmalloc的升级版。和ptmalloc相比,tcmalloc对于小块内存的速度要比ptmalloc快得多,并且相对于每一个内存块分配都需要8B,tcmalloc对于细节的优化ptmalloc做得好。tcmalloc对于页的理解和主流不太一样,他是以8K作为一个页。由于tcmalloc是第三方库,所以要使用的话就得自行下载安装。下面谈谈tcmalloc的分配。
tcmalloc将内存请求分为大内存和小内存。大内存>32K,小内存<=32K。如果要优化多线程的内存分配,tcmalloc为每一个线程创建了一个私有的cache,在处理小内存的请求时会先从这一个cache来查找内存块。由于这是每个线程私有的空间,所以在处理申请的时候就不需要进行加锁解锁操作。处理大内存的时候会从中央堆来分配。下面挨个来讨论。
小内存分配与回收
tcmalloc内的维护了86个分配器,这些分配器通过一个链表free list来管理。但是他们相邻差并不是类似ptmalloc的相等,而是以8,16,32这样的数列分开。在分配内存的时候,他会先把大小映射到对应的空间集合,接着从链表里找出比申请内存大的最小空间,找到以后直接返回给调用者。如果没找到,就会向中央堆申请内存,然后填充到对应的集合里,接着放到线程本地free list,接着就修改free list的最大可能长度这个属性。每次分配都会增大最大可能长度,最后会返回一个给调用者。如果中央堆也没有内存了,就会向系统申请一个页,切割一块比申请内存大的最小空间出来分配,并且记录。
Free list的结构如下:
前面提到,线程会在cache里找不到的情况下向中央堆里申请内存。在从中央堆申请内存的函数里,我们可以看到。函数为了保证线程安全会先上锁,上了锁以后就会从缓存区ECEntry里看有没有符合条件的内存。如果有,就会返回。如果没有,就会调用FretchFromSpanSafe函数来从某个span里获得内存并返回。
从线程free list 获得内存的代码如下:
inline void * ThreadCache:: Allocate(size_t size, size_t cl )
{ //并没有加锁
ASSERT(size <= kMaxSize);
ASSERT(size == Static:: sizemap()->ByteSizeForClass (cl));
FreeList* list = &list_[ cl];
if (list ->empty())
{
return FetchFromCentralCache (cl, size); //如果从free list找不到,那么就去中央堆里找
}
size_ -= size ;
return list ->Pop(); //把最上面那个给调用者
}
在线程自身的cache里分配是不用加锁的,但是如果需要向中央堆来申请的话。由于中央堆是共享的,所以要加自旋锁。
从中央堆里获得内存的代码如下:
int CentralFreeList ::RemoveRange( void **start , void ** end, int N) //返回的是获得内存块的数目,N代表需要多少个内存块
{
ASSERT( N > 0);
lock_. Lock(); //上锁
if ( N == Static ::sizemap()-> num_objects_to_move(size_class_ ) && used_slots_ > 0) //缓存区里有足够的内存块
{
int slot = --used_slots_;
ASSERT(slot >= 0);
TCEntry *entry = &tc_slots_[ slot]; //从TCEntry缓存数列里查找
* start = entry ->head;
* end = entry ->tail;
lock_.Unlock ();
return N ;
}
……
Int result = 0; //准备直接从span里取,取的多少就把result置为多少
Int *start = NULL; //先置为空,如果找不到,就直接返回空
Int *end = NULL;
tail = FetchFromSpansSafe(); //找不到就只能去span里获得了
if ( tail != NULL )
{
head = tail ;
result = 1;
while (result < N)
{
void *t = FetchFromSpans();
if (!t ) break;
result++;
}
}
lock_. Unlock(); //解锁
*start = head;
*end = tail;
return result;
}
Span标示一段连续的内存页,他可以作为节点和其他span串起来,也可以把内存页化为一个个objects供分配给小内存。他的定义如下:
Struct Span
{
PageID start; //starting page number
Length length; //number of pages in span
Span* next; //use when in link list
Span* prev; //use when in link list
Void* objects; //linked list of free objects
}
如果之前查找缓存区失败,就会尝试从span获得内存:
void* CentralFreeList ::FetchFromSpansSafe()
{
void * t = FetchFromSpans (); //先试着从没分配完的页里获得内存
if (! t)
{
Populate(); //从页堆(也就是多个页连在一起的heap)申请内存
t = FetchFromSpans ();
}
return t;
}
先试着从FetchFromSpans获取:
void* CentralFreeList ::FetchFromSpans()
{
if ( tcmalloc::DLL_IsEmpty (&nonempty_))
return NULL ; //如果说 非空的span(也就是被使用过但是内存没用完的span)没有了,直接返回
Span* span = nonempty_ .next;
ASSERT( span->objects != NULL);
span-> refcount++; //找到的这个span还能用,先在引用数上+1
void* result = span ->objects;
span-> objects = *(reinterpret_cast <void**>( result));
if ( span->objects == NULL) //如果这次引用完了就没空间了,那就放到一个empty里去
{
tcmalloc::DLL_Remove (span);
tcmalloc::DLL_Prepend (&empty_, span);
Event(span , 'E', 0);
}
counter_--;
return result;
}
如果span里找不到,那就只能去pageheap里看看了:
void CentralFreeList ::Populate()
{
lock_. Unlock(); //先把中央堆的锁给解了,用不着
const size_t npages = Static:: sizemap()->class_to_pages (size_class_);
Span* span;
{
SpinLockHolder h(Static ::pageheap_lock()); //给pageheap锁了
span = Static ::pageheap()-> New(npages );
if (span ) Static:: pageheap()->RegisterSizeClass (span, size_class_);
}
if ( span == NULL ) //如果span是空的话就得在日志文件里记录错误返回了。
{
……
}
ASSERT( span->length == npages);
for ( int i = 0; i < npages; i ++)
{
Static::pageheap ()->CacheSizeClass( span->start + i, size_class_); //把页切割成size_class的大小
}
……
char* limit = ptr + (npages << kPageShift);
int num = 0;
while ( ptr + size <= limit)
{
……num++; //累加获得的块的大小
}
tcmalloc:: DLL_Prepend(&nonempty_ , span);//放到缓存区
++num_spans_;
counter_ += num;
}
如果一些都失败了,就会考虑free的内存和unmmaped的内存可能有足够多的小块页内存。于是会把free的内存全部释放掉。释放过程起始也会有和邻近的页内存合并的过程。这样就达到了吧所有可用的小块内存合并的内存。如果还是找不到(强迫症)就会扩充堆的大小(PageHeap::GrowHeap),GrowHeap从系统获取指定大小的内存(以页对齐)
大内存分配
如果申请的内存大于32K,就会向中央堆里申请内存。中央堆里的内存单位是页,也是通过一个数组来管理。一共有256个元素。前255是代表着从1页到255页。最后一项代表着需要更多页面的小空间。如果依旧不够用,就会向系统申请。具体的方法函数见上文。