# mimalloc 原始碼分析
> 以最新版本 2.0.1 為例
> - `dev`: development branch for mimalloc v1.
> - `dev-slice`: development branch for mimalloc v2 with a new algorithm for managing internal mimalloc pages.
## Types
### `mi_block`
```c
// The free lists use encoded next fields
// (Only actually encodes when MI_ENCODED_FREELIST is defined.)
typedef uintptr_t mi_encoded_t;
// free lists contain blocks
typedef struct mi_block_s {
mi_encoded_t next;
} mi_block_t;
```
用來回傳使用者真正需要的記憶體空間位置,不需要紀錄 block 的大小是因為 `mi_block_t` 都由 `mi_page_t` 來管理,而每個 `mi_page_t` 都只對應一個固定的大小 `xblock_size` 來決定 `mi_block_t` 可使用的範圍。
每個尚未分配給使用者的 `mi_block_t` 皆為一個 linked list,其使用 `next` 欄位紀錄在同個 `mi_page_t` 內的下個未使用的 `mi_block_t`。特別的是當定義 `MI_ENCODED_FREELIST` 時,`next` 不是紀錄記憶體位置,而是會透過編碼轉換 (類似 XOR) 來增加安全性。
故使用者透過 `mi_malloc(size_t size)` 獲得的空間,一般情況下,內部為該 block 在 `mi_page_t` 時存放的 `next` 之內容,而當 `MI_SECURE` 開啟時,則會特別將 `next` 清空,防止資料洩漏:
```c
extern inline void* _mi_page_malloc(mi_heap_t* heap, mi_page_t* page, size_t size) mi_attr_noexcept {
...
#elif (MI_SECURE!=0)
block->next = 0; // don't leak internal data
#endif
...
}
```
### `mi_page`
```c
typedef struct mi_page_s {
// "owned" by the segment
uint32_t slice_count; // slices in this page (0 if not a page)
uint32_t slice_offset; // distance from the actual page data slice (0 if a page)
uint8_t is_reset : 1; // `true` if the page memory was reset
uint8_t is_committed : 1; // `true` if the page virtual memory is committed
uint8_t is_zero_init : 1; // `true` if the page was zero initialized
// layout like this to optimize access in `mi_malloc` and `mi_free`
uint16_t capacity; // number of blocks committed, must be the first field, see `segment.c:page_clear`
uint16_t reserved; // number of blocks reserved in memory
mi_page_flags_t flags; // `in_full` and `has_aligned` flags (8 bits)
uint8_t is_zero : 1; // `true` if the blocks in the free list are zero initialized
uint8_t retire_expire : 7; // expiration count for retired blocks
mi_block_t* free; // list of available free blocks (`malloc` allocates from this list)
#ifdef MI_ENCODE_FREELIST
uintptr_t keys[2]; // two random keys to encode the free lists (see `_mi_block_next`)
#endif
uint32_t used; // number of blocks in use (including blocks in `local_free` and `thread_free`)
uint32_t xblock_size; // size available in each block (always `>0`)
mi_block_t* local_free; // list of deferred free blocks by this thread (migrates to `free`)
_Atomic(mi_thread_free_t) xthread_free; // list of deferred free blocks freed by other threads
_Atomic(uintptr_t) xheap;
struct mi_page_s* next; // next page owned by this thread with the same `block_size`
struct mi_page_s* prev; // previous page owned by this thread with the same `block_size`
// 64-bit 9 words, 32-bit 12 words, (+2 for secure)
#if MI_INTPTR_SIZE==8
uintptr_t padding[1];
#endif
} mi_page_t;
```
* `mi_block_t* free`:此 page 中,尚未被使用者使用的 block
* `mi_block_t* local_free`:對應論文中的 local free list
* `mi_thread_free_t* xthread_free`:對應論文中的 thread free list
另外 `xblock_size` 為此 page 內每個 block 可佔有的空間,每個存有相同大小的 block 之 page 也會由 `next` 與 `prev` 欄位連結成 doubly linked list。
### `mi_segment`
```c
typedef mi_page_t mi_slice_t;
typedef struct mi_segment_s {
size_t memid; // memory id for arena allocation
bool mem_is_pinned; // `true` if we cannot decommit/reset/protect in this memory (i.e. when allocated using large OS pages)
bool mem_is_large; // in large/huge os pages?
bool mem_is_committed; // `true` if the whole segment is eagerly committed
bool allow_decommit;
mi_msecs_t decommit_expire;
mi_commit_mask_t decommit_mask;
mi_commit_mask_t commit_mask;
_Atomic(struct mi_segment_s*) abandoned_next;
// from here is zero initialized
struct mi_segment_s* next; // the list of freed segments in the cache (must be first field, see `segment.c:mi_segment_init`)
size_t abandoned; // abandoned pages (i.e. the original owning thread stopped) (`abandoned <= used`)
size_t abandoned_visits; // count how often this segment is visited in the abandoned list (to force reclaim it it is too long)
size_t used; // count of pages in use
uintptr_t cookie; // verify addresses in debug mode: `mi_ptr_cookie(segment) == segment->cookie`
size_t segment_slices; // for huge segments this may be different from `MI_SLICES_PER_SEGMENT`
size_t segment_info_slices; // initial slices we are using segment info and possible guard pages.
// layout like this to optimize access in `mi_free`
mi_segment_kind_t kind;
_Atomic(uintptr_t) thread_id; // unique id of the thread owning this segment
size_t slice_entries; // entries in the `slices` array, at most `MI_SLICES_PER_SEGMENT`
mi_slice_t slices[MI_SLICES_PER_SEGMENT];
} mi_segment_t;
```
`mi_segment` 為實際上 mimalloc 主要從 OS 上獲得的空間,管理著所需要的 page (`mi_slice_t slices[MI_SLICES_PER_SEGMENT`)。其中 `slices[0]` 被用來儲存 segment 中的額外資訊。
### `mi_heap`
```c
// A heap owns a set of pages.
struct mi_heap_s {
mi_tld_t* tld;
mi_page_t* pages_free_direct[MI_PAGES_DIRECT]; // optimize: array where every entry points a page with possibly free blocks in the corresponding queue for that size.
mi_page_queue_t pages[MI_BIN_FULL + 1]; // queue of pages for each size class (or "bin")
_Atomic(mi_block_t*) thread_delayed_free;
uintptr_t thread_id; // thread this heap belongs too
uintptr_t cookie; // random cookie to verify pointers (see `_mi_ptr_cookie`)
uintptr_t keys[2]; // two random keys used to encode the `thread_delayed_free` list
mi_random_ctx_t random; // random number context used for secure allocation
size_t page_count; // total number of pages in the `pages` queues.
size_t page_retired_min; // smallest retired index (retired pages are fully free, but still in the page queues)
size_t page_retired_max; // largest retired index into the `pages` array.
mi_heap_t* next; // list of heaps per thread
bool no_reclaim; // `true` if this heap should not reclaim abandoned pages
};
```
每個執行緒上都有一個 `mi_heap`,用來紀錄目前該執行緒所儲存的 page,放入在 `mi_page_queue_t pages` 當中,其中相同大小的 page 被放入在同個 queue 之中,而 `pages[MI_BIN_FULL - 1]` 特別被用作放置存有特別大的 block 之 page,`pages[MI_BIN_FULL]` 則用來當作存放論文中提到的 full list。
### `mi_tld`
```c
// Thread local data
struct mi_tld_s {
unsigned long long heartbeat; // monotonic heartbeat count
bool recurse; // true if deferred was called; used to prevent infinite recursion.
mi_heap_t* heap_backing; // backing heap of this thread (cannot be deleted)
mi_heap_t* heaps; // list of heaps in this thread (so we can abandon all when the thread terminates)
mi_segments_tld_t segments; // segment tld
mi_os_tld_t os; // os tld
mi_stats_t stats; // statistics
};
```
`mi_tid` 存放此執行緒的相關運行資料,包括 `mi_heap` 的資料、作業系統的資料等等,`mi_segments_tld_t segment` 為目前此執行緒中與 segment 相關的資料,其內部 `mi_tld_t->segments->spans[MI_SEGMENT_BIN_MAX+1]` 成員存放此執行緒可供使用的所有 page 用來加快存取效率。
## Function
### `mi_malloc`
```c
extern inline mi_decl_restrict void* mi_malloc(size_t size) mi_attr_noexcept {
return mi_heap_malloc(mi_get_default_heap(), size);
}
```
透過 `mi_malloc` 分配記憶體,其中 `mi_get_default_heap` 會回傳 `_mi_heap_deafult1`,取得目前執行緒所管理的 heap。
* 如果是主執行緒 (main thread),則執行緒管理的 `_mi_heap_default` 在程式執行時已被設定為 `_mi_heap_main`
* 如果是非主行緒,則執行緒管理的 `_mi_heap_default` 初始化為 `_mi_heap_empty`,表示此 heap 尚未分配空間,在其後 `mi_malloc` 的過程中分配新的空間給予 `_meap_default` 來當作此執行緒可用的 heap。
```c
extern inline mi_decl_restrict void* mi_heap_malloc(mi_heap_t* heap, size_t size) mi_attr_noexcept {
if (mi_likely(size <= MI_SMALL_SIZE_MAX)) {
return mi_heap_malloc_small(heap, size);
}
else {
void* const p = _mi_malloc_generic(heap, size + MI_PADDING_SIZE); // note: size can overflow but it is detected in malloc_generic
return p;
}
}
```
依照需要的 block 大小與 `MI_SMALL_SIZE_MAX (128)` 決定使用 `mi_malloc` 的 fast path 還是 slow path。
* fast path
```c
static inline mi_page_t* _mi_heap_get_free_small_page(mi_heap_t* heap, size_t size) {
const size_t idx = _mi_wsize_from_size(size);
return heap->pages_free_direct[idx];
}
// allocate a small block
extern inline mi_decl_restrict void* mi_heap_malloc_small(mi_heap_t* heap, size_t size) mi_attr_noexcept {
mi_page_t* page = _mi_heap_get_free_small_page(heap,size + MI_PADDING_SIZE);
void* p = _mi_page_malloc(heap, page, size + MI_PADDING_SIZE);
return p;
}
```
`heap->page_free_direct` 為存有 130 個 `mi_page_t` 的陣列,儲存 size 較小的 page,fast path 會從 `heap->pages_free_direct` 內直接取得需要配置空間的 page,而不需要逐一走訪 page queue 中尋找可用的 page。
```c
// Fast allocation in a page: just pop from the free list.
// Fall back to generic allocation only if the list is empty.
extern inline void* _mi_page_malloc(mi_heap_t* heap, mi_page_t* page, size_t size) mi_attr_noexcept {
mi_block_t* const block = page->free;
if (mi_unlikely(block == NULL)) {
return _mi_malloc_generic(heap, size);
}
// pop from the free list
page->used++;
page->free = mi_block_next(page, block);
return block;
}
```
`page->free` 指向此 page 中尚未被用到的 block 空間,故分配時只需從該空間取出可用的 block,當 page 內沒有可用的 block 時,呼叫 `_mi_malloc_generic` 轉移到 slow path 做 malloc。
* slow path
當分配所需空間 > `MI_SMALL_SIZE_MAX` 、page 為空 (`page == _mi_page_empty`) 或是 page 沒有可用空間 (`page->free == NULL`) 時,呼叫 `_mi_malloc_generic` 函式尋找新的 page:
```c
// Generic allocation routine if the fast path (`alloc.c:mi_page_malloc`) does not succeed.
// Note: in debug mode the size includes MI_PADDING_SIZE and might have overflowed.
void* _mi_malloc_generic(mi_heap_t* heap, size_t size) mi_attr_noexcept
{
// initialize if necessary
if (mi_unlikely(!mi_heap_is_initialized(heap))) {
mi_thread_init(); // calls `_mi_heap_init` in turn
heap = mi_get_default_heap();
if (mi_unlikely(!mi_heap_is_initialized(heap))) {
return NULL;
}
}
// call potential deferred free routines
_mi_deferred_free(heap, false);
// free delayed frees from other threads
_mi_heap_delayed_free(heap);
// find (or allocate) a page of the right size
mi_page_t* page = mi_find_page(heap, size);
if (mi_unlikely(page == NULL)) { // first time out of memory, try to collect and retry the allocation once more
mi_heap_collect(heap, true /* force */);
page = mi_find_page(heap, size);
}
if (mi_unlikely(page == NULL)) { // out of memory
const size_t req_size = size - MI_PADDING_SIZE; // correct for padding_size in case of an overflow on `size`
_mi_error_message(ENOMEM, "unable to allocate memory (%zu bytes)\n", req_size);
return NULL;
}
// and try again, this time succeeding! (i.e. this should never recurse)
return _mi_page_malloc(heap, page, size);
}
```
`_mi_malloc_generic` 會先檢查 heap 是否初始化,如果呼叫此函式的執行緒為第一次呼叫 `mi_malloc` 且不為主執行緒時,`mi_heap_is_initialized` 會回傳 false 進入 `mi_thread_init` 做初始化。
`_mi_deferred_free` 呼叫使用者自訂的釋放函式,`_mi_heap_delayed_free` 則釋放 thread delayed free list,其下方再介紹。
`mi_find_page` 會嘗試尋找可用的 page,可用的 page 可能是重新分配一個 segment 來取得,或是從已分配的 segment 中尋找。當 `mi_find_page` 回傳 `NULL` 時,表示 out of memory,透過 `mi_heap_collect` 嘗試回收更多的空間並在試一次。最後再重新進入 `_mi_page_malloc` 從取得的 page 回傳可使用的 block 空間。
```c
// Allocate a page
// Note: in debug mode the size includes MI_PADDING_SIZE and might have overflowed.
static mi_page_t* mi_find_page(mi_heap_t* heap, size_t size) mi_attr_noexcept {
// huge allocation?
const size_t req_size = size - MI_PADDING_SIZE; // correct for padding_size in case of an overflow on `size`
if (mi_unlikely(req_size > (MI_MEDIUM_OBJ_SIZE_MAX - MI_PADDING_SIZE) )) {
if (mi_unlikely(req_size > PTRDIFF_MAX)) { // we don't allocate more than PTRDIFF_MAX (see <https://sourceware.org/ml/libc-announce/2019/msg00001.html>)
_mi_error_message(EOVERFLOW, "allocation request is too large (%zu bytes)\n", req_size);
return NULL;
}
else {
return mi_large_huge_page_alloc(heap,size);
}
}
else {
// otherwise find a page with free blocks in our size segregated queues
return mi_find_free_page(heap, size);
}
}
```
`mi_find_page` 用來尋找或分配 page,依據 `MI_MEDIUM_OBJ_SIZE_MAX` 分類,當需要分配的空間大於此數值時,此時每個 page 皆只包含一個 block,其大小為需要的空間,故直接執行 `mi_large_huge_page_alloc` 從 segment 分配一個新的 page,而不需要再從 page queue 中搜尋;當需要分配的空間大小不超過 `MI_MEDIUM_OBJ_SIZE_MAX` 時,可用的 block 空間可能會在已分配的 page 中找到,故從對應的 page queue 中尋找,如果找不到時,也會跟 `mi_large_huge_page_alloc` 呼叫路徑相同從 segment 中重新分配一個新的 page。
```c
static inline mi_page_queue_t* mi_page_queue(const mi_heap_t* heap, size_t size) {
return &((mi_heap_t*)heap)->pages[_mi_bin(size)];
}
// Find a page with free blocks of `size`.
static inline mi_page_t* mi_find_free_page(mi_heap_t* heap, size_t size) {
mi_page_queue_t* pq = mi_page_queue(heap,size);
mi_page_t* page = pq->first;
if (page != NULL) {
_mi_page_free_collect(page,false);
if (mi_page_immediate_available(page)) {
page->retire_expire = 0;
return page; // fast path
}
}
return mi_page_queue_find_free_ex(heap, pq, true);
}
```
`mi_page_queue` 會找到 size 對應的 page queue,如果該 page queue 之 `first` 的 free list 不為空,則表示該 page 可用,直接回傳,否則呼叫 `mi_page_queue_find_free_ex` 在 page queue 中搜尋 free list 不為空的 page:
```c
// Find a page with free blocks of `page->block_size`.
static mi_page_t* mi_page_queue_find_free_ex(mi_heap_t* heap, mi_page_queue_t* pq, bool first_try)
{
// search through the pages in "next fit" order
mi_page_t* page = pq->first;
while (page != NULL)
{
mi_page_t* next = page->next; // remember next
// 0. collect freed blocks by us and other threads
_mi_page_free_collect(page, false);
// 1. if the page contains free blocks, we are done
if (mi_page_immediate_available(page)) {
break; // pick this one
}
// 2. Try to extend
if (page->capacity < page->reserved) {
mi_page_extend_free(heap, page, heap->tld);
break;
}
// 3. If the page is completely full, move it to the `mi_pages_full`
// queue so we don't visit long-lived pages too often.
mi_page_to_full(page, pq);
page = next;
} // for each page
if (page == NULL) {
_mi_heap_collect_retired(heap, false); // perhaps make a page available?
page = mi_page_fresh(heap, pq);
if (page == NULL && first_try) {
// out-of-memory _or_ an abandoned page with free blocks was reclaimed, try once again
page = mi_page_queue_find_free_ex(heap, pq, false);
}
}
else {
page->retire_expire = 0;
}
return page;
}
```
如果 page queue 不為空,則逐一走訪 queue 並嘗試釋放空間,如果在走訪的過程中找到空的 free block 時便直接回傳該 page。當 page queue 中的所有 page 都沒有可用的 block 時,先呼叫 `_mi_heap_collect_retired` 回收空間,接著 `mi_page_fresh` 會嘗試從 segment 中獲得一塊新的 page 空間使用。
```c
// Get a fresh page to use
static mi_page_t* mi_page_fresh(mi_heap_t* heap, mi_page_queue_t* pq) {
mi_page_t* page = mi_page_fresh_alloc(heap, pq, pq->block_size);
if (page==NULL) return NULL;
return page;
}
// allocate a fresh page from a segment
static mi_page_t* mi_page_fresh_alloc(mi_heap_t* heap, mi_page_queue_t* pq, size_t block_size) {
mi_page_t* page = _mi_segment_page_alloc(heap, block_size, &heap->tld->segments, &heap->tld->os);
if (page == NULL) {
// this may be out-of-memory, or an abandoned page was reclaimed (and in our queue)
return NULL;
}
mi_page_init(heap, page, block_size, heap->tld);
if (pq!=NULL) mi_page_queue_push(heap, pq, page); // huge pages use pq==NULL
return page;
}
```
`_mi_segment_page_alloc` 為實際從 segment 分配 page 的函式,當找到可用的 page 時,通過 `_mi_page_init` 初始化,並將該 page 放到先前所選擇對應大小的 page queue 中。
```c
mi_page_t* _mi_segment_page_alloc(mi_heap_t* heap, size_t block_size, mi_segments_tld_t* tld, mi_os_tld_t* os_tld) {
mi_page_t* page;
if (block_size <= MI_SMALL_OBJ_SIZE_MAX) {
page = mi_segments_page_alloc(heap,MI_PAGE_SMALL,block_size,block_size,tld,os_tld);
}
else if (block_size <= MI_MEDIUM_OBJ_SIZE_MAX) {
page = mi_segments_page_alloc(heap,MI_PAGE_MEDIUM,MI_MEDIUM_PAGE_SIZE,block_size,tld, os_tld);
}
else if (block_size <= MI_LARGE_OBJ_SIZE_MAX) {
page = mi_segments_page_alloc(heap,MI_PAGE_LARGE,block_size,block_size,tld, os_tld);
}
else {
page = mi_segment_huge_page_alloc(block_size,tld,os_tld);
}
return page;
}
```
根據 `block_size` 決定分配 page 所需的大小,mimalloc 中將 `block_size` 分成四類,每類配置的空間的方式都不同:
* Small, [$0$ KiB, $8$ KiB]
每個 segment 有多個 page,每個 page 有多個 block,每個 block 的 `block_size` 為 small 大小
* Medium, ($8$ KiB, $128$ KiB]
每個 segment 有多個 page,每個 page 有多個 block,每個 block 的 `block_size` 為 medium 大小
* Large, ($128$ KiB, $4$ MiB]
每個 segment 有多個 page,每個 page 有一個 block,每個 block 的 `block_size` 為 large 大小
* Huge, ($4$ MiB, $2^{63}$ Bytes)
每個 segment 有一個 page,每個 page 有一個 block,每個 block 的 `block_size` 為 huge 大小
```c
static mi_page_t* mi_segments_page_alloc(mi_heap_t* heap, mi_page_kind_t page_kind, size_t required, size_t block_size, mi_segments_tld_t* tld, mi_os_tld_t* os_tld)
{
// find a free page
size_t page_size = _mi_align_up(required, (required > MI_MEDIUM_PAGE_SIZE ? MI_MEDIUM_PAGE_SIZE : MI_SEGMENT_SLICE_SIZE));
size_t slices_needed = page_size / MI_SEGMENT_SLICE_SIZE;
mi_page_t* page = mi_segments_page_find_and_allocate(slices_needed, tld);
if (page==NULL) {
// no free page, allocate a new segment and try again
if (mi_segment_reclaim_or_alloc(heap, slices_needed, block_size, tld, os_tld) == NULL) {
// OOM or reclaimed a good page in the heap
return NULL;
}
else {
// otherwise try again
return mi_segments_page_alloc(heap, page_kind, required, block_size, tld, os_tld);
}
}
mi_segment_delayed_decommit(_mi_ptr_segment(page), false, tld->stats);
return page;
}
```
`mi_segments_page_find_and_allocate` 會從已經分配的 segment 中尋找可用的 page,如果找不到則透過 `mi_segment_reclaim_or_alloc` 重新分配一個新的 segment 來尋找。
```c
static mi_segment_t* mi_segment_reclaim_or_alloc(mi_heap_t* heap, size_t needed_slices, size_t block_size, mi_segments_tld_t* tld, mi_os_tld_t* os_tld)
{
// 1. try to get a segment from our cache
mi_segment_t* segment = mi_segment_cache_pop(MI_SEGMENT_SIZE, tld);
if (segment != NULL) {
mi_segment_init(segment, 0, tld, os_tld, NULL);
return segment;
}
// 2. try to reclaim an abandoned segment
bool reclaimed;
segment = mi_segment_try_reclaim(heap, needed_slices, block_size, &reclaimed, tld);
if (reclaimed) {
// reclaimed the right page right into the heap
return NULL; // pretend out-of-memory as the page will be in the page queue of the heap with available blocks
}
else if (segment != NULL) {
// reclaimed a segment with a large enough empty span in it
return segment;
}
// 3. otherwise allocate a fresh segment
return mi_segment_alloc(0, tld, os_tld, NULL);
}
```
TODO: reclaim usage
嘗試獲得 semgent 空間,在 semgent 用完時,會將該 segment 放到 cache 中等待下次分配,這樣可以防止頻繁的從系統中分配空間,如果 cache 以及 reclaim 都失敗的話,呼叫 `mi_segment_alloc` 從 OS 分配實際的空間出來。
```c
static mi_page_t* mi_segments_page_find_and_allocate(size_t slice_count, mi_segments_tld_t* tld) {
// search from best fit up
mi_span_queue_t* sq = mi_span_queue_for(slice_count, tld);
if (slice_count == 0) slice_count = 1;
while (sq <= &tld->spans[MI_SEGMENT_BIN_MAX]) {
for (mi_slice_t* slice = sq->first; slice != NULL; slice = slice->next) {
if (slice->slice_count >= slice_count) {
// found one
mi_span_queue_delete(sq, slice);
mi_segment_t* segment = _mi_ptr_segment(slice);
if (slice->slice_count > slice_count) {
mi_segment_slice_split(segment, slice, slice_count, tld);
}
mi_page_t* page = mi_segment_span_allocate(segment, mi_slice_index(slice), slice->slice_count, tld);
if (page == NULL) {
// commit failed; return NULL but first restore the slice
mi_segment_span_free_coalesce(slice, tld);
return NULL;
}
return page;
}
}
sq++;
}
// could not find a page..
return NULL;
}
```
`mi_segments_tld_t:spans` 紀錄在該執行緒中,所有 segment 內可用的空間,並以 queue 串接。中間的 `for` 迴圈就是在從這些 queue 內尋找可用來當成 page 的空間 (這邊命名為 `slice`),並從該空間取得對應的 segment。`mi_segment_slice_split` 將此空間分割成目前需要的空間以及用不到的空間,並把用不到的空間放回 queue 中以便之後可以再利用。目前需要的空間則透過 `mi_segment_span_allocate` 實際設定成 page,以及 page 的相關訊息。`mi_segment_span_free_coalesce` 則是分配失敗時,把剛剛分割的空間還原。
segment 使用 `mi_segment_t:slices` 紀錄所分配的 page,每個 `slice` 內的成員 `slice_count`,表示此 page 占用 segment 的幾個 `slice`,`slice_offset` 不為 0 則表示此 `slice` 被其他實際的 `slice` 占用,而 `slice_offset` 就是該 `slice` 與實際 `slice` 間的 byte 數,以圖片表示:
```graphviz
digraph {
node [shape=record, fontcolor=black, fontsize=14.5, width=11, height=1, fixedsize=true];
values [label="<f0> slices[0]\n\{slice_count=2\}\n\{slice_offset=0\} | <f1> slices[1]\n\{slice_count=0\}\n\{slice_offset=96\} | <f2> slices[2]\n\{slice_count=1\}\n\{slice_offset=0\} | <f3> slices[3]\n\{slice_count=3\}\n\{slice_offset=0\} | <f4> slices[4]\n\{slice_count=0\}\n\{slice_offset=96\} | <f5> slices[5]\n\{slice_count=0\}\n\{slice_offset=192\} | <f6> slices[6]\n\{slice_count=1\}\n\{slice_offset=0\}"]
}
```
:::info
因 `sizeof(mi_slice_t)=96`,故 `slice_offset` 為 96 的倍數。
:::
其中 `slices[0], slices[2], slices[3], skuces[6]` 都會回傳並成為 page,而 page 實際大小就為該 page 所占有的所有 slices。
```c
static void mi_segment_slice_split(mi_segment_t* segment, mi_slice_t* slice, size_t slice_count, mi_segments_tld_t* tld) {
if (slice->slice_count <= slice_count) return;
size_t next_index = mi_slice_index(slice) + slice_count;
size_t next_count = slice->slice_count - slice_count;
mi_segment_span_free(segment, next_index, next_count, tld);
slice->slice_count = (uint32_t)slice_count;
}
```
再回頭看 `mi_segment_slice_split` 便很好理解,`next_index` 為分割後下個 `slice` 的位置,`next_count` 為下個 `slice` 擁有的 slice 數目,再透過 `mi_segment_span_free` 把這些數據設定到 segment 中下個 slice 的位置。
```c
// note: can be called on abandoned segments
static void mi_segment_span_free(mi_segment_t* segment, size_t slice_index, size_t slice_count, mi_segments_tld_t* tld) {
mi_span_queue_t* sq = (segment->kind == MI_SEGMENT_HUGE || mi_segment_is_abandoned(segment)
? NULL : mi_span_queue_for(slice_count,tld));
if (slice_count==0) slice_count = 1;
// set first and last slice (the intermediates can be undetermined)
mi_slice_t* slice = &segment->slices[slice_index];
slice->slice_count = (uint32_t)slice_count;
slice->slice_offset = 0;
if (slice_count > 1) {
mi_slice_t* last = &segment->slices[slice_index + slice_count - 1];
last->slice_count = 0;
last->slice_offset = (uint32_t)(sizeof(mi_page_t)*(slice_count - 1));
last->xblock_size = 0;
}
// perhaps decommit
mi_segment_perhaps_decommit(segment,mi_slice_start(slice),slice_count*MI_SEGMENT_SLICE_SIZE,tld->stats);
// and push it on the free page queue (if it was not a huge page)
if (sq != NULL) mi_span_queue_push( sq, slice );
else slice->xblock_size = 0; // mark huge page as free anyways
}
```
設定 `slice_count`,以及更新最後一個 `slice` 的 `slice_offset` (最後一個 `slice` 的 `slice_offset` 為與最高 index 的 `slice` 間的 byte 數),最後 `mi_span_queue_push` 將這個新分割的 `slice` 放回 queue 中,以便下次搜尋可以使用。
```c
// Note: may still return NULL if committing the memory failed
static mi_page_t* mi_segment_span_allocate(mi_segment_t* segment, size_t slice_index, size_t slice_count, mi_segments_tld_t* tld) {
mi_slice_t* slice = &segment->slices[slice_index];
// commit before changing the slice data
if (!mi_segment_ensure_committed(segment, _mi_segment_page_start_from_slice(segment, slice, NULL), slice_count * MI_SEGMENT_SLICE_SIZE, tld->stats)) {
return NULL; // commit failed!
}
// convert the slices to a page
slice->slice_offset = 0;
slice->slice_count = (uint32_t)slice_count;
const size_t bsize = slice_count * MI_SEGMENT_SLICE_SIZE;
slice->xblock_size = (uint32_t)(bsize >= MI_HUGE_BLOCK_SIZE ? MI_HUGE_BLOCK_SIZE : bsize);
mi_page_t* page = mi_slice_to_page(slice);
// set slice back pointers for the first MI_MAX_SLICE_OFFSET entries
size_t extra = slice_count-1;
if (extra > MI_MAX_SLICE_OFFSET) extra = MI_MAX_SLICE_OFFSET;
if (slice_index + extra >= segment->slice_entries) extra = segment->slice_entries - slice_index - 1; // huge objects may have more slices than avaiable entries in the segment->slices
slice++;
for (size_t i = 1; i <= extra; i++, slice++) {
slice->slice_offset = (uint32_t)(sizeof(mi_slice_t)*i);
slice->slice_count = 0;
slice->xblock_size = 1;
}
// and also for the last one (if not set already) (the last one is needed for coalescing)
mi_slice_t* last = &segment->slices[slice_index + slice_count - 1];
if (last < mi_segment_slices_end(segment) && last >= slice) {
last->slice_offset = (uint32_t)(sizeof(mi_slice_t)*(slice_count-1));
last->slice_count = 0;
last->xblock_size = 1;
}
// and initialize the page
page->is_reset = false;
page->is_committed = true;
segment->used++;
return page;
}
```
`mi_segment_span_allocate` 將 `slice` 轉成 page 回傳,要注意此時 `slice->xblock_size` 並非 page 內 block 的大小,而是 page 本身的大小,再之後會呼叫 `mi_page_init` 將 `slice->xblock_size` 設定為應儲存的 block 大小。`extra` 為此 slice 會占用到的所有其他 slice 的數量,需要將這些 slice 的 `slice_offset` 設定為正確的距離,另外最後一個 slice 沒設置時也會一併設定。
```c
// note: can be called on abandoned segments
static mi_slice_t* mi_segment_span_free_coalesce(mi_slice_t* slice, mi_segments_tld_t* tld) {
mi_segment_t* segment = _mi_ptr_segment(slice);
bool is_abandoned = mi_segment_is_abandoned(segment);
// for huge pages, just mark as free but don't add to the queues
if (segment->kind == MI_SEGMENT_HUGE) {
slice->xblock_size = 0; // mark as free anyways
// we should mark the last slice `xblock_size=0` now to maintain invariants but we skip it to
// avoid a possible cache miss (and the segment is about to be freed)
return slice;
}
// otherwise coalesce the span and add to the free span queues
size_t slice_count = slice->slice_count;
mi_slice_t* next = slice + slice->slice_count;
if (next < mi_segment_slices_end(segment) && next->xblock_size==0) {
// free next block -- remove it from free and merge
slice_count += next->slice_count; // extend
if (!is_abandoned) {
mi_segment_span_remove_from_queue(next, tld);
}
}
if (slice > segment->slices) {
mi_slice_t* prev = mi_slice_first(slice - 1);
if (prev->xblock_size==0) {
// free previous slice -- remove it from free and merge
slice_count += prev->slice_count;
if (!is_abandoned) {
mi_segment_span_remove_from_queue(prev, tld);
}
slice = prev;
}
}
// and add the new free page
mi_segment_span_free(segment, mi_slice_index(slice), slice_count, tld);
return slice;
}
```
`mi_segment_span_free_coalesce` 將先前分割的 slice 合併,並且加入回 `tld` 中的 queue,另外對於 huge 類型的 segment (只含有一個 page) 則不須加入到 queue 當中,因其是使用 `mi_segment_huge_page_alloc` 分配 segment 空間,而不是 `mi_segments_page_alloc` 從 queue 中尋找。
如果在 `mi_find_page` 要分配的 page 的大小超過 `MI_MEDIUM_OBJ_SIZE_MAX` 時,此時直接呼叫 `mi_large_huge_page_alloc` 從 segment 中找可用的 page 空間:
```c
// Large and huge page allocation.
// Huge pages are allocated directly without being in a queue.
// Because huge pages contain just one block, and the segment contains
// just that page, we always treat them as abandoned and any thread
// that frees the block can free the whole page and segment directly.
static mi_page_t* mi_large_huge_page_alloc(mi_heap_t* heap, size_t size) {
size_t block_size = _mi_os_good_alloc_size(size);
bool is_huge = (block_size > MI_LARGE_OBJ_SIZE_MAX);
mi_page_queue_t* pq = (is_huge ? NULL : mi_page_queue(heap, block_size));
mi_page_t* page = mi_page_fresh_alloc(heap, pq, block_size);
if (page != NULL) {
const size_t bsize = mi_page_block_size(page); // note: not `mi_page_usable_block_size` as `size` includes padding
if (pq == NULL) {
mi_page_set_heap(page, NULL);
}
}
return page;
}
```
可以看到與 `mi_find_free_page` 失敗時一樣,皆會呼叫 `mi_page_fresh_alloc` 來從 segment 中尋找 page。另外 huge 類型因為每個 segment 只有一個 page,所有不用 page queue 儲存。
`mi_page_fresh_alloc` 接著會呼叫 `_mi_segment_page_alloc` 嘗試從 segment 上尋找可用的 page (上方有介紹),在 `_mi_segment_page_alloc` 中,當 `block_size` 為 huge 類型時呼叫 `mi_segment_huge_page_alloc` 直接分配新的 segment:
```c
static mi_page_t* mi_segment_huge_page_alloc(size_t size, mi_segments_tld_t* tld, mi_os_tld_t* os_tld)
{
mi_page_t* page = NULL;
mi_segment_t* segment = mi_segment_alloc(size,tld,os_tld,&page);
if (segment == NULL || page==NULL) return NULL;
segment->thread_id = 0; // huge segments are immediately abandoned
return page;
}
```
### `mi_free`
```c
// Free a block
void mi_free(void* p) mi_attr_noexcept
{
const mi_segment_t* const segment = mi_checked_ptr_segment(p,"mi_free");
if (mi_unlikely(segment == NULL)) return;
const uintptr_t tid = _mi_thread_id();
mi_page_t* const page = _mi_segment_page_of(segment, p);
if (mi_likely(tid == segment->thread_id && page->flags.full_aligned == 0)) { // the thread id matches and it is not a full page, nor has aligned blocks
// local, and not full or aligned
mi_block_t* block = (mi_block_t*)(p);
mi_block_set_next(page, block, page->local_free);
page->local_free = block;
if (mi_unlikely(--page->used == 0)) { // using this expression generates better code than: page->used--; if (mi_page_all_free(page))
_mi_page_retire(page);
}
}
else {
// non-local, aligned blocks, or a full page; use the more generic path
// note: recalc page in generic to improve code generation
mi_free_generic(segment, tid == segment->thread_id, p);
}
}
```
`mi_free` 與 `mi_malloc` 一樣分成兩種情況,第一種是執行緒釋放相同執行緒上的變數,此時只需將 block 放入 `page->local_free` 當中即可,而實際將 `page->local_free` 移動到 `page->free` 形成可用空間的時機在於執行 `_mi_malloc_generic` 中找尋 page 的過程。
如果是不同執行緒間的釋放空間、釋放 full list 上的 page 或是釋放 unaligned 的 page,則會呼叫 `mi_free_generic`。
:::info
```c
typedef union mi_page_flags_s {
uint8_t full_aligned;
struct {
uint8_t in_full : 1;
uint8_t has_aligned : 1;
} x;
} mi_page_flags_t;
```
page 將 `in_full`, `has_aligned` 與 `full_aligned` 用同個 union 表示,只要 `x` 中有一個成員不為 0,則 `full_aligned` 就不為 0。
:::
```c
static void mi_decl_noinline mi_free_generic(const mi_segment_t* segment, bool local, void* p) {
mi_page_t* const page = _mi_segment_page_of(segment, p);
mi_block_t* const block = (mi_page_has_aligned(page) ? _mi_page_ptr_unalign(segment, page, p) : (mi_block_t*)p);
_mi_free_block(page, local, block);
}
// regular free
static inline void _mi_free_block(mi_page_t* page, bool local, mi_block_t* block)
{
// and push it on the free list
if (mi_likely(local)) {
// owning thread can free a block directly
if (mi_unlikely(mi_check_is_double_free(page, block))) return;
mi_block_set_next(page, block, page->local_free);
page->local_free = block;
page->used--;
if (mi_unlikely(mi_page_all_free(page))) {
_mi_page_retire(page);
}
else if (mi_unlikely(mi_page_is_in_full(page))) {
_mi_page_unfull(page);
}
}
else {
_mi_free_block_mt(page,block);
}
}
```
`_mi_free_block` 前半段同個執行緒下的釋放空間與 `mi_free` 相同,後方 `_mi_free_block_mt` 則為不同執行緒間的釋放函式,因為要處理不同執行緒間的變數,故在 `_mi_free_block_mt` 之中使用到了 atomic operation:
```c
// multi-threaded free
static mi_decl_noinline void _mi_free_block_mt(mi_page_t* page, mi_block_t* block)
{
// huge page segments are always abandoned and can be freed immediately
mi_segment_t* segment = _mi_page_segment(page);
if (segment->kind==MI_SEGMENT_HUGE) {
_mi_segment_huge_page_free(segment, page, block);
return;
}
// Try to put the block on either the page-local thread free list, or the heap delayed free list.
mi_thread_free_t tfreex;
bool use_delayed;
mi_thread_free_t tfree = mi_atomic_load_relaxed(&page->xthread_free);
do {
use_delayed = (mi_tf_delayed(tfree) == MI_USE_DELAYED_FREE);
if (mi_unlikely(use_delayed)) {
// unlikely: this only happens on the first concurrent free in a page that is in the full list
tfreex = mi_tf_set_delayed(tfree,MI_DELAYED_FREEING);
}
else {
// usual: directly add to page thread_free list
mi_block_set_next(page, block, mi_tf_block(tfree));
tfreex = mi_tf_set_block(tfree,block);
}
} while (!mi_atomic_cas_weak_release(&page->xthread_free, &tfree, tfreex));
if (mi_unlikely(use_delayed)) {
// racy read on `heap`, but ok because MI_DELAYED_FREEING is set (see `mi_heap_delete` and `mi_heap_collect_abandon`)
mi_heap_t* const heap = (mi_heap_t*)(mi_atomic_load_acquire(&page->xheap)); //mi_page_heap(page);
if (heap != NULL) {
// add to the delayed free list of this heap. (do this atomically as the lock only protects heap memory validity)
mi_block_t* dfree = mi_atomic_load_ptr_relaxed(mi_block_t, &heap->thread_delayed_free);
do {
mi_block_set_nextx(heap,block,dfree, heap->keys);
} while (!mi_atomic_cas_ptr_weak_release(mi_block_t,&heap->thread_delayed_free, &dfree, block));
}
// and reset the MI_DELAYED_FREEING flag
tfree = mi_atomic_load_relaxed(&page->xthread_free);
do {
tfreex = tfree;
mi_assert_internal(mi_tf_delayed(tfree) == MI_DELAYED_FREEING);
tfreex = mi_tf_set_delayed(tfree,MI_NO_DELAYED_FREE);
} while (!mi_atomic_cas_weak_release(&page->xthread_free, &tfree, tfreex));
}
}
```
`_mi_free_block_mt` 會將 block 放到兩個可能的地方,一個是 heap 的 thread delayed free list (`heap->thread_delayed_free`) 當中,另一個為 page 的 thread free list (`page->xthread_free`) 當中,只有當 thread delayed free list 為空時 (page 第一次分配 block 或呼叫 `_mi_heap_delayed_free` 後),才會將 block 放到該 list 當中。
thread delayed free list 的用途在於節省 lock 的時間,當某個 page 內的 block 數已滿時,該 page 會透過 `mi_page_to_full` 函式被放入到 `heap->page[MI_BIN_FULL]` 中 (論文內的 full list),使其不在 page queue 當中被搜尋以加快節點走訪速度。而當該 page 內的 block 被釋放後,需要將該 page 從 full list 移回 page queue 當中,如果是同個執行緒,則在 `mi_free` 時就會執行此動作,因為是同個執行緒所以不需要考慮 race condition,但如果釋放的空間是放在 thread free list 的話,檢查 block 是否釋放以及 page 的移動 (可能在移動的過程中 page 內的 block 被其他執行緒釋放) 都必須加上 lock 防止衝突。
在 heap 上增加 thread delayed free list 後,移動 page 的這個動作被延後執行,`_mi_heap_delayed_free` (在 `_mi_malloc_generic` 時呼叫) 會透過 atomic operation 把 thread delayed free list 移動到 `heap->local_free` 當中,之後由於都在同個執行緒上,便不需要額外的 lock,在 `_mi_heap_delayed_free -> _mi_free_delayed_block -> _mi_free_block` 中便能無鎖的把 page 從 full list 移回 page queue。
簡單來說,thread delayed free list 將有被不同執行緒釋放 block 的 page 連接,並給予 page 一個固定的時間點檢查該 page 是否需要從 full list 移動到 page queue 當中,同時也能真正釋放這些被不同執行緒釋放的 block。
> 如果沒有 thread delayed free list 的設計,在 `_mi_malloc_generic` 的過程中,會先嘗試從 segment 中找可用的 page,而不是從 full list 中找尋有被其他 thread 釋放空間 page,造成記憶體用量增加。
在 mimalloc 中,使用 `page->xthread_free` LSB 的 3 個 bits 來當作 delayed free 的 flags,其餘 bits 表示 thread free list 的 block:
```c
// Thread free flag helpers
static inline mi_block_t* mi_tf_block(mi_thread_free_t tf) {
return (mi_block_t*)(tf & ~0x03);
}
static inline mi_delayed_t mi_tf_delayed(mi_thread_free_t tf) {
return (mi_delayed_t)(tf & 0x03);
}
```
所以上方 `_mi_free_block_mt` 在 `mi_tf_delayed(tfree) == MI_USE_DELAYED_FREE` 時會將 block 放到 thread delayed free list (`heap->thread_delayed_free`),其餘時間則放到 thread free list (`page->xthread_free`) 中,並把 `page->xthread_free` 內的 flag 設定為 `MI_NO_DELAYED_FREE`,下次就不會再使用 delayed free list。
在 `_mi_malloc_generic` 中有提到會呼叫 `_mi_heap_delayed_free`,此函式會實際將 heap 中的 thread delayed free list 移動回 page 的 `local_free`:
```c
void _mi_heap_delayed_free(mi_heap_t* heap) {
// take over the list (note: no atomic exchange since it is often NULL)
mi_block_t* block = mi_atomic_load_ptr_relaxed(mi_block_t, &heap->thread_delayed_free);
while (block != NULL && !mi_atomic_cas_ptr_weak_acq_rel(mi_block_t, &heap->thread_delayed_free, &block, NULL)) { /* nothing */ };
// and free them all
while(block != NULL) {
mi_block_t* next = mi_block_nextx(heap,block, heap->keys);
// use internal free instead of regular one to keep stats etc correct
if (!_mi_free_delayed_block(block)) {
// we might already start delayed freeing while another thread has not yet
// reset the delayed_freeing flag; in that case delay it further by reinserting.
mi_block_t* dfree = mi_atomic_load_ptr_relaxed(mi_block_t, &heap->thread_delayed_free);
do {
mi_block_set_nextx(heap, block, dfree, heap->keys);
} while (!mi_atomic_cas_ptr_weak_release(mi_block_t,&heap->thread_delayed_free, &dfree, block));
}
block = next;
}
}
```
`while` 迴圈逐一走訪 thread delayed free list,並對每個 block 執行 `_mi_free_delayed_block`。
```c
bool _mi_free_delayed_block(mi_block_t* block) {
// get segment and page
const mi_segment_t* const segment = _mi_ptr_segment(block);
mi_page_t* const page = _mi_segment_page_of(segment, block);
// Clear the no-delayed flag so delayed freeing is used again for this page.
// This must be done before collecting the free lists on this page -- otherwise
// some blocks may end up in the page `thread_free` list with no blocks in the
// heap `thread_delayed_free` list which may cause the page to be never freed!
// (it would only be freed if we happen to scan it in `mi_page_queue_find_free_ex`)
_mi_page_use_delayed_free(page, MI_USE_DELAYED_FREE, false /* dont overwrite never delayed */);
// collect all other non-local frees to ensure up-to-date `used` count
_mi_page_free_collect(page, false);
// and free the block (possibly freeing the page as well since used is updated)
_mi_free_block(page, true, block);
return true;
}
```
`_mi_page_use_delayed_free` 很簡單,就是將 page 內的 delayed free list 中的 flag 設定回 `MI_USE_DELAYED_FREE`,表示 block 可以再放到 thread delayed free list 中;`_mi_page_free_collect` 將可用空間 thread free list 及 local free list 移動回 free list;`_mi_free_block` 上方有介紹,將此 block (在 thread delayed free list 中的 block) 移動回 page 的 local free list 中。
```c
void _mi_page_free_collect(mi_page_t* page, bool force) {
// collect the thread free list
if (force || mi_page_thread_free(page) != NULL) { // quick test to avoid an atomic operation
_mi_page_thread_free_collect(page);
}
// and the local free list
if (page->local_free != NULL) {
if (mi_likely(page->free == NULL)) {
// usual case
page->free = page->local_free;
page->local_free = NULL;
page->is_zero = false;
}
else if (force) {
// append -- only on shutdown (force) as this is a linear operation
mi_block_t* tail = page->local_free;
mi_block_t* next;
while ((next = mi_block_next(page, tail)) != NULL) {
tail = next;
}
mi_block_set_next(page, tail, page->free);
page->free = page->local_free;
page->local_free = NULL;
page->is_zero = false;
}
}
}
```
`_mi_page_free_collect` 是把 local free list 以及 thread free list 移回 free list 的函式,除了在 `_mi_free_delayed_block` 中被呼叫外,也會在其他需要 page 空間時呼叫來取得可用 page 空間。
其中 `_mi_page_thread_free_collect` 用來回收 thread free list,而下方則是一般 local free list 的回收,將 `page->local_free` 與 `page->free` 連接,並把 `page->local_free` 清空。
### `_mi_thread_done`
每個執行緒結束時都要正確回收空間,在 mimalloc 當中註冊了 `_mi_thread_done` 函式,讓執行緒在結束時能夠呼叫該函式來回收空間。
```c
// Set up handlers so `mi_thread_done` is called automatically
static void mi_process_setup_auto_thread_done(void) {
static bool tls_initialized = false; // fine if it races
if (tls_initialized) return;
tls_initialized = true;
#if defined(_WIN32) && defined(MI_SHARED_LIB)
// nothing to do as it is done in DllMain
#elif defined(_WIN32) && !defined(MI_SHARED_LIB)
mi_fls_key = FlsAlloc(&mi_fls_done);
#elif defined(MI_USE_PTHREADS)
mi_assert_internal(_mi_heap_default_key == (pthread_key_t)(-1));
pthread_key_create(&_mi_heap_default_key, &mi_pthread_done);
#endif
_mi_heap_set_default_direct(&_mi_heap_main);
}
```
在 windows 平台上註冊 `mi_fls_done`;在 POSIX 上則是註冊 `mi_pthread_done`,兩函式皆會呼叫 `_mi_thread_done` 來回收空間。
```c
static void _mi_thread_done(mi_heap_t* heap) {
// check thread-id as on Windows shutdown with FLS the main (exit) thread may call this on thread-local heaps...
if (heap->thread_id != _mi_thread_id()) return;
// abandon the thread local heap
if (_mi_heap_done(heap)) return; // returns true if already ran
}
// Free the thread local default heap (called from `mi_thread_done`)
static bool _mi_heap_done(mi_heap_t* heap) {
if (!mi_heap_is_initialized(heap)) return true;
// reset default heap
_mi_heap_set_default_direct(_mi_is_main_thread() ? &_mi_heap_main : (mi_heap_t*)&_mi_heap_empty);
// switch to backing heap
heap = heap->tld->heap_backing;
if (!mi_heap_is_initialized(heap)) return false;
// delete all non-backing heaps in this thread
mi_heap_t* curr = heap->tld->heaps;
while (curr != NULL) {
mi_heap_t* next = curr->next; // save `next` as `curr` will be freed
if (curr != heap) {
mi_heap_delete(curr);
}
curr = next;
}
// collect if not the main thread
if (heap != &_mi_heap_main) {
_mi_heap_collect_abandon(heap);
}
// merge stats
_mi_stats_done(&heap->tld->stats);
// free if not the main thread
if (heap != &_mi_heap_main) {
// the following assertion does not always hold for huge segments as those are always treated
// as abondened: one may allocate it in one thread, but deallocate in another in which case
// the count can be too large or negative. todo: perhaps not count huge segments? see issue #363
// mi_assert_internal(heap->tld->segments.count == 0 || heap->thread_id != _mi_thread_id());
_mi_os_free(heap, sizeof(mi_thread_data_t), &_mi_stats_main);
}
return false;
}
```
`_mi_thread_done` 會呼叫 `_mi_heap_done` 釋放 heap 空間,在 `_mi_heap_done` 中,會透過 `mi_heap_delete` 刪除 `heap->tld->heaps` 中除了 `heap->tld->heap_backing` 外的其他 heap,這些 heap 是透過 `mi_heap_new` 產生的,在後方會再說明。接著執行 `_mi_heap_collect_abandon`,此函式將 heap 內部的 page 回收,最後 `_mi_os_free` 則實際釋放該 heap 的記憶體空間。
```c
void _mi_heap_collect_abandon(mi_heap_t* heap) {
mi_heap_collect_ex(heap, MI_ABANDON);
}
static void mi_heap_collect_ex(mi_heap_t* heap, mi_collect_t collect)
{
if (heap==NULL || !mi_heap_is_initialized(heap)) return;
_mi_deferred_free(heap, collect >= MI_FORCE);
// note: never reclaim on collect but leave it to threads that need storage to reclaim
if (
#ifdef NDEBUG
collect == MI_FORCE
#else
collect >= MI_FORCE
#endif
&& _mi_is_main_thread() && mi_heap_is_backing(heap) && !heap->no_reclaim)
{
// the main thread is abandoned (end-of-program), try to reclaim all abandoned segments.
// if all memory is freed by now, all segments should be freed.
_mi_abandoned_reclaim_all(heap, &heap->tld->segments);
}
// if abandoning, mark all pages to no longer add to delayed_free
if (collect == MI_ABANDON) {
mi_heap_visit_pages(heap, &mi_heap_page_never_delayed_free, NULL, NULL);
}
// free thread delayed blocks.
// (if abandoning, after this there are no more thread-delayed references into the pages.)
_mi_heap_delayed_free(heap);
// collect retired pages
_mi_heap_collect_retired(heap, collect >= MI_FORCE);
// collect all pages owned by this thread
mi_heap_visit_pages(heap, &mi_heap_page_collect, &collect, NULL);
// collect segment caches
if (collect >= MI_FORCE) {
_mi_segment_thread_collect(&heap->tld->segments);
}
}
```
`_mi_deferred_free` 會呼叫使用者註冊的 `deferred_free` 函式,其下方的 `_mi_abandoned_reclaim_all` 因不是主執行緒不會運行到,`mi_heap_visit_pages` 會逐一走訪 page,並對每個 page 執行給定的函式 (此例為 `mi_heap_page_never_delayed_free`)。
下方 `_mi_heap_delayed_free` 及 `_mi_heap_collect_retired` 回收 delayed free list 以及 retireed page。並再呼叫一次 `mi_heap_visit_pages` 回收所有 page。最後 `_mi_segment_thread_collect` 把存在 cache 中的 segment 釋放。
```c
static bool mi_heap_page_collect(mi_heap_t* heap, mi_page_queue_t* pq, mi_page_t* page, void* arg_collect, void* arg2 ) {
UNUSED(arg2);
UNUSED(heap);
mi_collect_t collect = *((mi_collect_t*)arg_collect);
_mi_page_free_collect(page, collect >= MI_FORCE);
if (mi_page_all_free(page)) {
// no more used blocks, free the page.
// note: this will free retired pages as well.
_mi_page_free(page, pq, collect >= MI_FORCE);
}
else if (collect == MI_ABANDON) {
// still used blocks but the thread is done; abandon the page
_mi_page_abandon(page, pq);
}
return true; // don't break
}
```
在 `mi_heap_collect_ex` 中,`mi_heap_visit_pages` 會將每個 page 透過 `mi_heap_page_collect` 來釋放,`_mi_page_free_collect` 在上方已有介紹,`mi_page_all_free` 檢查 page 內的 block 是否全部釋放,如果是的話則 `_mi_page_free` 會將該 page 從 segment 中釋放;如果該 page 還有可用的 block 在其他 thread 中被使用,則會呼叫 `_mi_page_abandon` 將該 page 設置為 abandon。
```c
// Free a page with no more free blocks
void _mi_page_free(mi_page_t* page, mi_page_queue_t* pq, bool force) {
// no more aligned blocks in here
mi_page_set_has_aligned(page, false);
mi_heap_t* heap = mi_page_heap(page);
// remove from the page list
// (no need to do _mi_heap_delayed_free first as all blocks are already free)
mi_segments_tld_t* segments_tld = &heap->tld->segments;
mi_page_queue_remove(pq, page);
// and free it
mi_page_set_heap(page,NULL);
_mi_segment_page_free(page, force, segments_tld);
}
```
`_mi_page_free` 將 page 從 page queue 中移除,並把 `page->heap` 設為 NULL,最後 `_mi_segment_page_free` 將 page 從 segment 中刪除。
```c
void _mi_segment_page_free(mi_page_t* page, bool force, mi_segments_tld_t* tld)
{
mi_segment_t* segment = _mi_page_segment(page);
// mark it as free now
mi_segment_page_clear(page, tld);
if (segment->used == 0) {
// no more used pages; remove from the free list and free the segment
mi_segment_free(segment, force, tld);
}
else if (segment->used == segment->abandoned) {
// only abandoned pages; remove from free list and abandon
mi_segment_abandon(segment,tld);
}
}
```
`mi_segment_page_clear` 將 page 回收回 segment,與 page 相似,當沒有可用的 page 時,透過 `mi_segment_free` 將 segment 也釋放,否則呼叫 `mi_segment_abandon` 將該 segment 設為 abandon。
```c
// note: can be called on abandoned pages
static mi_slice_t* mi_segment_page_clear(mi_page_t* page, mi_segments_tld_t* tld) {
mi_segment_t* segment = _mi_ptr_segment(page);
size_t inuse = page->capacity * mi_page_block_size(page);
// reset the page memory to reduce memory pressure?
if (!segment->mem_is_pinned && !page->is_reset && mi_option_is_enabled(mi_option_page_reset)) {
size_t psize;
uint8_t* start = _mi_page_start(segment, page, &psize);
page->is_reset = true;
_mi_os_reset(start, psize, tld->stats);
}
// zero the page data, but not the segment fields
page->is_zero_init = false;
ptrdiff_t ofs = offsetof(mi_page_t, capacity);
memset((uint8_t*)page + ofs, 0, sizeof(*page) - ofs);
page->xblock_size = 1;
// and free it
mi_slice_t* slice = mi_segment_span_free_coalesce(mi_page_to_slice(page), tld);
segment->used--;
// cannot assert segment valid as it is called during reclaim
// mi_assert_expensive(mi_segment_is_valid(segment, tld));
return slice;
}
```
`mi_segment_page_clear` 將 page 內的值清空,並透過 `mi_segment_span_free_coalesce` 把 page 放回 `tld->span` 這個 queue 中。
```c
static void mi_segment_free(mi_segment_t* segment, bool force, mi_segments_tld_t* tld) {
// Remove the free pages
mi_slice_t* slice = &segment->slices[0];
const mi_slice_t* end = mi_segment_slices_end(segment);
size_t page_count = 0;
while (slice < end) {
if (slice->xblock_size == 0 && segment->kind != MI_SEGMENT_HUGE) {
mi_segment_span_remove_from_queue(slice, tld);
}
page_count++;
slice = slice + slice->slice_count;
}
if (!force && mi_segment_cache_push(segment, tld)) {
// it is put in our cache
}
else {
// otherwise return it to the OS
mi_segment_os_free(segment, tld);
}
}
```
`mi_segment_free` 檢查 segment 中的所有 `slice`,將這些有用到的 `slice` 放回 span queue 當中,要注意當 `slice` 還未分配出去時,`xblock_size` 用來表示該 `slice` 是否位於 span queue 中,0 表示在 queue 當中,1 則表示不在 queue 當中。`mi_segment_cache_push` 將 segment 放入 cache 中,下次需要 segment 時可以直接從 cache 中取得記憶體。如果必須釋放空間的話 (如 huge segment),則呼叫 `mi_segment_os_free` 真正釋放開記憶體。
:::info
`page`(`slice`) 中的 `xblock_size` 有許多用途,在 `slice` 階段 (還未要分配給使用者)時,分成兩種:
* `xblock_size == 0` 表示此 slice 目前存放在 `tld->spans` 這個 queue 當中
* `xblock_size == 1` 表示此 slice 目前不存放在 `tld->spans` 這個 queue 當中
當要把 `slice` 分配給使用者時,分配函式 `mi_segment_span_allocate` 內的 `xblock_size` 表示的是此 `slice` (`page`) 實際佔有的空間,也就是所有可分配的空間;在分配後,`xblock_size` 變成這個 `page` 內的每個 block 擁有的空間。
:::
### `mi_heap_*`
mimalloc 提供建立在執行緒內自行管理的 heap,相關函式可參考[連結](https://microsoft.github.io/mimalloc/group__heap.html)。
```c
mi_heap_t* mi_heap_new(void)
{
mi_heap_t* bheap = mi_heap_get_backing();
mi_heap_t* heap = mi_heap_malloc_tp(bheap, mi_heap_t); // todo: OS allocate in secure mode?
if (heap==NULL) return NULL;
_mi_memcpy_aligned(heap, &_mi_heap_empty, sizeof(mi_heap_t));
heap->tld = bheap->tld;
heap->thread_id = _mi_thread_id();
_mi_random_split(&bheap->random, &heap->random);
heap->cookie = _mi_heap_random_next(heap) | 1;
heap->keys[0] = _mi_heap_random_next(heap);
heap->keys[1] = _mi_heap_random_next(heap);
heap->no_reclaim = true; // don't reclaim abandoned pages or otherwise destroy is unsafe
// push on the thread local heaps list
heap->next = heap->tld->heaps;
heap->tld->heaps = heap;
return heap;
}
```
`mi_heap_new` 在目前執行緒中建立一個新的 heap,`mi_heap_get_backing` 回傳目前執行緒的主要 heap,`mi_heap_malloc_tp` 實際分配一個新的 heap 空間,要注意的是分配 heap 與一般分配 block 的程式碼相同是使用 `mi_heap_malloc` 在目前執行緒的主要 heap 的 page 上分配,設置 heap 的過程與 `mi_heap_main_init` 相似,最後再將新增的 heap 放到 `tld->heads` 的 linked list 上。
```c
// Safe delete a heap without freeing any still allocated blocks in that heap.
void mi_heap_delete(mi_heap_t* heap)
{
if (heap==NULL || !mi_heap_is_initialized(heap)) return;
if (!mi_heap_is_backing(heap)) {
// tranfer still used pages to the backing heap
mi_heap_absorb(heap->tld->heap_backing, heap);
}
else {
// the backing heap abandons its pages
_mi_heap_collect_abandon(heap);
}
mi_heap_free(heap);
}
```
`mi_heap_delete` 將 heap 內的 block 回收,其中在 heap 與執行緒主要 heap 不同時呼叫 `mi_heap_absorb`,將要被釋放的 heap 內空間轉移給主要 heap 來管理,而如果是執行緒主要的 heap,則改呼叫 `_mi_heap_collect_abandon` 將內部空間釋放,其作用上方已有解釋,最後 `mi_heap_free` 會實際把 heap 從 `tld->heaps` 上移除,並釋放 heap 空間。
:::info
:information_source: `mi_heap_free` 或許可以改為放在 `if (!mi_heap_is_backing(heap))` 底下。
:::
```c
// Tranfer the pages from one heap to the other
static void mi_heap_absorb(mi_heap_t* heap, mi_heap_t* from) {
if (from==NULL || from->page_count == 0) return;
// reduce the size of the delayed frees
_mi_heap_delayed_free(from);
// transfer all pages by appending the queues; this will set a new heap field
// so threads may do delayed frees in either heap for a while.
// note: appending waits for each page to not be in the `MI_DELAYED_FREEING` state
// so after this only the new heap will get delayed frees
for (size_t i = 0; i <= MI_BIN_FULL; i++) {
mi_page_queue_t* pq = &heap->pages[i];
mi_page_queue_t* append = &from->pages[i];
size_t pcount = _mi_page_queue_append(heap, pq, append);
heap->page_count += pcount;
from->page_count -= pcount;
}
// and do outstanding delayed frees in the `from` heap
// note: be careful here as the `heap` field in all those pages no longer point to `from`,
// turns out to be ok as `_mi_heap_delayed_free` only visits the list and calls a
// the regular `_mi_free_delayed_block` which is safe.
_mi_heap_delayed_free(from);
// and reset the `from` heap
mi_heap_reset_pages(from);
}
```
`mi_heap_absorb` 將有用到的 page queue append 到目標 heap 上,再用 `mi_heap_rest_pages` 把 heap 內的 page 歸 0。
```c
// called from `mi_heap_destroy` and `mi_heap_delete` to free the internal heap resources.
static void mi_heap_free(mi_heap_t* heap) {
if (heap==NULL || !mi_heap_is_initialized(heap)) return;
if (mi_heap_is_backing(heap)) return; // dont free the backing heap
// reset default
if (mi_heap_is_default(heap)) {
_mi_heap_set_default_direct(heap->tld->heap_backing);
}
// remove ourselves from the thread local heaps list
// linear search but we expect the number of heaps to be relatively small
mi_heap_t* prev = NULL;
mi_heap_t* curr = heap->tld->heaps;
while (curr != heap && curr != NULL) {
prev = curr;
curr = curr->next;
}
if (curr == heap) {
if (prev != NULL) { prev->next = heap->next; }
else { heap->tld->heaps = heap->next; }
}
// and free the used memory
mi_free(heap);
}
```
`mi_heap_free` 設定 default heap 並將 heap 從 `tld->heaps` 中移除,最後利用 `mi_free` 釋放 heap 空間,因為 internal heap 與一般的 block 一樣記憶體是存放在 page 當中,故與一般釋放 block 一樣透過 `mi_free` 即可釋放。
```c
void mi_heap_destroy(mi_heap_t* heap) {
if (heap==NULL || !mi_heap_is_initialized(heap)) return;
if (!heap->no_reclaim) {
// don't free in case it may contain reclaimed pages
mi_heap_delete(heap);
}
else {
// free all pages
_mi_heap_destroy_pages(heap);
mi_heap_free(heap);
}
}
void _mi_heap_destroy_pages(mi_heap_t* heap) {
mi_heap_visit_pages(heap, &_mi_heap_page_destroy, NULL, NULL);
mi_heap_reset_pages(heap);
}
```
`mi_heap_destroy` 與 `mi_heap_delete` 不同的地方在於會把 heap 內使用的空間全部釋放,透過呼叫 `_mi_heap_destroy_pages` 來釋放每個 page。`_mi_heap_destroy_page` 會逐一走訪 heap 上的每個 page,並對每個 page 執行 `_mi_heap_page_destroy` 函式:
```c
static bool _mi_heap_page_destroy(mi_heap_t* heap, mi_page_queue_t* pq, mi_page_t* page, void* arg1, void* arg2) {
// ensure no more thread_delayed_free will be added
_mi_page_use_delayed_free(page, MI_NEVER_DELAYED_FREE, false);
/// pretend it is all free now
page->used = 0;
// and free the page
// mi_page_free(page,false);
page->next = NULL;
page->prev = NULL;
_mi_segment_page_free(page,false /* no force? */, &heap->tld->segments);
return true; // keep going
}
```
`_mi_heap_page_destroy` 呼叫 `_mi_segment_page_free` 來將 segment 內的該 page 移除。