mimalloc剖析

Linkwk7發表於2019-07-16

mimalloc是微軟最近開源的一個malloc實現,其實驗資料表明相比於jemalloc、tcmalloc等實現大約快了10%。其通過將空閒塊列表(Free List)進行分片(Sharding)來保證分配的記憶體有更好的空間的區域性性,從而提升效能。在mimalloc中一共進行了4次Free List的Sharding。接下來我們會分別介紹這4個Free List的Sharding的位置以及其為什麼要進行Free List的Sharding。


在Mimalloc頁中進行的Free List的Sharding

在其他的記憶體分配器的實現中,一般會為每一類大小的物件保留一個Free List,隨著記憶體的不斷分配與釋放,這個列表中的物件可能散佈在整個地址空間中,因此記憶體分配的區域性性較差。而在mimalloc中,其通過將記憶體分頁,每個頁負責一個特定大小的記憶體的分配,每個頁有一個Free List,因此記憶體分配的空間區域性性較好。
其他記憶體分配器的Free List
 

mimalloc的Free List

Local Free List

mimalloc希望能夠限制記憶體分配與記憶體釋放在最差情況下的時間消耗,如果上層應用需要釋放一個非常大的結構體,其可能需要遞迴的釋放一些子結構體,因而可能帶來非常大的時間消耗。因而在koka與lean語言中,其執行時系統使用引用計數來追蹤各種資料,然後保留一個延遲釋放的佇列(deferred decrement list),當每次釋放的記憶體塊超過一定數量後就將剩餘需要釋放的記憶體塊加入該佇列,在之後需要釋放的時候再進行釋放。那麼下一個需要確定的問題是什麼時候再去從該佇列中釋放記憶體。從延遲釋放佇列中繼續進行釋放的時機最好應當是記憶體分配器需要更多空間的時候,因此這需要記憶體分配器與上層應用的協作。

在mimalloc中,其提供一個回撥函式,當進行了一定次數記憶體的分配與釋放後會主動呼叫該回撥函式來通知上層應用。mimalloc在實現時檢測當前進行記憶體分配的頁的Free List是否為空,如果為空則呼叫該回撥,但是為了避免用於一直不斷的分配與釋放記憶體,導致Free List一直不為空,而導致回撥函式一直得不到回撥。因此mimalloc將Free List第二次進行Sharding,將其分為Free List與Local Free List。

當記憶體在進行分配時會從對應頁的Free List中取得記憶體塊,而釋放時會將記憶體塊加入Local Free List中,因而在進行一定次數的記憶體分配後,Free List必定為空,此時可以進行deferred free的回撥函式的呼叫。

Thread Free List

在mimalloc中每個堆都是一個Thread Local的變數,而每次進行記憶體分配時,其均會從這個Thread Local的堆中進行記憶體的分配,而釋放時即可能從該執行緒中釋放也可能從其他執行緒中進行釋放。如果進行記憶體釋放的執行緒是該堆的擁有者,則其釋放的記憶體會加入到對應頁的Local Free List中,而由於還可能有其他的執行緒來釋放這些記憶體,因此mimalloc第三次進行Free List的Sharding,將Local Free List分為Local Free List與Thread Free List。在進行記憶體的釋放時,如果釋放的執行緒為記憶體塊對應堆的擁有著則將其加入Local Free List,否則利用CAS操作將其加入Thread Free List中。mimalloc通過這次分割來保證堆的所有者執行緒在自己的堆上進行記憶體的釋放是無鎖的,從而提升一些效能上的表現。

Full List

第四次的Free List的Sharding其實來自於mimalloc自身的實現,其記憶體分配的虛擬碼如下。由於在mimalloc中每個堆中都有一個陣列pages,該陣列中每個元素都是一個由相應大小的頁組成的佇列;同時還有一個pages_direct的陣列,該陣列中每個元素對應一個記憶體塊的大小型別,每個元素均為指向負責對應大小記憶體塊分配的頁的指標。因此mimalloc在進行記憶體分配時會首先從該陣列指向的頁中嘗試進行分配,如果分配失敗則呼叫malloc_generic,在該函式中會遍歷pages陣列中對應大小的佇列,此時如果對應的佇列中有很多頁均是滿的,且佇列很長那麼每次分配的時候都會進行佇列的遍歷,導致效能的損失。

void* malloc_small( size_t n ) {
  heap_t* heap = tlb;
  page_t* page = heap->pages_direct[(n+7)>>3];
  block_t* block = page->free;
  if (block==NULL) return malloc_generic(heap,n);
  page->free = block->next;
  page->used++;
  return block;
}

因此mimalloc構建了一個Full List,將所有已經沒有空閒空間的頁放入該佇列中,僅當該頁中有一些空閒空間被釋放後才會將其放回pages對應的佇列中。而在由於記憶體的釋放可能由對應堆的擁有者執行緒進行也可能由其他執行緒進行,因此需要一定的方式提醒對應的堆該頁已經有空閒塊了,同時為了避免使用鎖導致的開銷,mimalloc通過加入一個Thread Delayed Free List,如果一個頁處於Full List中,那麼在釋放時會將記憶體塊加入Thread Delayed Free List中,該佇列會在呼叫malloc_generic時進行檢測與清除(由於時Thread Local的堆,因此僅可能是擁有者來進行),因此此時僅需通過原子操作即可完成。那麼還有一個問題是當釋放記憶體的時候,其他執行緒如何知道是將記憶體塊加入Thread Free List中還是Thread Delayed Free List中。mimalloc通過設定NORMAL、DELAYED、DELAYING三種狀態來完成該操作。

總結

mimalloc通過將Free List進行分割,保證分配的記憶體具有較好的區域性性並避免了鎖的使用,從而獲得了更好的效能。文章如果有哪裡有問題,歡迎提出,對該專案感興趣的可以去看一下其倉庫1,或者參考這篇文章2

引用


相關文章