記憶體管理(三)tcmalloc1 記憶體分配及原始碼剖析
本來打算花一天時間看看tcmalloc就算結束了。但是在網上找部落格的時候發現,100個人有101鐘不同的解釋,完全沒有公論。一怒之下,剖原始碼!
tcmalloc是對ptmalloc的升級版。和ptmalloc相比,tcmalloc對於小塊記憶體的速度要比ptmalloc快得多,並且相對於每一個記憶體塊分配都需要8B,tcmalloc對於細節的優化ptmalloc做得好。tcmalloc對於頁的理解和主流不太一樣,他是以8K作為一個頁。由於tcmalloc是第三方庫,所以要使用的話就得自行下載安裝。下面談談tcmalloc的分配。
tcmalloc將記憶體請求分為大記憶體和小記憶體。大記憶體
小記憶體分配與回收
tcmalloc內的維護了86個分配器,這些分配器通過一個連結串列free list來管理。但是他們相鄰差並不是類似ptmalloc的相等,而是以8,16,32這樣的數列分開。在分配記憶體的時候,他會先把大小對映到對應的空間集合,接著從連結串列裡找出比申請記憶體大的最小空間,找到以後直接返回給呼叫者。如果沒找到,就會向中央堆申請記憶體
Free list的結構如下:
前面提到,執行緒會在cache裡找不到的情況下向中央堆裡申請記憶體。在從中央堆申請記憶體的函式裡,我們可以看到。函式為了保證執行緒安全會先上鎖,上了鎖以後就會從快取區ECEntry裡看有沒有符合條件的記憶體。如果有,就會返回。如果沒有,就會呼叫FretchFromSpanSafe
從執行緒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頁。最後一項代表著需要更多頁面的小空間。如果依舊不夠用,就會向系統申請。具體的方法函式見上文。