contributed by < Wufangni
>
解說影片連結:Linux 核心專題: 紅黑樹實作改進
fennecJ
針對 linux kernel 中紅黑樹的相關函式 API 進行廣泛的探討,閱讀文章過程中我學到了許多,也複習了不熟悉的紅黑樹操作流程。
問題一
「紅黑樹在重新平衡的標準下容忍度相較高,減少需重新平衡的時間成本。」可否進一步說明,或透過分析其演算法呈現為何其容忍度較高。
紅黑樹遵守五大原則(在 TODO: rbtree & XTree 中提及),因此做插入或刪除操作時只會因為黑色節點數量不一致或顏色規則而需重新平衡,而 AVL Tree 在平衡結構方面規劃的較為嚴謹,若左右子樹差超過 1 時就須重新平衡,因此整體所需的重平衡成本相較高於紅黑樹。
問題二
針對運用場景的歸納,為何 AVL tree 相較 rb tree 更適合用於搜尋較多的場合
因上訴回答說明,AVL Tree 在平衡方面有較嚴謹的規範,因此整體平衡性會高於紅黑樹,因此在搜尋操作下相較於紅黑樹能更有效率的找出目標節點。
問題三
rb_augmented_tree 中使用了 propagate function 進行資料的維護,該 function 的時間複雜度為何?是否會影響到原本 insert/del 的時間複雜度。
根據紅黑樹性質其樹高最多被限制在 ,而
propagate
用於向上更新節點附加資訊(從更新節點位置到至多根節點位置),因此時間複雜度為 ,且由於額外維護操作與樹高成正比,因此在插入及刪除操作下時間複雜度仍為 。
問題四
可否進一步解釋 xtree 中使用的超立方體資料結構是如何實做的
Xtree 中每個節點皆擁有自己的一份超立方體,用來含括資料範圍提高範圍搜尋效率,因此超立方體結構須包含最大值、最小值以及維度等資訊,範例如下:
marsh-fish
rbtree_augmented.h
的 __rb_erase_augmented
並不是在進行刪除節點的操作,而是在判斷是否進行再平衡(rebalance)
紅黑樹做為一種自平衡樹在新增、刪除或搜尋等操作下時間複雜度均為 ,特別的地方在於樹高採用 black height(不含自身節點到樹葉節點的總黑色節點數量),相較於 AVL Tree 嚴格要求每個節點左右子樹高度差不超過1,紅黑樹在重新平衡的標準下容忍度相較高,減少需重新平衡的時間成本。
因此以不同角度對AVL Tree與紅黑樹做觀察和比較,前者雖然整體結構較平衡,但需要比多的平衡時間,而在搜尋資料方面 AVL Tree 因較好的平衡規範表現優於紅黑樹,所需空間方面紅黑樹只需 1 位元的變數來區分紅色或黑色資訊,因此可將兩種資料結構的運用場景歸納如下:
紅黑樹從 2-3-4 樹變化而來,因此可以自由地在兩結構之間做變換,主要遵守五大原則:
rb_node
結構體存放三種資訊,*rb_left
、*rb_right
分別指向左右子節點位址,__rb_parent_color
存放了父節點位址,且由於 __attribute__((aligned(sizeof(long)))
會讓編譯器對 sizeof(long)
做對齊,因此最低兩位元不會被使用到(永遠都是零),可用來儲存該節點顏色資訊,而 rb_root
用來儲存根節點的位址資訊。
較常使用的 rb_parent
過濾掉 __rb_parent_color
最低兩位元資訊,用於獲取父節點的位址。
rb_link_node
透過指定 __rb_parent_color
到 *parent
的方式將 *node
的父節點位址指定到 *parent
。
rb_root_cached
除了儲存根節點位址外也存了最左節點(樹的最小節點)資訊,因此使用 #define rb_first_cached(root) (root)->rb_leftmost
可直接存取cache儲存的最左節點,搜尋結果的時間複雜度只需 。
對 rb_root_cached
做插入、刪除、替換節點的操作時需要一同更新 cache 內最左節點的資訊。
從 rbtree_augmented.h
可看到對於 Augmented Red-Black Tree 的相關定義與實作方式,Augmented Red-Black Tree 可在一般傳統紅黑樹上附加額外的資訊及維護功能,可將額外資訊藏在節點中(比如子樹的最大值、最小值…等),使用 rb_augment_callbacks
做各種維護操作,主要結構如下:
*propagate
代表在插入、刪除節點和調整顏色時用來向上更新子樹的附加資訊*copy
代表節點被替換時複製附加資訊*rotate
代表在旋轉時更新附加資訊rbtree_augmented.h
內的常用定義:
rb_set_parent
將 *rb
節點的顏色與 *p
的位址合起來指定給 rb->__rb_parent_color
,等同於設定 *p
作為 *rb
的父節點。
__rb_change_child
為替換節點操作,如果父節點存在則判斷要被替換的節點是否為左子節點,一致就將左子節點位址指定到新節點,否則指定右子節點位址到新節點,若父節點不存在代表此被替代節點為根節點,則指定根節點為址到新節點。
__rb_erase_augmented
刪除節點情況:
Case1 如果刪除節點沒有子節點或只有一個子節點:
Case2 如果刪除節點有兩個子節點,且刪除節點的右子節點就是後繼節點:
若child2為紅色,後繼節點必為黑色,則刪除節點有兩種情況:
Case3 如果刪除節點有兩個子節點,且刪除節點的右子節點不是後繼節點:
持續直到找到後繼節點( succesor )為止:
將後繼節點往上拉到刪除節點的右子節點:
此時欲替代刪除節點的後繼節點已變成刪除節點的右子節點,與 Case2 情況相同,因此後續交換節點與平衡方式等同於 Case2 作法。
在 lib/rbtree.c
中的基本輔助函數 rb_set_black
與 *rb_red_parent
,前者用於設置輸入節點的顏色為黑色,後者則是獲取紅色節點 *red
的父節點指針。
__rb_rotate_set_parents
在旋轉操作中設置父節點和顏色,old 節點被替換成 new 節點,且更新父節點指針和顏色。
XTree 用於多維度數據空間索引的資料結構,可高效率的對高維度數據做搜尋,適用於需高維度資料查詢的場景下,例如需要建立地圖、座標等高維資訊的地理資訊系統(Geographic Information System, 簡稱 GIS),含有圖像、聲音、影片等高維特徵向量的多媒體數據庫…等,可在空間利用率和資料查詢性能之間取得較好的平衡。
節點類型分為兩種:
每個節點會有一個自己的 Split Dimension 和 Split Threshold,當節點的資料數量達到 Split Threshold 設立的上限,則需依靠 Split Dimension 提供的分割維度分裂節點, XTree使用超立方體來覆蓋每個節點的資料,一個節點代表一個超立方體,邊長為該節點包含的資料最大範圍在各維度上的跨度,使用超立方體可簡化節點的管理和搜尋過程。
XTree 的工作原理主要劃分為以下三項:
1. 插入節點操作
新資料根據多維座標找到適合插入的樹葉節點,為了確保邊長能覆蓋到此節點包含的所有資料需要對此節點的超立方體進行更新。
2. 分裂節點操作
若插入的資料超過此節點的容量限制( Split Threshold ),則需分裂節點,且分裂後的新節點會各自擁有自己的超立方體。
3. 搜尋節點操作
XTree 會透過查詢條件判斷是否與節點的超立方體重疊來快速過濾不相關的節點,提高搜尋效率。
XTree基於上述特性可整理出它帶有的優缺點,如下統整:
因此與先前教材中的紅黑樹特性去做一個比較整合,下表顯示 rbtree vs XTree 的特性比較:
- | rbtree | XTree |
---|---|---|
應用場景 | 需要快速搜尋、插入和刪除的場合,例如記憶體管理等系統操作。 | 需要對多維資料作處理的場景,例如 GIS 和多媒體資料庫。 |
實現難易度 | 難度高,但效能穩定,可廣泛適用於多種場景下使用。 | 難度高,主要針對多維度資料處理的應用。 |
效能 | 一維資料上效能穩定,操作複雜度維 。 | 在高維資料上效能上有優勢,能高效率查詢多維資訊。 |
重平衡時間 | rbtree 因高效的平衡操作,所需時間成本較短。 | XTree 在多維資料處理上效率較高。 |
空間成本 | 每個節點需要儲存顏色資訊。 | 由於節點容量需求較大,空間成本開銷較高。 |
資料範圍 | 主要針對一維資料操作。 | 主要針對多維資料操作。 |
在 linux 核心中紅黑樹資料結構應用在任務的排程器當中,系統將可執行的任務儲存在紅黑樹,再以執行時間的線性順序插入到 process 中,且因紅黑樹的自平衡搜尋樹特性,樹高會與基本操作(插入、刪除和搜尋)的時間成本成正比,因此在平衡過程中會盡可能減低樹的高度以降低時間成本。
CPU 排程器在選取下一個要執行的任務時會依據 vruntime 最小來做選擇,因此需找出紅黑樹最左邊節點,一般紅黑樹在 n 個節點下樹高 ≤ 2 log2(n + 1),搜尋的時間複雜度為 ,然而在 lib/rbtree.c 中可看到紅黑樹存在有快取和非快取兩種結構的版本,有快取版本中用來儲存根節點位址的結構體 rb_root_cached
會多儲存最左邊節點的資訊,且以多個更新操作去維護他,此時對最小節點的搜尋成本降為 ,可顯著加速搜尋過程,CFS 中採用快取版本來維護最小執行時間的任務。
若任務是被阻止的不會再次進入 runqueue,但若是遇到 timeslice 結束或是被其他任務搶先則會插入到更新執行時間後的紅黑樹中,且可能不會在原先的位置(經過 時間的重新平衡)。
以上圖範例來看,左邊以一個黑色子節點代表最淺的樹葉節點,右邊則利用紅黑穿插來增加樹高(不能有連續紅節點情況),且要符合每條路徑內的黑節點數量一致,因此最深可達樹高 3 的樹葉節點,確保了最深樹葉節點的樹高不會超過最淺樹葉節點的兩倍。
根據上述內容及前置作業對紅黑樹的基本認知,紅黑樹的優點可以分為下面兩點:
將紅黑樹與常使用的 AVL tree 做比較,列出了兩結構在時間、空間及使用場景下的差異性,且由於 CFS 需要頻繁的插入和刪除,因此紅黑樹更適合 CFS。
rbtree | AVL tree | |
---|---|---|
平衡性 | 較低,但相較於AVL tree 減少了每次操作需要重新平衡的次數 | 較高,AVL tree 有嚴格規定左右子樹之間的高度不能大於2 |
空間需求 | 較低,只需一位元儲存顏色資訊 | 較高,需要一整個整數變量來儲存平衡因子 |
搜尋效率 | 較低,平衡狀態不如 AVL tree,時間複雜度為 | 較高,屬於高度平衡結構因此時間複雜度為 |
應用場景 | 適用於頻繁添加/刪除節點的場景 | 適用在需密集查詢資料的場景 |
設計紅黑樹測試內容,引入 rbtree.c 及 rbtree.h 並撰寫測試檔 rbtree-tst.c,首先建立欲操作的紅黑樹節點結構 my_node
,包含一個節點值 key 及紅黑樹存放指針結構 rb_node
。
加入搜尋、插入 my_insert
、刪除等操作,且在插入及刪除節點時使用紅黑樹平衡函式 rb_insert_color
及 rb_erase
,避免紅黑樹規則被破壞。
新增紅黑樹根節點 tree
,使用 clock_t
紀錄操作開始與結束的時間,且定義要紀錄的最大節點數 max_node_count
、每次操作的節點數量 step_size
,以及迭代次數 iterations
,實驗會以多次迭代的平均作為該次操作的計算結果。
新增用來計算操作時長的函數 get_time_in_seconds
。
n 等同於 step_size
,每次操作會採用 rand()
對該次操作的全部節點賦予隨機值。
計算插入節點操作
建立新的插入節點 *new_node
並賦予 key 值,start = clock()
和 end = clock()
分別紀錄操作開始與結束的時間,且使用 get_time_in_seconds
計算總操作時長。
計算搜尋節點操作
與插入操作相同計算方式,分別紀錄開始與結束時間並使用 get_time_in_seconds
取得總操作時長。
計算刪除節點操作
先利用 my_search
搜尋欲刪除的節點,後續計算方式與插入操作相同,分別紀錄開始與結束時間並使用 get_time_in_seconds
取得總操作時長。
最後在每次操作結束後釋放儲存節點值的記憶體 free(keys)
。
完整程式碼在 Wufangni/rbtree
最大節點數增加到1000時可明顯分別出三個操作下時間長度的差異,插入和刪除操作因多次重平衡(旋轉和上色)所需時間會高於搜尋操作。
圖表中最後呈現結果反而在刪除操作時效能會優於搜尋操作,但搜尋時不需要做重新平衡,理應低於刪除操作的執行時長,但只有在初期實驗(最大節點數 50 以內)有呈現這個趨勢,目前還沒找出能解釋的原因。
此外也分別對不同迭代次數做實驗,以下皆為 max_node_count = 250
時不同 iterations
的實驗結果:
iterations = 1
iterations = 5
iterations = 10
iterations = 50
可看到若沒有設置迭代次數(iterations = 1
)只單靠一次操作時長做紀錄,會因每次操作的細微差異造成不穩定的曲線狀態,在三種不同操作下的辨識程度會非常低,難以區分各操作的效能優劣,隨著迭代次數的增加取平均後可看到不同操作分佈逐漸分開,曲線也不會像單一次數那樣呈現震盪,若是迭代次數設置太大(iterations = 50
)則容易造成分佈相似的操作更緊密,差異較大的分的更明顯的現象,對於原本比較接近的刪除和搜尋兩操作會更不易於分辨。
在 rbtree.h
檔案中新增外部宣告 rb_rotation_count
,用於紀錄旋轉次數。
在 rbtree.c
中加入 reset_rb_rotation_count
函式
更改 __rb_rotate_left
和 __rb_rotate_right
,增加 rb_rotation_count++
以計算每次左旋和右旋的次數。
修改 Makefile
根據圖表呈現插入操作的旋轉次數使用最多,其次為刪除操作,驗證了兩操作在時間總長實驗下的差異性(插入所花時間明顯高於刪除),而搜尋操作因沒有直接對樹的本身結構做改變則不須重新平衡,故旋轉次數為零。
cache miss 作為一個新的判斷效能的依據,若在執行過程中產生 miss 則須從記憶體再做一次存取的操作,造成效能降低,因此使用 perf
來統計 Data Cache Misses 總次數,並對不同操作進行了分析及比較。
新增 measure_cache_misses
函數,建立一個 command 命令用來統計插入、刪除和搜尋操作的 cache 未命中次數。
打開進程來執行剛剛建立的 command,並連接到 perf_output
建立一個儲存 cache 輸出結果的緩衝區,並從 perf_output 讀取一行輸出到 buffer,轉換型態存進 *cache_misses
中,最後關閉剛剛被打開的 perf_output。
在插入刪除,及搜尋操作下呼叫 measure_cache_misses
函數並以不同變數累加該次的 cache miss,最後除以迭代次數取的平均值。
可看到在插入操作時的 miss 情況會比較多,與執行所花時長呈現正相關,表示插入操作執行過程中會比另兩個操作多花費了大量重新存取資料的時間。
實做 AVL tree 的兩個檔案 avltree.c 及 avltree.h,並新增 avlVSrb-tst.c 測試檔,引入兩種資料結構和分別紀錄在插入刪除和搜尋三個操作下的執行時間。
根據兩資料結構的性質,rbtree 較適合進行大量插入刪除的操作,而在實驗結果中可看到在插入操作過程中 rbtree 的執行時間明顯低於 AVL tree,而在刪除與搜尋兩操作執行時間差異不大,較難對此做區分。
參考〈Xa-tree: 在 OLAP 上有效支援範圍查詢的多維度索引架構〉論文的作法,Xa-Tree 做為 X-Tree 的一種變化型態,結合了 X-Tree 和 Ra-tree 的優點,可以有效提升範圍查詢的效能,提昇查詢效率。
改進 X-Tree 方式
contains
,利用每個維度的最小值與最大值檢查該範圍是否完全包含於另一種範圍。
且在範圍搜尋函數內使用 contains
判斷此節點內容是否包含於搜尋範圍內,若是則直接採用節點內的特徵值。
實驗結果
下面是〈Xa-tree: 在 OLAP 上有效支援範圍查詢的多維度索引架構〉論文提供的在不同範圍大小的資料搜尋下各資料結構的效能。
查詢範圍 90%各維度 CPU 計算時間
查詢範圍 10%各維度 CPU 計算時間
查詢範圍 90%各維度節點存取次數
查詢範圍 10%各維度節點存取次數