您的位置:首页 > 其它

内存管理(三)tcmalloc1 内存分配及源码剖析

2017-05-07 15:24 405 查看
    本来打算花一天时间看看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里找不到的情况下向中央堆里申请内存。在从中央堆申请内存的函数里,我们可以
d2fc
看到。函数为了保证线程安全会先上锁,上了锁以后就会从缓存区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页。最后一项代表着需要更多页面的小空间。如果依旧不够用,就会向系统申请。具体的方法函数见上文。

 

 

 

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: