1. 程式人生 > >記憶體管理(三)tcmalloc1 記憶體分配及原始碼剖析

記憶體管理(三)tcmalloc1 記憶體分配及原始碼剖析

    本來打算花一天時間看看tcmalloc就算結束了。但是在網上找部落格的時候發現,100個人有101鐘不同的解釋,完全沒有公論。一怒之下,剖原始碼!

        tcmalloc是對ptmalloc的升級版。和ptmalloc相比,tcmalloc對於小塊記憶體的速度要比ptmalloc快得多,並且相對於每一個記憶體塊分配都需要8Btcmalloc對於細節的優化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頁。最後一項代表著需要更多頁面的小空間。如果依舊不夠用,就會向系統申請。具體的方法函式見上文。