執行人: Tonr01
相關資訊
sl_list
為 skip list 的結構體,其中 size
存放整個 skip list 的元素個數,level
存放 skip list 的層級數,而 head
會指向每層的 list 開頭。
sl_node
為 skip list 的節點結構,其中 key
存放該節點的值,val
為存放該節點的位址,而 link
會指向其他層的該節點。
sl_node_alloc
sl_node_alloc
為配置記憶體空間給節點,因為每個元素所佔有的層數是隨機的,若是宣告成大小一致的 link
,會造成記憶體的浪費,於是在 sl_node
中宣告 struct sl_link link[0];
,將其宣告成一個可以擴充的陣列,所以在配置記憶體時就能視該元素所佔有的層數去動態的配置記憶體,一個節點所佔用的空間大小為節點本身的結構大小加上每一層連接的 link。
sl_list_alloc
sl_list_alloc
為初始化 skip list,會將所有的 list head 初始化,將它的 prev
, next
link 都指向自己。
這裡的 set_random();
的功用是什麼
注意其本質: "A skip list is a probabilistic data structure"
sl_delete
sl_delete
是用來釋放整條 skip list,head[0]
為第一層的 list,也就是包含所有元素的 list,所以將每個節點都釋放,最後再將 list 釋放。
sl_search
sl_search
是用來搜尋節點是否存在於 skip list,首先會從 skip list 的最上層開始找起,若是找到 key
值一樣的節點就回傳該節點的位址,若是目前走訪的節點的 key
值大於要搜尋的節點的 key
值,則表示搜尋的節點可能會在下一層,所以先回到前一個節點,再往下一層去走訪,直到每一層都走訪完。
sl_insert
sl_insert
是用來插入新節點到 skip list,並回傳 skip list 大小,一開始先用 random_level
函式決定出該節點所佔的層數。
因為節點所佔的層數是隨機產生的,所以這裡使用隨機方法 (稱作 "coin flip",也就是「擲硬幣看正反面結果」) 決定這個元素有幾層節點。詳細的實作方法為先用 random()
去得到一個隨機數,random_seed & 1
的用意是視 random_seed
last bit 是 1 或 0 去決定是否增加層數,也就是決定該元素僅有 1 層節點機率為 ,且有 2 層節點機率為 ,僅有 3 層節點機率為 ,以此類推。
走訪節點的方式與 list_search
相同,會先從該節點所佔的最高層數開始走訪,去比較每個節點的 key
值,找到插入的地方,再用 list_add
插入節點到每層 list,直到走訪完全部的 skip list。
若是發現該節點已經在 skip list 中,則 goto failed;
,表示插入失敗。
sl_erase
sl_erase
是用來刪除指定節點,一樣從最高層數開始,先去找目前層數是否有該節點,若沒有,則往下一層找,若有,則使用 list_del
函式將該節點從 list 分離出來,並將前後的 link 連接。
compiler_barrier()
的目的在於告訴編譯器,在 barrier() 前後必須明確區隔,barrier 之前的操作不可跑到 barrier 後,防止程式碼因編譯器優化而造成問題。
若是當刪除該節點後,該 list 為空時,則更新層數的資料,再繼續往下一層走訪,直到最後一層。
在 random_level
函式中,須從最低位元開始檢查,直到該 bit 為 0 時才停止,所以最壞情況會是 ,也就是最差須做 32 次檢查。
可以看出只需要知道從最低位元到高位元第一個為 0 的 bit 位置,就可以得到 level
的值,所以這裡使用 __builtin_ffs
來改善。
int __builtin_ffs (unsigned int x) 會回傳 1 + LSB 為 1 的 index
主要的想法是先將 random_seed
的值與 -1
做 ,可以得到與 random_seed
0, 1 相反的值,再去用 __builtin_ffs
去得到 LSB 為 1 的 index ,此時 LSB 為 1 的 index 就是原本從最低位元到高位元第一個為 0 的 bit 位置。
這裡分別用改善前與改善後的 random_level
函式產生 1000000 筆資料,並執行 5 次做比較。
random_level_origin
random_level_enhance1
event data | origin | enhance1 |
---|---|---|
cache-misses | 10,745 | 10,910 |
cache-references | 28,087 | 27,999 |
instructions | 87,840,933 | 79,842,405 |
cycles | 72,349,681 | 59,620,192 |
可以看出 cache-misses 與 cache-references 是差不多的,減少了大約 10% 的 instructions,並減少約 18% 的 cycles。
因為 skiplist 是一種 probabilistic data structure,所以在決定 skiplist 的層數時,最壞情況可能會增長很多不需要的層數,因為決定層數主要是用 flip-coin 的方法,多餘的層數會造成不必要的走訪。
可以看出 level 8~11 重複 4 層,當走訪的值不是 36 時,則必須多走這些重複的層,會造成些許效能上的浪費,balanced skip list 的第 Level i+1 通道包含 Level i 通道的每 1/p 個元素,所以最佳的層數為 ,所以新增一個函式去對 key 值範圍取 log,藉此去限制整個 skiplist 的層數。
這個取 log 的函式是先前在測驗裡的方法,這裡剛好能使用到,詳細方法在 log 函式。
__builtin_clz
去改寫 log 函式要對一個 32 bits 的值取 只需要知道該值的 MSB 為 1 的位置即可,所以用 __builtin_clz
去取得 MSB 為 1 ,其左邊 0 的數量,在用 32 減去,就能知道 MSB 為 1 的位置。
以 key 值範圍為 50 為例, 取上限為 6 ,所以可以將層數限制在 6 層內,可以省下些許不必要的空間。
參見 mm/memory-tiers.c
Data sturcture | Access | Search | Insertion | Deletion |
---|---|---|---|---|
Linked list | ||||
Red-black tree | ||||
Skip list |
一般的鏈結串列在資料數目龐大時,其 access, search 會有效能上的瓶頸,而 red-black tree 雖然在平均的時間複雜度上跟 skiplist 一樣,而且可以藉由 read-copy-update (RCU) 去達到 concurrent reads,但卻無法達到 concurrent writes,而 skiplist 可以達到 concurrent writes 與 reads。