以最新版本 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.
mi_block
用來回傳使用者真正需要的記憶體空間位置,不需要紀錄 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
清空,防止資料洩漏:
mi_page
mi_block_t* free
:此 page 中,尚未被使用者使用的 blockmi_block_t* local_free
:對應論文中的 local free listmi_thread_free_t* xthread_free
:對應論文中的 thread free list另外 xblock_size
為此 page 內每個 block 可佔有的空間,每個存有相同大小的 block 之 page 也會由 next
與 prev
欄位連結成 doubly linked list。
mi_segment
mi_segment
為實際上 mimalloc 主要從 OS 上獲得的空間,管理著所需要的 page (mi_slice_t slices[MI_SLICES_PER_SEGMENT
)。其中 slices[0]
被用來儲存 segment 中的額外資訊。
mi_heap
每個執行緒上都有一個 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
mi_tid
存放此執行緒的相關運行資料,包括 mi_heap
的資料、作業系統的資料等等,mi_segments_tld_t segment
為目前此執行緒中與 segment 相關的資料,其內部 mi_tld_t->segments->spans[MI_SEGMENT_BIN_MAX+1]
成員存放此執行緒可供使用的所有 page 用來加快存取效率。
mi_malloc
透過 mi_malloc
分配記憶體,其中 mi_get_default_heap
會回傳 _mi_heap_deafult1
,取得目前執行緒所管理的 heap。
_mi_heap_default
在程式執行時已被設定為 _mi_heap_main
_mi_heap_default
初始化為 _mi_heap_empty
,表示此 heap 尚未分配空間,在其後 mi_malloc
的過程中分配新的空間給予 _meap_default
來當作此執行緒可用的 heap。依照需要的 block 大小與 MI_SMALL_SIZE_MAX (128)
決定使用 mi_malloc
的 fast path 還是 slow path。
fast path
heap->page_free_direct
為存有 130 個 mi_page_t
的陣列,儲存 size 較小的 page,fast path 會從 heap->pages_free_direct
內直接取得需要配置空間的 page,而不需要逐一走訪 page queue 中尋找可用的 page。
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:
_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 空間。
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。
mi_page_queue
會找到 size 對應的 page queue,如果該 page queue 之 first
的 free list 不為空,則表示該 page 可用,直接回傳,否則呼叫 mi_page_queue_find_free_ex
在 page queue 中搜尋 free list 不為空的 page:
如果 page queue 不為空,則逐一走訪 queue 並嘗試釋放空間,如果在走訪的過程中找到空的 free block 時便直接回傳該 page。當 page queue 中的所有 page 都沒有可用的 block 時,先呼叫 _mi_heap_collect_retired
回收空間,接著 mi_page_fresh
會嘗試從 segment 中獲得一塊新的 page 空間使用。
_mi_segment_page_alloc
為實際從 segment 分配 page 的函式,當找到可用的 page 時,通過 _mi_page_init
初始化,並將該 page 放到先前所選擇對應大小的 page queue 中。
根據 block_size
決定分配 page 所需的大小,mimalloc 中將 block_size
分成四類,每類配置的空間的方式都不同:
block_size
為 small 大小block_size
為 medium 大小block_size
為 large 大小block_size
為 huge 大小mi_segments_page_find_and_allocate
會從已經分配的 segment 中尋找可用的 page,如果找不到則透過 mi_segment_reclaim_or_alloc
重新分配一個新的 segment 來尋找。
TODO: reclaim usage
嘗試獲得 semgent 空間,在 semgent 用完時,會將該 segment 放到 cache 中等待下次分配,這樣可以防止頻繁的從系統中分配空間,如果 cache 以及 reclaim 都失敗的話,呼叫 mi_segment_alloc
從 OS 分配實際的空間出來。
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 數,以圖片表示:
因 sizeof(mi_slice_t)=96
,故 slice_offset
為 96 的倍數。
其中 slices[0], slices[2], slices[3], skuces[6]
都會回傳並成為 page,而 page 實際大小就為該 page 所占有的所有 slices。
再回頭看 mi_segment_slice_split
便很好理解,next_index
為分割後下個 slice
的位置,next_count
為下個 slice
擁有的 slice 數目,再透過 mi_segment_span_free
把這些數據設定到 segment 中下個 slice 的位置。
設定 slice_count
,以及更新最後一個 slice
的 slice_offset
(最後一個 slice
的 slice_offset
為與最高 index 的 slice
間的 byte 數),最後 mi_span_queue_push
將這個新分割的 slice
放回 queue 中,以便下次搜尋可以使用。
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 沒設置時也會一併設定。
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 空間:
可以看到與 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:
mi_free
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
。
page 將 in_full
, has_aligned
與 full_aligned
用同個 union 表示,只要 x
中有一個成員不為 0,則 full_aligned
就不為 0。
_mi_free_block
前半段同個執行緒下的釋放空間與 mi_free
相同,後方 _mi_free_block_mt
則為不同執行緒間的釋放函式,因為要處理不同執行緒間的變數,故在 _mi_free_block_mt
之中使用到了 atomic operation:
_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:
所以上方 _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
:
while
迴圈逐一走訪 thread delayed free list,並對每個 block 執行 _mi_free_delayed_block
。
_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 中。
_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
函式,讓執行緒在結束時能夠呼叫該函式來回收空間。
在 windows 平台上註冊 mi_fls_done
;在 POSIX 上則是註冊 mi_pthread_done
,兩函式皆會呼叫 _mi_thread_done
來回收空間。
_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 的記憶體空間。
_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 釋放。
在 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。
_mi_page_free
將 page 從 page queue 中移除,並把 page->heap
設為 NULL,最後 _mi_segment_page_free
將 page 從 segment 中刪除。
mi_segment_page_clear
將 page 回收回 segment,與 page 相似,當沒有可用的 page 時,透過 mi_segment_free
將 segment 也釋放,否則呼叫 mi_segment_abandon
將該 segment 設為 abandon。
mi_segment_page_clear
將 page 內的值清空,並透過 mi_segment_span_free_coalesce
把 page 放回 tld->span
這個 queue 中。
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
真正釋放開記憶體。
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,相關函式可參考連結。
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 上。
mi_heap_delete
將 heap 內的 block 回收,其中在 heap 與執行緒主要 heap 不同時呼叫 mi_heap_absorb
,將要被釋放的 heap 內空間轉移給主要 heap 來管理,而如果是執行緒主要的 heap,則改呼叫 _mi_heap_collect_abandon
將內部空間釋放,其作用上方已有解釋,最後 mi_heap_free
會實際把 heap 從 tld->heaps
上移除,並釋放 heap 空間。
mi_heap_free
或許可以改為放在 if (!mi_heap_is_backing(heap))
底下。
mi_heap_absorb
將有用到的 page queue append 到目標 heap 上,再用 mi_heap_rest_pages
把 heap 內的 page 歸 0。
mi_heap_free
設定 default heap 並將 heap 從 tld->heaps
中移除,最後利用 mi_free
釋放 heap 空間,因為 internal heap 與一般的 block 一樣記憶體是存放在 page 當中,故與一般釋放 block 一樣透過 mi_free
即可釋放。
mi_heap_destroy
與 mi_heap_delete
不同的地方在於會把 heap 內使用的空間全部釋放,透過呼叫 _mi_heap_destroy_pages
來釋放每個 page。_mi_heap_destroy_page
會逐一走訪 heap 上的每個 page,並對每個 page 執行 _mi_heap_page_destroy
函式:
_mi_heap_page_destroy
呼叫 _mi_segment_page_free
來將 segment 內的該 page 移除。