dlmalloc 2.8.6 源碼詳解(5),dlmalloc2.8.6
本文章由vector03原創, 轉載請註明出處.
郵箱地址: mmzsmm@163.com, 歡迎來信討論.
3. 分配及實現
本章節介紹dlmalloc的分配演算法和實現.由於存在多mspace的情況, dlmalloc使用了兩套API.一套對應預設的mspace,以dl首碼開頭,如dlmalloc, dlrealloc等.如果建立了自訂的mspace,則使用mspace開頭的API,如mspace_malloc, mspace_realloc等.但兩套API在基礎演算法上是一致的.我們就以預設的API為主要對象介紹.
3.1 演算法概覽
事實上, dlmalloc雖然複雜,核心演算法卻非常簡單,如果有前面章節的基礎很容易就能看懂.
核心分配演算法針對small request和large request概括起來各五句話(注意這裡的分配請求大小都是經過align和padding處理後的大小),對應small request(<256位元組),
1. 首先在分配請求對應大小的分箱以及更大一級分箱中尋找, 如果有則返回,否則進入下一步.選擇這兩個分箱因為它們最接近分配目標大小,且剩餘部分都無法單獨成為一個chunk (原文中稱之為remainderless chunk).
2. 如果dv大小足夠滿足,則切割dv chunk,否則進入下一步.
3. 在所有分箱範圍內尋找(包括small bins和tree bins),找到可以滿足需求的最小的chunk,切割,將剩餘部分指定為新的dv,否則進入下一步.
4. 如果top chunk滿足需求,則切割top,否則進入下一步.
5. 從系統擷取記憶體並使用它.
對應large request,
1. 從tree bins中尋找最小可用的tchunk,如果其比dv更加適合(更接近目標大小),就使用該chunk.如果其剩餘部分超過最小可分配chunk,則切割它.否則進入下一步.
2. 如果dv滿足需求,且比任何分箱中的chunk更適合,則使用dv,否則進入下一步.
3. 如果top滿足需求,則使用top,否則進入下一步.
4. 如果分配請求大於mmap_threshold閾值,則直接通過mmap分配,否則進入下一步.
5. 從系統擷取記憶體並使用它.
從類型上, dlmalloc屬於best-fit型分配器,只是Doug Lea在此基礎上做了諸多最佳化.本質上都是本著物盡其用的思想來挑選合適的free chunk, 只有當不能首先滿足時, dlmalloc會通過dv和top來做進一步的挑選,這就最大限度的減小了內部片段產生.同時dv和top的存在也能比較有效減少外部片段.
而如果外部請求過大, dlmalloc不是優先擷取系統記憶體後分配,反而傾向於直接通過mmap擷取.原因在於位於top的free chunk有可能因為相鄰高地址的alloced chunk而一直無法釋放.如果dlmalloc向系統申請了大塊記憶體,即便被應用程式free,也可能因為auto trmming失敗而導致它們長期駐留在top space中.而直接mmap的好處就是隨時可以將這些huge chunk返回給系統,只要應用程式決定不再使用它們.
下面是更詳細的程式碼分析,
基本上還是比較好理解的, 下面對一些地方做展開說明,
1. Line5491, 這裡如果使用lock, 會在開始確認一些全域參數是否初始化.這些參數儲存在名為mparams的全域變數裡,類型為malloc_params,包含交叉檢查的magic,當前系統頁面大小,設定的粒度大小, mmap的閾值, trimming閾值以及預設的mspace參數.並且以magic作為參數初始化的標誌.
2. Line4595, PREACTION和POSTACTION成對出現,就是加鎖和解鎖.因為是平台相關的,針對不同系統需要有具體的實現.從這裡其實也可以看出dlmalloc對多線程條件下的分配設計的還是比較簡陋的,關注的還是單線程下分配演算法的實現.
3. Line4619, 這裡是一個對double link list首節點的刪除操作,且如果list為空白就更新small map.注意, dlmalloc為了提高list處理速度,是設計了前端節點的,因此這個first chunk並不是前端節點,而是其前一個節點.這個曾經在前面的章節提到過,可以通過這裡的具體實現看到這些優勢,其中B指代list前端節點, P是需要刪除的節點.
4. Line4663, 是替換dv的過程,舊dv如果還存在,會送回到分箱系統中管理,而新的chunk作為其替代. M指mstate, P是繼任dv, S為繼任dv大小.
這裡insert_small_chunk是前面刪除的反向操作,實現如下,
在插入的同時, 會對small map進行維護操作.
3.2 tmalloc_small
tmalloc_small是在tree bins中分配small chunk的子函數.用於small request的核心分配演算法3,即當remainderless和dv都無法滿足,且剩餘small bins也沒有free chunk時,從tree bins中搜尋.
代碼本身其實比較容易理解, 源碼注釋如下,
兩點說明,
1. Line4537, 尋找DST最小節點通過宏leftmost_child完成,該宏的定義如下,
這裡涉及到最小節點的遍曆. 我們知道, 對於BST來說,根節點與左右子樹有嚴格的排序關係,因此尋找最小節點就是從根節點出發向左子樹步進,一直遇到左子樹為null停止的過程.
但如2.2.5小節中所述, DST本就不是一棵排序樹,根節點同子樹間不能確定大小關係,相比之下擷取最小節點就更困難一些.但可以確定的一點是,同一級level中,越靠近左側的子樹節點就越小,因此我們可以大致圈定最小節點出現的範圍,如下,
中用顏色標記了每一級level最左側的節點(不限於左子樹或右子樹),儘管暫時還無法斷定哪一個是最小節點,但它肯定出現在從A到E的路徑上.所以DST的搜尋路徑為,從根節點出發,一路向左子樹步進,若遇到左子樹為空白,就轉頭向右子樹,一直遇到左右子樹都為空白停止.換句話說,沿著整棵DST的最左側邊緣走,
關於這一點, 我想應該是DST最大的缺陷,因為無論如何,遍曆的次數是與樹高相關的,中最小節點可能出現在位置C,但你需要完成每一次比較才能最後下決定.不過好在對於size_t等於4位元組的系統,樹高最多也只有32.無論如何這比線性尋找還是要快得多了.
2. Line4551, 與DST的刪除操作相關.由於unlink_large_chunk宏的代碼比較長,還是先說明一下節點刪除演算法.基本上, Doug Lea的DST刪除演算法分為三個步驟,
第一步, 判斷待刪除節點X是否存在相同大小的兄弟節點.如果有,只要簡單的將其從雙鏈表上摘除再重新接好鏈表即可.,
第二步, 如果節點X所在位置只有其一個節點,就需要選出一個繼任節點R以替代X空缺的位置,同時還要保證DST的性質.由於DST也屬於首碼樹(prefix tree)的一種,因此子樹節點提升level是很容易的,但降低level情況就相對複雜了.比如,子樹節點首碼為0101,可以提升為010,但如果下降為0101x…x就必須參考其他子樹的情況.,
這裡如果我們選擇R作為繼任節點,則原節點X的左子樹節點L就要改變其level.這時必須參考子樹R的情況,為L尋找一個合適的插入點.如果R的內部很複雜,這個過程就會相對漫長.
因此Doug Lea取了個巧,他選擇了right-most葉子節點作為X的繼任節點.既然是分葉節點,只需要簡單的提升level即可,其他子樹節點的level和位置都不會發生任何變化,於是就繞過了上述問題.選擇right-most的原因還在於, dlmalloc在遍曆best-fit節點時,會按照left-most的路徑尋找,導致多數情況下,左子樹節點數少於右子樹.為了平衡左右子樹,同時削減子樹高度,選擇right-most相比更為合適,,
中尋找到子樹X的right-most節點R,用其替換X空缺的位置,可以看到L節點等子樹節點位置沒有任何變化.同時,改變R的位置平衡了左至右子樹,讓DST整體更均衡.
第三步, 這裡就比較清楚了,只需要重新串連繼任節點與原X的父節點和左右子樹節點即可.
整個過程的源碼注釋如下,
3.3 tmalloc_large
該函數是在tree bins中分配large request的子函數.與tmalloc_small略有區別, large request並不是尋找最小節點,而是best-fit節點,即一個大於等於期望值的最小節點.
基本演算法如下,
1. 以分配請求大小nb作為key值進行基值檢索,並做兩點記錄.一是記錄最接近的候選節點v,另一個記錄當前最近的未被遍曆的右子樹節點rst (The deepest untaken right subtree).同時如果找到相同大小的chunk則立即返回.
2. 若已遍曆到子樹的最下層, 則返回記錄的rst子樹節點,從這個位置開始進行left-most遍曆,這裡同tmalloc_small中尋找最小子樹節點是一致的.
3. 若找不到可用節點, 則從treemap中尋找最小可用分箱,從可用分箱中尋找.
4. 若dv比候選節點v更適合,則直接返回0,否則切割候選節點,並最終返回payload.
在同一分箱內的搜尋過程,
在一個分箱內, 搜尋best-fit節點按照從A到F的順序執行.其中A-D屬於基值檢索,以nb的首碼為key值,而E-F則按照left-most檢索,因為E子樹是當前分箱中大於目標值的最小子樹,只要找到最小節點即可.從這裡也可以看出這個演算法本質上很簡單,就是先按照首碼尋找最接近的目標節點,如果沒有則擴大範圍在大於目標值的最小子樹中搜尋,還沒有再到最接近的更大的分箱中尋找,直到找到為止.
代碼注釋如下,
Line4465, Line4480, Line4490,這三處地方其實就是對nb掩碼逐bit位的測試操作,以進行基值檢索.
該宏展開如下,
看上去有些複雜, 其實就是除最高有效位以及次最高有效位之外, 將後續bit位移動到msb端.之後每次迴圈就取出一位進行檢測.如所示,
稍微不好理解的就是i >> 1的作用.回顧一下2.2.4小節中tree bins索引定址的說明,這裡就是computer_tree_index的逆運算.結果是不算末尾8bit,最高有效位的位號.這裡減2的原因是最高有效和次高有效位用於計算分箱號,因此不計入key值.位移後獲得的掩碼在檢測時會重新右移至最低位,並提取以決定是向左子樹還是右子樹步進.在整個迴圈中會不斷左移掩碼以保證遍曆持續進行,直至達到最底層子樹節點.
事實上這個宏Doug Lea搞得有點麻煩,這個運算用CLZ指令加2就能獲得同樣的結果.我猜Doug Lea不這樣寫的原因可能是盡量減少各個平台的區別,或者純粹是他懶得再分別寫四種實現.