contributed by < hsuedw
>
n->next = first
n->pprev = &h->first
為了說明方便,將程式碼還原如下所示。
map_init()
(第 10~25 行)map_init()
會為 hash table 配置記憶體空間,並對 hash table 做初始化。 map_init()
完成後,會產生如下圖所示的 object。NULL
()。map_add()
(第 61~77 行)key
經 hash()
計算後,所對應到的 bucket 不為 empty。也就是有發生碰撞發生。ht[1]
所指向的非環狀雙向鏈結串列。則 kn
指標所指向的新節點會會成為此非環狀雙向鏈結串列的第一個節點。key
經 hash()
計算後,所對應到的 bucket 為 empty。也就是沒有發生碰撞發生。ht[2]
所指向的非環狀雙向鏈結串列。map_get()
與 find_key()
(第 45~59 行)map_get()
經由 find_key()
的協助,在 hash table 中尋找與 key
相吻合的資料節點 ( struct hash_key
型別的 object )。若有找到,則傳回該資料節點中表示資料的 object。否則傳回 NULL。find_key()
中,經 hash()
計算得到 key
的雜湊值。也就是找到可能具有 key
這個資料節點的 bucket。然後,在第 47~51 行的 for
迴圈中,會在此 bucket (即 ht[k]->first
所指向的非環狀雙向鏈結串列)中搜尋具有 key
的資料節點。若有找到,則返回此資料節點。否則,返回NULL。map_deinit()
(第 79~106 行)GOLDEN_RATIO_PRIME
,探討其實作考量乘法運算子為二元運算子 (binary operator),也就是有兩個運算元 (operand) 。若有一個運算元為變數,另一個為常數,那麼就可以用 bitwise left shift 與減法得到乘法的效果,而不必真的動用到硬體的乘法指令。
x * 14
這個 C 語言運算式為例。 因為 ,所以可以把 x * 14
改寫為 (x << 3) + (x << 2) + (x << 1)
或 (x << 4) - (x << 1)
。再看看 linux kernel 是如何計算雜湊值的。
請注意第 6 行與第 22 行。這兩行說明了在計算雜湊值的過程中,會用到一個變數乘以一個常數的運算。
根據 [PATCH v3 05/10] Eliminate bad hash multipliers from hash_32() and hash_64(),早期的 GOLDEN_RATIO_PRIME
的定義如下:
而目前的 GOLDEN_RATIO_PRIME
的定義如下:
以 GOLDEN_RATIO_PRIME_32
做說明。原本的定義中,bit 1 ~ bit 15 全部為 0。如果變數 val
的值的二進位中為 1 的 bit 集中在這個範圍內,那麼計算出來的雜湊值就會很接近,而造成碰撞發生的機會大增。原本的 GOLDEN_RATIO_PRIME_64
也有相同問題。
所以,目前的 GOLDEN_RATIO_32
與 GOLDEN_RATIO_64
分別定義為 0x61C88647
與 0x61C8864680B583EBull
讓 0 與 1 交錯地均勻分布。這樣一來,計算出來的雜湊值發生碰撞的機會就會下降。
COND1 = head->next && head->val == head->next->val
COND1 = head->next && head->val == head->next->val
MMM1
= list_for_each_entry_safe
MMM2
= list_for_each_entry
MMM3
= list_for_each_entry
MMM4
= list_last_entry
延伸問題:
資料結構(LRUCache, LRUNode)
LRUCache
是描述 cache 的資料結構。
capacity
為 LRU cache 能容納的節點數。count
為目前 LRU cache 內的節點數目。當 count 等於 capacity 時,表示 LRU cache 已滿。LRUNode
是描述 cache 中資料節點的資料結構。
key
與 value
為資料節點的 key 與 value。LRUCache *lRUCacheCreate(int capacity)
lRUCacheCreate
會配置記憶體空間給一個型態為 LRUCache
的 object 並對它做初始化。 LRUCache
會產生如下圖所示的 object。
LRUCache
object 中有兩 dhead
與 hhead
兩種雙向鏈結串列。
dhead
是一個以資料節點被存取的時間序為排列順序的雙向鏈結串列。最近被存取過的資料節點會被放在 dhead
所指向之雙向鏈結串列的最前端。最早被存取過的資料節點則被放在 dhead
所指向之雙向鏈結串列的尾端。hhead
則是一個 hash table。被插入的資料節點的 key 所對應的 hash 為 k 時,則此資料節點會被插入 hhead[k]
所指向的雙向鏈結串列中。dhead
與 hhead[k]
所管理。
void lRUCachePut(LRUCache *obj, int key, int value)
key
的 hash
。這裡的實作是用取餘數的方式算 hash。capacity
為 3。key
為 0, value
為 0。key
為 1, value
為 1。
LRUNode
的 object。並增加 LRU cache 中的節點個數 (count
)。dhead
所指向之雙向鏈結串列的最前端 (原本最前端的節點為 key
= 0, value
= 0)。hhead[1]
所指向的雙向鏈結串列的最前端 (原本為 empty)。key
更新為 1。value
更新為 1。
key
= 3, value
= 6 的節點。
for
迴圈會找到已存在 hhash[0]
所指的雙向鏈結串列中那個 key
= 3 的節點。然後操作 dhead
所指向的雙向鏈結串列,將此資料節點移到此雙向鏈結串列的第一個節點,再將此節點的 value
更新為 6。但 hhead[0]
所指之雙向鏈結串列維持不動。完成狀況如下圖所示。
key
= 4, value
= 4)。
則第 13 行的 if
條件式成立,並執行第 14~16 行。將 dhead
所指之雙向鏈結串列的最後一個節點 (key
= 6, value
= 6)自 ddhead
與 hhead[0]
雙向鏈結串列中移除(但不刪除),並以 lru
指標指向此被刪除的節點。
接著執行第 21~24 行。
lru
所指向的節點的 key
更新為 4。lru
所指向的節點插入 dhead
所指向之雙向鏈結串列的最前端。lru
所指向的節點插入hhead[1]
所指向的雙向鏈結串列的最前端 (原本為 empty)。lru
所指向的節點的 value
更新為 4。
以上討論的幾種情況已涵蓋整個 lRUCachePut()
的主要程式碼。
int lRUCacheGet(LRUCache *obj, int key)
假設目前 LRU cache 的狀況如下圖所示。
若要找的資料節點的 key
不為 0、3 或 6,則返回 -1。
若要找的資料節點的 key
為 6。則第 5~10 行的 for
迴圈會在 hhead[0]
所指的雙向鏈結串列中找到最後一個節點的 key
為 6。然後將此節點移到 ddhead
所指的雙向鏈結串列的第一個節點。 完成後,如下圖所示。
void lRUCacheFree(LRUCache *obj)
lRUCacheFree
會釋放 LRU cache 所占用的記憶體空間。lRUCachePut()
與 lRUCacheGet()
的討論得知,ddhead
所指的雙向鏈結串列涵蓋整個 LRU cache 的所有資料節點。所以只要遍歷此雙向鏈結串,並逐一移除節點,再釋放節點記憶體空間,就可以清除所有的資料節點。這就是第 4~7 行 for
迴圈所做的事。--left
++right
延伸問題:
heads
指標所指向的 object。此 hash table 可容納的節點數為 nums
陣列的大小。find()
函式負責在 hash table 中尋找指定的數值 (nums[i]
)。若此數值存在 hash table 中,則返回該資料節點。否則,返回 NULL。for
迴圈對 hash table 進行初始化。假設 nums = [100,4,1,200,1,100,3,2]
,則初始化後的 hash table 如下圖所示。for
迴圈把 nums
陣列中的所有元素插入 hash table 中。在插入的過程中,若發現 nums[i]
已經存在 hash table 中,則不予插入。所以 nums[4]
與 nums[5]
會被跳過。這段程式碼完成後,hash table 的狀態如下圖所示。for
迴圈從頭開始逐一走訪 nums
陣列中的每個元素。而針對某一元素 nums[i]
,在第 43~60 行的 while
迴圈中,以 nums[i]
為中心,在 hash table 中向左方找 nusm[i] - 1
,向右方找 nums[i] + 1
。
nums
陣列中有連續數列 (consequtive sequence),則會分布在 nums[i]
的左右兩邊。所以只要在 hash table 中找到連續數列中的任一元素,就可以找到其他元素。進而算出最長連續數列的長度。while
迴圈中,會把找到的連續數列自 hash table 中移除。所以,某個連續數列的長度只會被計算一次。2022 年 Linux 核心設計第 1 週隨堂測驗 (A)
2022 年 Linux 核心設計第 1 週隨堂測驗 (B)