重新定義 S-tree 提供的 public interface。
可在 st_create
中看到節點之間的 order 比較(cmp
)、節點的建立與銷毀(create_node
/destroy_node
) 被從 S-tree 中獨立出來,以 callback function 的方式讓函式庫的使用者自行實作。其餘包含樹的走訪邏輯、root node 的維護等都直接在內部完成。
順帶一提,原本 treeint_insert
和 treeint_remove
中走訪 S-tree 的邏輯其實很相似。這裡我們實際上也可以重用 st_find
函式來歸納之,避免重寫相同的邏輯,以減少程式碼編譯後產生之二進位檔的大小。
完整的實作改進可以參照 s_tree.c。有了這項調整,未來就可以很輕易地將 S-tree 應用在其他合適的專案之中了!
雖然介面上變得簡單許多,不過其實如果對照 Linux 的 rbtree 實作,會發現其反而避免使用了使用 callback 的方式,作法比較接近本題原始的實作。
原因可參照原作者在 rbtree.h 中的註解說明:
To use rbtrees you'll have to implement your own insert and search cores. This will avoid us to use callbacks and to drop drammatically performances. I know it's not the cleaner way, but in C (not in C++) to get performances and genericity…
首先,為利於後續加入其他演算法進行效能評比,對原始程式碼做出以下改動,以建立相關基礎建設:
benchmark
,方便測量一段程式碼完成的時間
主要測量 insert/find/remove 三種操作的時間
tree-perf.py
,方便將測量結果視覺化
為了讓測量結果盡量不受 noise 干擾,且視覺化後的曲線圖可以更平滑,試著用簡單的標準差法排除蒐集到的時間中之 outlier
雖然前面提到 callback 的方式可能會對函式呼叫整體造成額外成本,但這裡我們主要的目標是要比較不同 tree 的 throughput,而非注重單一 tree 本身在實際應用上的可能優化
因此取捨傾向先以能夠容易且快速的加入各種 BST 實作的設計方式為主
效能評比程式有了,那麼 rbtree 的函式庫該從何而來呢? 稍微搜尋了一下,一方面似乎沒有比較被大眾廣泛接受的紅黑樹相關 C 函式庫;一方面,S-tree 的實作和 Linux 中的 rbtree 介面上其實存在不少相似之處。因此我們乾脆將 Linux 的紅黑樹實作拉到 userspace 來應用。這裡參照 Linux 6.4 版本中的程式碼,並且只擷取與 insert/find/remove 三項操作相關的函式實作,具體整理出來的程式碼可以參考:
接著我們就可以來測量各個操作間的差異。
將實驗分為循序輸入和亂序輸入的場景。對於循序輸入的部份,我們將從 0
一直到 tree_size - 1
的 key 值插入到樹中,然後再以相同的次序進行搜尋和刪除。過程記錄不同的樹節點數量與完成相應操作所需之累積時間的關係。則實驗的結果如下圖:
循序狀況下,似乎在樹節點數量較少時兩者差異不大,而節點增加時,S-tree 相較於 rbtree 的表現更好?
而亂序輸入的場景下,我們會隨機產生任意的插入/搜尋/移除值,實驗結果如下:
從結果來看,亂序狀況下,S-tree 整體的效能看起來稍優於 rbtree。且在亂序情形下 rbtree 似乎更容易有 worst case (圖中極端值) 的出現。
該怎麼依此給出合理的解釋呢?
注意退化的狀況,worst case 的討論
我們假設目前的 S-Tree 已經有 1000, 1, 2, 8 插入,接著插入 16 來觀察
st_insert
前st_update
前st_update
: 由於 16 沒有左右節點因此不會做任何動作,但是由於 16 的 hint 為 0 所以會對其親節 8 點做 update。st_update
: 由於 st_balance 為 1 因此不會做任何動作,但由於 8 的 hint 有經過更改 (0 -> 1) 所以會對其親節 1000 點做 update。st_update
: 做完 rotate 後,由於 1000 的 hint 沒有改變 (1->1) 因此結束。從上面會發現由於 hint 本身只要沒有改變且不等於 0 就不會往上 update ,代表 S-Tree 只要 update leaf 時發生 AVL 的 LR 或是 RL 就不會再往上 update 了
S-Tree 效仿 Linux 核心的 linked list,而定義了以下的結構體以及操作函式、巨集(此處僅列舉部份函式、巨集)
可見 st_node
中,先撇除 hint
物件(會在後續說明用意),剩下的物件也都是指標,分別是指向父節點、左側子節點以及右側的子節點。參考 Linux 核心的 linked list 設計理念,不難推論 S-Tree 想達成以下示意圖的效果:
同樣參照 Linux 核心原始程式碼巨集: container_of
,可發現與 linked list 的效果有異曲同工之妙。S-Tree 同樣提供一內含數個指標的結構以及相關的操作函式,讓開發者引用 st_node
、st_root
來自行定義二元搜尋樹所需的 struct
,而涉及二元樹的插入新節點、刪除指定節點的操作,可利用 st_insert()
、st_remove()
兩個函式,來完成增減節點的操作函式,而程式碼便不用再撰寫指標操作相關的部份,因為一切交由 st_insert()
和 st_remove()
來處理,讓每一節點中的 parent
、left
、right
物件,都可以指向應指向的節點。如此,開發者即可建立適當的 S-Tree。
(((sz + mask) / alignment) * alignment)
,其中 mask = alignment - 1
。(((sz + mask) >> alignment) << alignment)
,其中 mask = alignment - 1
,考量 pattern 中不可出現 alignment,可改寫成 (((sz + mask) >> (mask + 1)) << (mask + 1))
。((sz + mask) & ~mask)
,其中 mask = alignment - 1
TSan
(ThreadSanitizer
) 編譯,在之後程式執行時可檢查 data race 的發生。TSan
警告有 data race 發生。從訊息中可以得到 data race 發生的記憶體位置為 0x7b4000000000
,在 qsort_mt.c:132
配置的 c.pool
之中。
c.pool
為 struct qsort
組成之陣列,其中的成員 st
應該要被 mtx_st
所保護。
但在 qsort_mt.c:211
的位置進行 st
的寫入動作前,並未持有 mtx_st
,與 qsort_mt.c:343
位置的讀取動作有 data race 發生的可能。
修正很簡單,就是在 st
執行寫入動作前,先爭取到 mtx_st
即可。