contributed by < lintin528
>
測驗一 實作非遞迴 (non-recursive; iterative) 的快速排序法
與在 lab0-c 之實作不同,此處使用間接指標的方式完成鏈結串列的節點移動,是因為這邊需要對節點的 head(即 *list)
做修改,因此不可使用 pass by value 的參數傳遞方式。
void list_add(node_t **list, node_t *node_t)
{
node_t->next = *list;
*list = node_t;
}
這邊遇到問題,若使用 subgraph 將導致無法套用
rankdir=LR;
,尚不知道原因,先以多段 code 分割圖片。
根據名稱以及後半段 quick_sort
的使用情形,可以推斷該函式功能為回傳鏈結串列最後的節點指標,以 left
, 即指向第一個節點的指標的指標為輸入參數,向鏈結串列尾端遍歷,因此可得知 AAAA
為 (*left)->next
。
node_t *list_tail(node_t **left)
{
while ((*left) && (*left)->next)
left = &(AAAA);
return *left;
}
類似於 list_tail
,但新增計數器並將節點個數回傳,因此可以判斷 BBBB
為遍歷過程中間接指標向下一個節點更新,同樣為 (*left)->next
。
int list_length(node_t **left)
{
int n = 0;
while (*left) {
++n;
left = &(BBBB);
}
return n;
}
分段拆解 quick_sort
的功能與流程:
首先,先理解到這邊並不是以遞迴呼叫的方式逐段排序,而是透過 stack
將各次迭代的起點、終點存儲在陣列後,透過改變 i
去實作原本的遞迴功能。其中, begin[], end[]
可視為該次迭代的作用範圍,一開始為整個鏈結串列,在第一輪時分別為該鏈結串列的第一個節點與最後一個節點,第一輪結束後分別儲存 "pivot
左側所有節點的第一個與最後一個節點"、"pivot
節點"、"pivot
右側所有節點的第一個與最後一個節點",因此 DDDD
與 EEEE
分別為 list_tail(&left), list_tail(&right)
。
進入 while (i >= 0)
迴圈後,主要的切分過程透過以下程式碼來實現:
while (p) {
node_t *n = p;
p = CCCC;
list_add(n->value > value ? &right : &left, n);
}
begin[i] = left;
end[i] = DDDD;
begin[i + 1] = pivot;
end[i + 1] = pivot;
begin[i + 2] = right;
end[i + 2] = EEEE;
觀察 while 迴圈,可以發現它的功能是遍歷該次 begin
到 end
節點,並將開頭節點設定為 pivot
,將該範圍中所有比 pivot
大的節點放入 right
鏈結串列,反之(含等於)則放入 left
,因此這裡可以知道 CCCC
應為 p->next
。在最後將 left = right = NULL;
是為了在下一次迭代中儲存該次的 begin, end
,一開始理解這個內部的中止條件 p
時產生了一個誤解,以為會需要使用 end
與當下指針的判斷去決定迴圈是否結束,但後來發現 end
僅用以判斷該區間是否為一個節點的區間,在切分 left, right
時其實是新增兩個鏈結串列,因此當 p == NULL
時,其實就完成該區間的遍歷了。
原本的鏈結串列:
通過 i = 0
第一次迭代後:
透過上圖,可以看出原本的鏈結串列被分成三個部分,且在最後以 begin, end
儲存分段後的鏈結串列之起點、終點,可由此推斷出 DDDD, EEEE
即為 list_tail(left), list_tail(right)
。
最後,原本的一個鏈結串列應該會變成 n 個僅有一個節點的鏈結串列,就是由上面的 left, right
切分出來的,在這個遞迴過程中即為終止條件 (L == R)
的狀況,考慮到 i
的變化情形,將會從最右側節點開始觸發這個終止條件,當我們進行 n 次的 list_add(&result, L);
即完成該鏈結串列的排序。
else {
if (L)
list_add(&result, L);
i--;
}
第一次迭代 (i = 0
):
第二次迭代 (i = 2
):
第三次迭代 (i = 3
) 結束後的結果鏈結串列:
觀察第二次迭代可以發現 left = NULL
,且 list_length(node_t **right) = 1
,將結果儲存在 begin, end
時,目前應為:
begin[2] = NULL
begin[3] = "pointer to 5"
begin[4] = "pointer to 7"
end[2] = NULL
end[3] = "pointer to 5"
end[4] = "pointer to 7"
再進入下次迭代 (i = 4
) ,這次將不會通過 (L != R)
之判斷,因此會進行
list_add(&result, L);
也就是將儲存 value = 7 的節點加入結果的鏈結串列內,結束後 i--
,然後 (i = 3
) 時做同樣的動作將 value = 5 的節點存入結果, (i = 2
) 時,因為 begin[2] = end[2] = NULL
,不會通過 if(L) 的判斷式,跳過, (i = 1
) 時將 pivot 存入結果,最終回到 begin[0]
並重複以上過程,直到最後一部 begin[0]
將等於 end[0]
,將所有節點加入結果的串列後, i == -1
,結束所有迴圈,排序結束。
可改善:終止條件 L or R 為NULL時
對於排序的效能分析而言,最常被使用到的有 quick sort 與 merge sort ,Timsort 這個演算法考慮到一般資料其實並不是完全隨機的,相比起以往的 merge sort , Timsort 在處理已經部分排序的資料時效能會更佳,一部分是因為減少了多餘的 devide 與 compare 次數,一部分是考慮到 Cache 的 Spacial locality。
Timsort 的步驟可以大約拆成兩個部分,第一步是從當下的起點與下個起點做比較,決定這個 run
為升序或降序,直到串列不符合升序、降序,並將這個 run
分隔出來,即 find_run
,stack
個數加一,第二步是在每次的 find_run
結束後,檢查 runs
的分布情形是否符合條件:
A 的長度要大於 B 和 C 的長度總和。
B 的長度要大於 C 的長度。
若不符合,則有相對應的 A+(B+C) 或 (A+B)+C 的合併操作,以保持 Timsort 的效能穩定,這就是 Timsort 一邊進行 run
的劃分一邊進行 merge
的設計方式。
這邊僅討論在 Timsort 操作過程中使用到的函式,其餘即為初始化產生隨機鏈接串列以及 Timsort 之後的檢驗。
比較 a,b
兩個鏈結串列中的第一個元素大小,回傳值為 a 第一個元素大小 - b 第一個元素大小
,後面被使用到的情形是在 merge
以及 find_run
的過程,僅考慮回傳值 res
的正負號。
if (a == b)
return 0;
int res = list_entry(a, element_t, list)->val -
list_entry(b, element_t, list)->val;
if (priv)
*((int *) priv) += 1;
return res;
這裡會牽涉到 find_run
中,對於每個 run
會將長度存放至 "第二個元素的 prev" 以 list_head *
型態儲存。
static inline size_t run_size(struct list_head *head)
{
if (!head)
return 0;
if (!head->next)
return 1;
return (size_t) (head->next->prev);
}
從起點 list
開始,在第一次比較第一個即第二個元素的時候決定為升序或降序,且降序的情況需要在 run
的建立過程中將鏈結串列反轉且在這裡每個 run
的儲存將變為單向的鏈結串列,最後將回傳一個 pair
結構體,result
,result.head
將指向該 run
的第一個節點,而 result.next
指向原本鏈結串列中,該 run
區間後的一個節點。
以題目範例 123432478
為例,執行結束後(未合併)示意圖如下:
將兩個 run
區間 a,b
合併,將會在比較完 a,b
鏈結串列的第一個元素後,將較小的節點放置於 head
的最後一位,相等則取 a
以確保 sort stability
,因此這裡使用間接指標的方式儲存,因此 AAAA
即為指向起始位置的指標的指標 &head
,而在後續的合併過程,同樣透過改變 *tail
的方式間接更新鏈結串列中最後一位的下一個節點,因此 BBBB
與 CCCC
分別為更新完後指向下個元素指標的指標, &a->next, &b->naxt
。
部分更新過程:
if (cmp(priv, a, b) <= 0) {
*tail = a;
tail = BBBB;
a = a->next;
if (!a) {
*tail = b;
break;
}
}
這裡以較常見的情況分析,此處可以概括為違反這兩種情況:
情況 1:A 的長度要大於 B 和 C 的長度總和。
情況 2:B 的長度要大於 C 的長度。
另外解析一下 merge_at
即為將目前所有的 runs
中最後一個 run(tp)
,與 tp->prev
合併並更新 tp
指標;當不滿足情況一時若 A<C 則將 A,B 合併,反之則將 B,C 合併。
if ((n >= 3 && run_size(tp->prev->prev) <= run_size(tp->prev) + run_size(tp))
if (run_size(tp->prev->prev) < run_size(tp)) {
tp->prev = merge_at(priv, cmp, tp->prev);
} else {
tp = merge_at(priv, cmp, tp);
if (run_size(tp->prev) <= run_size(tp))
tp = merge_at(priv, cmp, tp);
在整個鏈結串列成功建立完所有的 runs
後,因為途中的 merge_collapse
執行後有可能部分的 runs
都還是滿足 Timsort 的主要條件,因此為了進行最後的 merge_final
,需要透過 merge_force_collapse
將存在的 runs
減少為一至兩個,在這裡 tp
一開始指向結構中的最後一個 run
,透過比較 A,C (C 為最後一個 run ) 的長度決定該如何合併,直至剩下兩個以下的 runs
。
static struct list_head *merge_force_collapse(void *priv,
list_cmp_func_t cmp,
struct list_head *tp)
{
while (stk_size >= 3) {
if (run_size(tp->prev->prev) < run_size(tp)) {
tp->prev = merge_at(priv, cmp, tp->prev);
} else {
tp = merge_at(priv, cmp, tp);
}
}
return tp;
}
雙向鏈結串列的結構在建立 runs
時已被破壞,並會透過 timsort
最後的 merge_final
以及 build_prev_link
進行重建,本函式用意是在最後合併完成後未在結果內的鏈結串列拼接上結果串列的尾端,因此可以判斷 DDDD
和 EEEE
分別為 tail->next
與 head->prev
。
另外在 timsort
內當滿足 if (stk_size <= FFFF)
判斷式時,其實代表在 merge_force_collapse
就已經完全合併完成,只差在需要將整個單向串列 stk0
拼接到結果 head
之後,並完成雙向指標的修復,因此 FFFF
即為 1
,不需再作最後的 merge_final
。
static void build_prev_link(struct list_head *head,
struct list_head *tail,
struct list_head *list)
{
tail->next = list;
do {
list->prev = tail;
tail = list;
list = list->next;
} while (list);
/* The final links to make a circular doubly-linked list */
DDDD = head;
EEEE = tail;
}
timsort
過程的最後,只剩下兩個 runs
時,重複比對兩個串列的頭個節點,並將較小的節點插入 head
串列的尾端,以下為 a
較 b
小(含)的情況,將 a
的第一個節點移動後,若 a
已無節點,則將整個剩餘的 b
拼接至 head
的尾端,完成排序。
,並在最後
if (cmp(priv, a, b) <= 0) {
tail->next = a;
a->prev = tail;
tail = a;
a = a->next;
if (!a)
break;
}
...
build_prev_link(head, tail, b);
以題目的 inorder = [9,3,15,20,7]
為例,可繪製出 :
透過函式名稱可以得知該函式的功用為在 h
指向的鏈結串列中新加入一個節點至 h->first
指向的位置,首先若該鏈結串列中有節點的話,更新原本第一個節點的 pprev
使其儲存新加入節點之 "指向下一個節點的指標的指標" ,將 n->next
設定為原本節點的指標,因此 AAAA
即為原本的第一個節點 h->first
,再來就是將 h->first
指向新加入節點,並更新 n->pprev
。
static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h)
{
if (h->first)
h->first->pprev = &n->next;
n->next = AAAA;
n->pprev = &h->first;
h->first = n;
}
示意圖:
根據 hash table 建立的規則,先對輸入的 num
做一次 hash function ,決定該在in_heads
中的哪一個鏈結串列做搜尋,接下來透過 hlist_for_each
遍歷該鏈結串列,因此 BBBB
應為該鏈結串列的 hlist_head
之位址,即為 &head[hash]
;再來為了獲得該鏈結串列中節點的值,必須先從 hlist_node
找出外部結構 order_node
,再去搜尋該結構中的值數值 on->val
,因此 CCCC
應該為透過 hlist_node
找出外部結構 order_node
之巨集,即為 list_entry
,這裡值得一提的是,不同以往透過遍歷鏈結串列搜索 O(n),透過 hash table 的方式去做搜尋可以減少搜尋的時間到常數時間 O(1)。
static int find(int num, int size, const struct hlist_head *heads)
{
struct hlist_node *p;
int hash = (num < 0 ? -num : num) % size;
hlist_for_each (p, BBBB) {
struct order_node *on = CCCC(p, struct order_node, node);
if (num == on->val)
return on->idx;
}
return -1;
}
此函式的功能為將節點加入 hash table 中對應值的鏈結串列的頭部,因此 DDDD
應為 &heads[hash]
static inline void node_add(int val,
int idx,
int size,
struct hlist_head *heads)
{
struct order_node *on = malloc(sizeof(*on));
on->val = val;
on->idx = idx;
int hash = (val < 0 ? -val : val) % size;
hlist_add_head(&on->node, DDDD);
}
可以看到他有六個參數,每個的功用為:
int *preorder 存取前序的陣列第一個位址
int pre_low 該次遞迴所設定的前序陣列下限
int pre_high 該次遞迴所設定的前序陣列上限
int *inorder 存取中序的陣列第一個位址
int in_low 該次遞迴所設定的中序陣列下限
int in_high 該次遞迴所設定的中序陣列上限
struct hlist_head *in_heads 儲存 hash table 結構體的位址
int size 為 hash table 之欄位總數
TreeNode
主要的的執行過程:第一步,配置一個新的 struct TreeNode
記憶體空間,並設定該 TreeNode 之數值為該次遞迴中 *preorder
中的第 pre_low
個元素,找到這個數值對應到 *inorder
中的 index ,並利用這個值決定進入左右子樹的遞迴。
struct TreeNode *tn = malloc(sizeof(*tn));
tn->val = preorder[pre_low];
int idx = find(preorder[pre_low], size, in_heads);
第二步,左子樹遞迴:
以第一次執行為例,這次的遞迴會將整個左子樹作為參數傳入,在 root 的左子樹中, *preorder
之範圍改變為 pre_low + 1, pre_low + (idx - in_low)
,按照前序 "中左右" 的搜尋順序,將一開始作為 root 的第一個元素移除,因此 pre_low
傳入的是 pre_low + 1
,而 pre_low + (idx - in_low)
的部分可以以下圖的方式理解:
這裡選取了 3 ,因此左子樹可以判斷出在 inorder 的左半邊,透過 (idx - in_low)
計算出個數後,將反映到 pre_high
上,即為 pre_low + (idx - in_low)
,也就是說在第一次的左子樹遞迴中, preorder
的範圍就被界定為 (1, 1)
,而 inorder
的範圍被界定為 (0, 0)
。
第三步,右子樹遞迴則類似於上面的判斷方式,透過 inorder
中的位置與 low_high
判斷右子樹的元素個數,並取出對應的 preorder
與 inorder
範圍作為參數傳入 dfs
,在這裡 preorder
的範圍被界定為 (2, 4)
,而 inorder
的範圍被界定為 (2, 4)
。
tn->left = dfs(preorder, pre_low + 1, pre_low + (idx - in_low), inorder,
in_low, idx - 1, in_heads, size);
最後觸發中止條件 in_low > in_high || pre_low > pre_high
時,回傳 TreeNode
,這個中止條件將在只有一個元素的時候進行 dfs
後的左右子樹遞迴觸發。
對於可快速存取但容量有限的 Cache 而言,通常會以 Spatial Locality 或 Temporal Locality 為考量選擇不同的排班策略,LRU 就是其中考慮 Temporal Locality 較多的一種,題目中以 dhead
這個鏈結串列儲存 Cache 中的使用情形,當在 Cache 已滿的時候發生 Cache miss ,則會將此鏈結串列的最後一個節點移出,即為最久沒被使用到的節點。
將目前指向的 hlist_node 刪除,首先用 next
儲存目前節點指向下個節點的指標, pprev
儲存指向目前這個節點的指標,透過間接指標 *pprev = next;
的方式將上一個節點的 next
指標指向下一個節點,最後則是更新 next
節點的 pprev
,因此 EEEE
即為 next->pprev
。
void hlist_del(struct hlist_node *n)
{
struct hlist_node *next = n->next, **pprev = n->pprev;
*pprev = next;
if (next)
EEEE = pprev;
}
list_add 的功用即為透過呼叫 __list_add
,將 _new
節點加入到 head
的後面,但觀察起來我認為不太需要 __list_add
這個函式,可以簡單透過像是 lab0-c 中的 "list.h" 中的方式,直接在 list_add
中宣告一個新的 struct list_head *next = head->next;
就可以完成一樣的操作了。
void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
void list_add(struct list_head *_new, struct list_head *head)
{
__list_add(_new, head, head->next);
}
在看到這兩個結構體後我一開始想的是為何會需要兩個鏈結串列去儲存 Cache 中的資料,其實僅透過 dhead
就可以完整的搜尋整個 Cache 的使用情形了,但後來想起上一題的 hash table 其實就是為了縮短搜尋的時間,在 Cache 中 key-value pair 的搜尋時間優化也是相對重要的。
LRUCache 中四個參數分別代表:
int capacity; 整個 Cache 可容納 LRUNode 的最大總數
int count; 目前 Cache 中存在使用中的 LRUNode 總數
struct list_head dhead; 追蹤使用狀態的雙向鏈結串列,最後使用的 LRUNode 將在最後面
struct hlist_head hhead[]; 存放 Cache 中各個 hash table 欄位的存放情形
LRUNode 中四個參數分別代表:
int key; 對應到 hash table 的其中一個欄位,並比對欄位中是否為同個節點
int value; 儲存該 LRUNode 的數值
struct hlist_node node; LRUNode 在 hash table 中對應欄位所指向的節點
struct list_head link; LRUNode 在使用順序的鏈結串列中的節點
對於 LRUNode 而言, node
與 link
作為 entry
來進行管理。
創立一個新的 Cache 結構體,主要在執行記憶體配置、設定初始參數,對 dhead
初始化、初始化 capacity
個 hash table 欄位的 first
指標。
LRUCache *lRUCacheCreate(int capacity)
{
LRUCache *cache = malloc(2 * sizeof(int) + sizeof(struct list_head) +
capacity * sizeof(struct list_head));
cache->capacity = capacity;
cache->count = 0;
INIT_LIST_HEAD(&cache->dhead);
for (int i = 0; i < capacity; i++)
INIT_HLIST_HEAD(&cache->hhead[i]);
return cache;
}
釋放整個 Cache 的記憶體空間,利用 list_for_each_safe
在遍歷每個節點的同時,可以對當下的 pos
進行釋放,因為有 safe node 'n' 保存著下個節點的位址,再來使用 list_entry
透過 list_head *pos
找到上層 LRUNode
結構,而 pos
在結構內的變數名稱為 link
,因此 FFFF
即為 link
,之後對 list_head
進行釋放,因此 GGGG
應為 pos
。
另外這邊我想到一個問題是雖然在 Cache 結構體內 hash table 的儲存型態 hhead[]
不為指標,不需要 free ,但在 obj
指標被釋放後期時喪失了 hhead[hash]
的造訪方式,那他內部所儲存的 first
及後面的 hlist_node
鏈結串列與指標所耗費的記憶體空間沒有被釋放出來,又或者說在執行 lRUCacheFree
之前, hash table 相關的釋放會先被執行。
void lRUCacheFree(LRUCache *obj)
{
struct list_head *pos, *n;
list_for_each_safe (pos, n, &obj->dhead) {
LRUNode *cache = list_entry(pos, LRUNode, FFFF);
list_del(GGGG);
free(cache);
}
free(obj);
}
接下來的 Get 與 Put 操作可以對應到 Cache 中的 read 與 write 操作,首先是 lRUCacheGet
,該函式是透過 hlist_for_each
遍歷該 key 值對應到的 hash table 欄位中的 hlist_node
,在 obj
這個 Cache 中查找有無 key
對應到的 LRUNode
,若有則將該節點的 value
讀出,若無則回傳 -1
,遍歷的過程中先使用 list_entry
透過目前的 hlist_node *pos
查找上層結構,而 pos
在 LRUNode
中為 node
成員,因此 HHHH
應為 node
,另外,因為我們使用 LRU 排程的關係,在我們讀取或寫入該 LRUNode
後,應該要將他在 dhead
鏈結串列中移至最前面,代表最近一次使用,並且 LRUNode
儲存在 dhead
鏈結串列中的成員為 list_head link
,因此此處的 IIII
應為 &cache->link
。
int lRUCacheGet(LRUCache *obj, int key)
{
int hash = key % obj->capacity;
struct hlist_node *pos;
hlist_for_each (pos, &obj->hhead[hash]) {
LRUNode *cache = list_entry(pos, LRUNode, HHHH);
if (cache->key == key) {
list_move(IIII, &obj->dhead);
return cache->value;
}
}
return -1;
}
分析時,將這個函式大致拆解為兩個部分:
LRUNode
存在,若有則儲存為變數 cache
:hlist_for_each
遍歷 key 對應之 hash table 欄位的鏈結串列,並做 list_entry
取出上層結構後查找是否有成員 key 相同者,若有則在使用順序鏈結串列 dhead
中放入最前方,因此可看出 KKKK
即為 &c->link
。LRUNode *cache = NULL;
int hash = key % obj->capacity;
struct hlist_node *pos;
hlist_for_each (pos, &obj->hhead[hash]) {
LRUNode *c = list_entry(pos, LRUNode, JJJJ);
if (c->key == key) {
list_move(KKKK, &obj->dhead);
cache = c;
}
}
a : Cache 已滿,即 obj->count == obj->capacity
,則將位於 dhead
最後一位之 LRUNode->link
取出後放至第一位,另外透過 hlist_del
與 hlist_add_head
在該 key 對應到的 hash table 鏈結串列中將此 LRUNode->node
移至第一位,這裡目前不知道改變節點在 hash table 欄位中的順序會影響什麼,推測是因為注重 Temporal Locality 的方面,考慮到多次使用同個 LRUNode
,這麼做可以減少在 hash table 中搜尋的時間。
b:在 Cache 未滿的狀況下需要新配置一個 LRUNode
記憶體空間並直接將該節點中的 node, link
成員分別放置於 &obj->hhead[hash], &obj->dhead
指向的兩個鏈結串列之開頭,並讓 count
計數器加一,紀錄目前 Cache 中被使用的欄位個數
if (!cache) {
if (obj->count == obj->capacity) {
cache = list_last_entry(&obj->dhead, LRUNode, link);
list_move(&cache->link, &obj->dhead);
hlist_del(&cache->node);
hlist_add_head(&cache->node, &obj->hhead[hash]);
} else {
cache = malloc(sizeof(LRUNode));
hlist_add_head(&cache->node, &obj->hhead[hash]);
list_add(&cache->link, &obj->dhead);
obj->count++;
}
cache->key = key;
}
cache->value = value;
最後在觀察這整個結構之後,發現他應該是一個 fully associative cache ,之前的認知是在一個容量較小的 Cache 中,可透過 capacity
個比較器直接查找 key 對應的 block 是否存在於 Cache 中,但在這個實作中是使用 hash table 查找,發現自己對於在硬體層面與軟體層面之間關聯性的理解不太明確。
ffs 即為 find first set ,從最低位向高位搜尋第一個 1 ,由此可以分析這些 if else 的功效,第一步則是判斷從 0-31 bit 是否有設置 1,若無,則將 word 向右移 32 bit ,實際上就是讓下次的比對從第 32 bit 的位置開始,因此 AAAA
應為檢查 word 的 0-31 bit 是否存在 1,可得 AAAA
= 0xffffffff
,這個函式相當於每次將 word 切分使用二分法的方式去搜尋。
int num = 0;
#if BITS_PER_LONG == 64
if ((word & AAAA) == 0) {
num += 32;
word >>= 32;
}
#endif
if ((word & 0xffff) == 0) {
num += 16;
word >>= 16;
}
if ((word & 0xff) == 0) {
num += 8;
word >>= 8;
}
if ((word & 0xf) == 0) {
num += 4;
word >>= 4;
}
if ((word & 0x3) == 0) {
num += 2;
word >>= 2;
}
if ((word & 0x1) == 0)
num += 1;
return num;
這邊得先提到 __clear_bit
為在進行 fns
時,是透過進行 n
次的 ffs
,並在每次搜尋到第一個 1 後,將其從 word 中去除所使用的函式,因此它利用 BIT_MASK
產出 第 nr
位為 1 ,其餘為 0 的 mask,並做一次反向並透過 & 運算將當下的 1 過濾掉,因此 BBBB
應為 ~mask
,另外這裡的 BIT_WORD
則是為了若 nr > 64
也就是一個 unsigned long 長度時,需要做記憶體位址的變更以查找更高位的位元在做運算。
static inline void __clear_bit(unsigned long nr, volatile unsigned long *addr)
{
unsigned long mask = BIT_MASK(nr);
unsigned long *p = ((unsigned long *) addr) + BIT_WORD(nr);
*p &= BBBB;
}
示意圖:
即為在 word
所在的記憶體空間中,第 n 個 1 的位置,若找不到則回傳 BITS_PER_LONG
,即 64 ,可以看到操作過程就是 找到第一位 -> 篩選掉 -> 重複 n 次
,值得一提的是在呼叫 fns 的時候,只會限制在一個 unsigned long 記憶體空間內,因此不理解為何在 __clear_bit
會需要執行 unsigned long *p = ((unsigned long *) addr) + BIT_WORD(nr)
這樣的動作,在巨集 FIND_NTH_BIT
中的呼叫, 也是先將 fns(tmp, nr)
中的 tmp
設定為 addr[idx]
,我想不出何種情況 __clear_bit
的 input bit
會大於 64 ,因此我認為在這個實作中他的 BIT_WORD(nr)
將總是為 0,可以刪除。
while (word) {
unsigned int bit = __ffs(word);
if (n-- == 0)
return bit;
__clear_bit(bit, &word);
}
return BITS_PER_LONG;
這個函式將在 size
個位元中尋找第 n 個 1 ,分成三種情況,第一種若是要尋找的位元比 size
還要或是一樣大,則必定回傳 size
,第二種情形為當 size
不超過一個 unsigned long 記憶體大小時,可直接呼叫 fns
而不用先透過巨集 FIND_NTH_BIT
分段查找多個 unsigned long 記憶體空間,最後則是超過一個 unsigned long 記憶體大小時,會利用 addr[idx]
分段造訪。
if (n >= size)
return size;
if (small_const_nbits(size)) {
unsigned long val = *addr & GENMASK(size - 1, 0);
return val ? fns(val, n) : size;
}
return FIND_NTH_BIT(addr[idx], size, n);
在這個 for 迴圈,就是在造訪多個 unsigned long 記憶體空間, tmp
將隨著 idx 改變為 FETCH
,也就是 addr[idx]
,在每個迭代中,會先透過 hweight_long
檢查該次的記憶體空間內有幾個 1 ,若 w > nr
即表示最終的第 n 個 1 位於此記憶體空間內,並且在最後一次的搜尋中,size
不為 64 的倍數,最高位的記憶體空間會先經過 if (sz CCCC BITS_PER_LONG)
中的過濾去將 size
最高位以後的位元篩選掉,因此判斷式中的 CCCC
應為 %
,在不整除的情況下才會進行。
for (idx = 0; (idx + 1) * BITS_PER_LONG <= sz; idx++) { \
if (idx * BITS_PER_LONG + nr >= sz) \
goto out; \
\
tmp = (FETCH); \
w = hweight_long(tmp); \
if (w > nr) \
goto found; \
\
nr -= w; \
} \
\
if (sz CCCC BITS_PER_LONG) \
tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz);
另外 BITMAP_LAST_WORD_MASK
巧妙的運用二補數去將超出 size
最後一塊記憶體空間的位元刪除,因為一次作業的單位為 64 bit ,所以進行 & (BITS_PER_LONG - 1)
,可以得到最高位元含右側全為 1 左側全為 0 的 64 bit 二進位數,基本上與 GENMASK
,l = 0
時功用相同,示意圖如下:
其中 nth bit = size % BITS_PER_LONG