# 2024q1 Homework2 (quiz1+2)
contributed by <[`hungyuhang`](https://github.com/hungyuhang)>
## 第 1 週測驗題
### 測驗 `1`
觀察程式碼,可以發現 `main` 程式碼做了以下事情:
1. 產生 100000 個數字並且執行 `shuffle` 將其順序打亂
2. 將這 100000 個數字使用 `list_construct` 函式轉換成鏈節串列
3. 使用 `quick_sort()` 函式對鏈節串列進行排序
4. 檢查排序結果並釋放記憶體
#### quick_sort() 函式解析
接下來進一步去觀察 `quick_sort()` 函式。首先透過呼叫 `list_length()` 來取得串列長度。從函式的功能,推測 BBBB = (*left)->next 。
在進入迴圈之前,程式碼先將整個鏈節串列的頭跟尾推入到 `begin` 跟 `end` 這兩個堆疊內:
```c
begin[0] = *list;
end[0] = list_tail(list);
```
從這裡不難看出 `list_tail()` 函式回傳的東西就是 list 的最後一個節點,所以可以推測 AAAA 的值是 `(*left)->next` 。
在主迴圈的一開始,可以看到程式碼會從 `begin` 跟 `end` 這兩個堆疊內取出這次要處理的子鏈節串列的頭尾位置:
```c
while (i >= 0) {
node_t *L = begin[i], *R = end[i];
```
並且在內部的迴圈依照數字大小來決定要把每個節點放到 `right` 還是 `left` 上面:
```c
while (p) {
node_t *n = p;
p = CCCC;
list_add(n->value > value ? &right : &left, n);
}
```
為了達成上述的功能,所以在 CCCC 的位置填上 `p->next` 。
完成這個步驟後,鏈節串列會被分成三個串列:
- `left` :裡面所有節點都比 pivot 還要小
- `pivot` :裡面只含有 pivot 一個節點
- `right` :裡面所有節點都比 pivot 還要大
到這裡可以推測:
- DDDD 應該要填入 `list_tail(left)`
- EEEE 應該要填入 `list_tail(right)`
並且程式碼會將這三個串列依序加到 `begin` 跟 `end` 兩個堆疊當中。
可以觀察到程式碼會把長度僅為 1 的 `pivot` 串列也加到堆疊裡面,那當程式碼從堆疊取出的串列長度為 1 的時候怎麼辦呢?以下是程式碼的處理方式:
```c
} else {
if (L)
list_add(&result, L);
i--;
}
```
這段程式處理得很簡潔,就是把這個串列裡的唯一節點加到 `result` 這個串列上面。
因為最後所有的子串列都會被切分成長度為 1 的串列,所以每個節點都會依序被加到 `result` 串列上。
#### 圖解範例
先假設有一個串列,並且排序方式如下:
```graphviz
digraph graphname{
node[shape=box]
rankdir=LR
{
rankdir = LR
n1[label=1]
n2[label=2]
n3[label=3]
n4[label=4]
n5[label=5]
}
n3->n4->n5->n1->n2
}
```
1. 選定 pivot 為串列中第一個元素(也就是 3 ),並且將比 pivot 小的節點加到 left , 比 pivot 大的節點加到 right :
```graphviz
digraph graphname{
node[shape=box]
rankdir=LR
{
rankdir = LR
n1[label=1]
n2[label=2]
n3[label=3]
n4[label=4]
n5[label=5]
}
{
left[shape=plaintext,fontcolor=blue]
pivot[shape=plaintext,fontcolor=red]
right[shape=plaintext,fontcolor=blue]
}
left->n1->n2
pivot->n3
right->n4->n5
}
```
2. 隨後將這三個串列加到 stack 裡面,在這個時間點, stack[2] 是堆疊的頂端:
```graphviz
digraph graphname{
node[shape=box]
rankdir=LR
{
rankdir = LR
n1[label=1]
n2[label=2]
n3[label=3]
n4[label=4]
n5[label=5]
}
{
left[label="stack[0]",shape=plaintext,fontcolor=black]
pivot[label="stack[1]",shape=plaintext,fontcolor=black]
right[label="stack[2]",shape=plaintext,fontcolor=black]
}
left->n1->n2
pivot->n3
right->n4->n5
}
```
3. 接下來程式碼會從 stack 的最頂端取出子串列然後重複進行步驟 2 跟 3 的操作
```graphviz
digraph graphname{
node[shape=box]
rankdir=LR
{
rankdir = LR
n1[label=1]
n2[label=2]
n3[label=3]
n4[label=4]
n5[label=5]
}
{
left[label="stack[0]",shape=plaintext,fontcolor=black]
pivot[label="stack[1]",shape=plaintext,fontcolor=black]
right[label="stack[2]",shape=plaintext,fontcolor=black]
righa[label="stack[3]",shape=plaintext,fontcolor=black]
}
left->n1->n2
pivot->n3
right->n4
righa->n5
}
```
4. 當從 stack 頂端取出的串列長度是 1 的話,就將該串列插入到 result 上面。所以節點 3 , 4 跟 5 會依序被插入到 result 上面:
```graphviz
digraph graphname{
node[shape=box]
rankdir=LR
{
rankdir = LR
n1[label=1]
n2[label=2]
n3[label=3]
n4[label=4]
n5[label=5]
}
{
left[label="stack[0]",shape=plaintext,fontcolor=black]
result[label="result",shape=plaintext,fontcolor=black]
}
left->n1->n2
result->n5->n4->n3
}
```
5. 最後當 stack 被清空之後, result 所指向的串列就會是一個已經排序好的串列:
```graphviz
digraph graphname{
node[shape=box]
rankdir=LR
{
rankdir = LR
n1[label=1]
n2[label=2]
n3[label=3]
n4[label=4]
n5[label=5]
}
{
left[label="stack[0]",shape=plaintext,fontcolor=black]
result[label="result",shape=plaintext,fontcolor=black]
}
result->n5->n4->n3->n2->n1
}
```
### 測驗 `2`
將 Timsort 實作整合進 lab0-c :[commit 1ca5d00](https://github.com/hungyuhang/lab0-c/commit/1ca5d001ff777fb465a90c36adfce00529833752)
## 第 2 週測驗題
### 測驗 `1`
該函式使用對同一棵二元樹的前序及中序走訪結果,重建此二元樹。
#### 程式碼解析
假設我們有以下輸入:
- `preorder` = [3, 9, 20, 15, 7]
- `inorder` = [9, 3, 15, 20, 7]
```c
for (int i = 0; i < inorderSize; i++)
INIT_HLIST_HEAD(&in_heads[i]);
for (int i = 0; i < inorderSize; i++)
node_add(inorder[i], i, inorderSize, in_heads);
```
在一開始,上方程式碼會建立一個 hash table `in_heads` ,並且將 `inorder` 裡面的元素跟其相對應的位置做成節點並放到 hash table 上面:
```graphviz
digraph G {
rankdir = TD;
splines = false;
node[shape = "record"]
{
rank="same"
hash[label = "<h0>0 | <h1>1 | <h2>2 | <h3>3 | <h4>4"];
tex[label="hash table",shape=plaintext,fontcolor=black];
}
rank="same"
n3[label="{val=3 | idx=1}",fontcolor=black];
n7[label="{val=7 | idx=4}",fontcolor=black];
n9[label="{val=9 | idx=0}",fontcolor=black];
n15[label="{val=15 | idx=2}",fontcolor=black];
n20[label="{val=20 | idx=3}",fontcolor=black];
hash:h0 -> n20 -> n15;
hash:h2 -> n7;
hash:h3 -> n3;
hash:h4 -> n9;
}
```
底下是 hash function :
```c
int hash = (val < 0 ? -val : val) % size;
```
舉 `inorder[2]` 的 15 為例,依照 hash function , 15 會被放在第 15 % 5 = 0 個 hash table entry 裡面。
接下來,程式碼會開始遞迴的呼叫 `dfs()` :
```c
static struct TreeNode *dfs(int *preorder,
int pre_low,
int pre_high,
int *inorder,
int in_low,
int in_high,
struct hlist_head *in_heads,
int size)
{
if (in_low > in_high || pre_low > pre_high)
return NULL;
struct TreeNode *tn = malloc(sizeof(*tn));
tn->val = preorder[pre_low];
int idx = find(preorder[pre_low], size, in_heads);
tn->left = dfs(preorder, pre_low + 1, pre_low + (idx - in_low), inorder,
in_low, idx - 1, in_heads, size);
tn->right = dfs(preorder, pre_high - (in_high - idx - 1), pre_high, inorder,
idx + 1, in_high, in_heads, size);
return tn;
}
```
輸入的資料結構會長的像下圖:
```graphviz
digraph G {
rankdir = TD;
splines = false;
node[shape = "record"]
pl[label="pre_low",shape=plaintext,fontcolor=black];
ph[label="pre_high",shape=plaintext,fontcolor=black];
il[label="in_low",shape=plaintext,fontcolor=black];
ih[label="in_high",shape=plaintext,fontcolor=black];
{
// rank="same"
preorder[label = "<n3>3 | <n9>9 | <n20>20 | <n15>15 | <n7>7"];
pre[label="preorder",shape=plaintext,fontcolor=black];
}
inorder[label = "<n9>9 | <n3>3 | <n15>15 | <n20>20 | <n7>7"];
in[label="inorder",shape=plaintext,fontcolor=black];
pl -> preorder:n3;
ph -> preorder:n7;
il -> inorder:n9;
ih -> inorder:n7;
preorder -> pre [arrowhead="none"];
inorder -> in [arrowhead="none"];
}
```
其中,`pre_low` , `pre_high` , `in_low` , `in_high` 的用途是把要處理的部份給框起來。在遞迴最頂端的函式呼叫,框起來的部份就是全部的範圍。
接下來,利用 preorder 的特性,我們可以直接得知傳入 `dfs()` 的這棵子樹的 root 就是 `3` 。
拿到 `3` 這個值之後,我們利用 `find()` 函式找出 `3` 這個節點在 `inorder` 的哪個位置。下面是 `find()` 函式的答案:
```diff
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);
+ hlist_for_each (p, heads + hash) {
+ struct order_node *on = list_entry(p,
if (num == on->val)
return on->idx;
}
return -1;
}
```
這個函式做的事情就是從雜湊表中用 $O(1)$ 的時間複雜度找出節點 {val=3, idx=1} 。這麼一來,我們就可以知道 `3` 在 `inorder` 的位置是 1 (以 `idx` 表示):
```graphviz
digraph G {
rankdir = TD;
splines = false;
node[shape = "record"]
pl[label="pre_low",shape=plaintext,fontcolor=black];
ph[label="pre_high",shape=plaintext,fontcolor=black];
il[label="in_low",shape=plaintext,fontcolor=black];
ih[label="in_high",shape=plaintext,fontcolor=black];
idx[label="idx",shape=plaintext,fontcolor=blue];
{
// rank="same"
preorder[label = "<n3>3 | <n9>9 | <n20>20 | <n15>15 | <n7>7"];
pre[label="preorder",shape=plaintext,fontcolor=black];
}
inorder[label = "<n9>9 | <n3>3 | <n15>15 | <n20>20 | <n7>7"];
in[label="inorder",shape=plaintext,fontcolor=black];
pl -> preorder:n3;
ph -> preorder:n7;
il -> inorder:n9;
ih -> inorder:n7;
idx -> inorder:n3;
preorder -> pre [arrowhead="none"];
inorder -> in [arrowhead="none"];
}
```
有了以上資訊,就可以算出:
- 左子樹的大小是 1
- 右子樹的大小是 3
如此一來,就可將 `preorder` 跟 `inorder` 分成下面三個部份(其中 root 用粗體標示):
- `preorder` : [**3**] [9] [20, 15, 7]
- `inorder` : [9] [**3**] [15, 20, 7]
並且再用遞迴的方式來產生左右子樹。而當取得左右子樹之後,就可以接到 root 上面,完成 binary tree 的製作。
### 測驗 `2`
該測驗的程式碼實作了 LRU Cache 的操作,以下是測驗程式碼所實作的函式:
- `lRUCacheCreate()`
- `lRUCacheFree()`
- `lRUCacheGet()`
- `lRUCachePut()`
#### 程式碼解析
##### `lRUCacheCreate(int capacity)`
該程式碼的功能就是建立一個空的 LRU cache 物件。
##### `lRUCacheFree(LRUCache *obj)`
該程式碼會把傳入的 LRU cache 物件給刪除,並且釋放所使用的記憶體。
其中,程式碼會先把 LRU cache 物件裡面 `dhead` 所指向的鏈節串列給刪除,然後再將 LRU cache 物件本身給刪除。
依照上述推導,可以開始推導空格中應該填入的程式碼:
```c
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);
}
```
由於 `pos` 的型別是 `list_head *` ,對照 `LRUNode` 擁有 `list_head` 型別的成員是 `link` :
```c
typedef struct {
int key;
int value;
struct hlist_node node;
struct list_head link;
} LRUNode;
```
所以 `FFFF` 需要填入 `link` :
```diff
- LRUNode *cache = list_entry(pos, LRUNode, FFFF);
+ LRUNode *cache = list_entry(pos, LRUNode, link);
```
而 `list_del()` 要傳入的參數型態是 `list_head *` ,所以我們要傳入的是 `pos` :
```diff
- list_del(GGGG);
+ list_del(pos);
```
##### `lRUCacheGet(LRUCache *obj, int key)`
> 這裡推測 key 代表的是記憶體位置
這個函式會做兩件事情:
- 從雜湊表尋找要取得的節點
- 找到後把該節點放到 `dhead` 所指向的串列的最前面
以下是程式碼:
```c
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(&cache->link/*IIII*/, &obj->dhead);
return cache->value;
}
}
return -1;
}
```
由於 `pos` 的型別是 `hlist_node` ,對照 `LRUNode` 擁有 `hlist_node` 型別的成員是 `node` ,所以 `HHHH` 需要填入 `node` :
```diff
- LRUNode *cache = list_entry(pos, LRUNode, HHHH);
+ LRUNode *cache = list_entry(pos, LRUNode, node);
```
而第二個空格要做的事情是把找到的資料節點安插到 `dlist` 的最前面,所以 `IIII` 要填入的是 `&cache->link` :
```diff
- list_move(IIII, &obj->dhead);
+ list_move(&cache->link, &obj->dhead);
```
##### `lRUCachePut(LRUCache *obj, int key, int value)`
> 這裡推測 key 代表的是記憶體位置,而 value 則是代表該記憶體位置內的資料
這個函是在前半部所作的事情跟 `lRUCacheGet()` 是非常相似的:
- 從雜湊表尋找要放入的節點是否已經存在
- 如果已經存在,則把已存在的節點放到 `dhead` 所指向的串列的最前面
所以以下的填空可以比照 `lRUCacheGet()` 的填空方式:
```diff
hlist_for_each (pos, &obj->hhead[hash]) {
- LRUNode *c = list_entry(pos, LRUNode, JJJJ);
+ LRUNode *c = list_entry(pos, LRUNode, node);
if (c->key == key) {
- list_move(KKKK, &obj->dhead);
+ list_move(&c->link, &obj->dhead);
cache = c;
}
}
```
而這裡是下半部的程式邏輯:
- 如果該節點不存在,而且 LRU Cache 已經滿了:
- 將 cache 裡面 `dhead` 串列的最後一個節點替換成要放入的節點,並且將其安插到 `dhead` 串列的最前面
- 如果該節點不存在,但 LRU Cache 還有空間:
- 在 cache 裡面新增一個節點,並且將其安插到 `dhead` 串列的最前面
- 如果該節點已經存在
- 將該節點的數值用引數 `value` 做更新
- 因為程式碼的前半段已經將這個節點安插到 `dhead` 串列的最前面,所以這邊就不重複做了。
而在 `lRUCachePut()` 裡面會用到 `hlist_del()` 這個函式:
```c
void hlist_del(struct hlist_node *n)
{
struct hlist_node *next = n->next, **pprev = n->pprev;
*pprev = next;
if (next)
EEEE = pprev;
}
```
以下簡單演示刪除的過程:
假設要將 node 1 從鏈節串列刪除:
```graphviz
digraph G {
rankdir = LR;
//splines = false;
node[shape = "record"]
h[label = "<f>first"];
n0[label = "{<p>pprev | <n>next} | <id>node 0"];
n1[label = "{<p>pprev | <n>next} | <id>node 1"];
n2[label = "{<p>pprev | <n>next} | <id>node 2"];
null[label="NULL",shape=plaintext];
h -> n0;
n0:n -> n1;
n1:n -> n2;
n2:n -> null;
n2:p -> n1:n
n1:p -> n0:n
n0:p -> h
}
```
首先將 `next` 跟 `pprev` 指向 node 1 周圍的節點( `pprev` 指向的是 node 0 的 `next` 成員):
```graphviz
digraph G {
rankdir = LR;
//splines = false;
node[shape = "record"]
h[label = "<f>first"];
n0[label = "{<p>pprev | <n>next} | <id>node 0"];
n1[label = "{<p>pprev | <n>next} | <id>node 1"];
n2[label = "{<p>pprev | <n>next} | <id>node 2"];
null[label="NULL",shape=plaintext];
next[label="next",shape=plaintext,fontcolor=blue];
pprev[label="pprev",shape=plaintext,fontcolor=red];
h -> n0;
n0:n -> n1;
n1:n -> n2;
n2:n -> null;
n2:p -> n1:n
n1:p -> n0:n
n0:p -> h
next -> n2;
pprev -> n0:n
}
```
接下來透過 `pprev` 修改 node 0 的 `next` 成員,將其指向 node 2:
```graphviz
digraph G {
rankdir = LR;
//splines = false;
node[shape = "record"]
h[label = "<f>first"];
n0[label = "{<p>pprev | <n>next} | <id>node 0"];
{
rank="same"
n1[label = "{<p>pprev | <n>next} | <id>node 1"];
hidden[shape=point height=0];
}
n2[label = "{<p>pprev | <n>next} | <id>node 2"];
null[label="NULL",shape=plaintext];
next[label="next",shape=plaintext,fontcolor=blue];
pprev[label="pprev",shape=plaintext,fontcolor=red];
h -> n0;
n0:n -> hidden [arrowhead=none];
hidden -> n2;
n1:n -> n2;
n2:n -> null;
n2:p -> n1:n
n1:p -> n0:n
n0:p -> h
next -> n2;
pprev -> n0:n
}
```
最後將 `next` 所指向的節點的 `pprev` 成員指向 node 0 ,即完成刪除的動作:
```graphviz
digraph G {
rankdir = LR;
//splines = false;
node[shape = "record"]
h[label = "<f>first"];
n0[label = "{<p>pprev | <n>next} | <id>node 0"];
{
rank="same"
n1[label = "{<p>pprev | <n>next} | <id>node 1"];
hidden[shape=point height=0];
hidden2[shape=point height=0];
}
n2[label = "{<p>pprev | <n>next} | <id>node 2"];
null[label="NULL",shape=plaintext];
next[label="next",shape=plaintext,fontcolor=blue];
pprev[label="pprev",shape=plaintext,fontcolor=red];
h -> n0;
n0:n -> hidden [arrowhead=none];
hidden -> n2;
n1:n -> n2;
n2:n -> null;
n2:p -> hidden2 [arrowhead=none];
hidden2 -> n0:n;
n1:p -> n0:n;
n0:p -> h;
next -> n2;
pprev -> n0:n
}
```
### 測驗 `3`
#### `find_nth_bit()` 介紹
該程式的功能,是在某一段記憶體空間找到第 n 個 bit 1 的所在位置。以下圖為例,指標 0 指向的位置就是第 0 個 bit 1 ,指標 1 指向的位置是第 1 個 bit 1 ,以此類推。
而當沒有找到第 n 個 bit 1 的時候,程式碼就會回傳該段記憶體空間的大小,在下面這個例子的話,就是 8 。
```graphviz
digraph G {
rankdir = TD;
splines = false;
node[shape = "record"]
p0[label="0",shape=plaintext,fontcolor=black];
p1[label="1",shape=plaintext,fontcolor=black];
p2[label="2",shape=plaintext,fontcolor=black];
p3[label="3",shape=plaintext,fontcolor=black];
{
rank="same"
bitfield[label = "<b7>1 | <b6>0 | <b5>0 | <b4>1 | <b3>1 | <b2>0 | <b1>1 | <b0>0"];
lsb[label="LSB",shape=plaintext,fontcolor=black];
}
p0 -> bitfield:b1;
p1 -> bitfield:b3;
p2 -> bitfield:b4;
p3 -> bitfield:b7;
}
```
#### 程式碼解析
`find_nth_bit()` 的主函式相當的簡潔:
```c
static inline unsigned long find_nth_bit(const unsigned long *addr,
unsigned long size,
unsigned long n)
{
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);
}
```
這裡可以看到,程式碼使用了 `small_const_nbits()` 這個巨集來判斷是否要用不同的方式來計算第 n 個 bit 的位置。
下面是 `small_const_nbits()` 巨集的定義:
```c
#define small_const_nbits(nbits) \
(__builtin_constant_p(nbits) && (nbits) <= BITS_PER_LONG && (nbits) > 0)
```
依照 C 運算元的優先順序,邏輯上我們可以把上面程式碼視為先運算下面三個式子之後再進行 Logical AND 之後的結果:
- `__builtin_constant_p(nbits)`
- `(nbits) <= BITS_PER_LONG`
- `(nbits) > 0`
參考 [GCC 文件](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html#index-_005f_005fbuiltin_005fconstant_005fp) , `__builtin_constant_p(nbits)` 的用途是去判斷 `nbits` 是否為 compile-time constant ,如果是的話就回傳 1 ,反之則回傳 0 。
`BITS_PER_LONG` 則是代表 long 型態的位元數量,在這裡是 64 。
所以這邊可以推得 `small_const_nbits()` 巨集的功能是去判斷輸入 `find_nth_bit()` 的長度參數是否為常數,且該常數是介於 1 到 64 的區間。
___
當程式碼判斷 `size` 為小常數之後,下面的程式碼就會被執行:
```c
unsigned long val = *addr & GENMASK(size - 1, 0);
return val ? fns(val, n) : size;
```
首先解析 `GENMASK` :
```c
#define GENMASK(h, l) \
(((~0UL) - (1UL << (l)) + 1) & (~0UL >> (BITS_PER_LONG - 1 - (h))))
```
該巨集會產生以 `h` 跟 `l` 為邊界的遮罩,舉例來說, `GENMASK(59, 4)` 會產出一個 `0x0FFFFFFFFFFFFFF0` 的遮罩。
接下來是 `fns()` :
```c
static inline unsigned long fns(unsigned long word, unsigned int n)
{
while (word) {
unsigned int bit = __ffs(word);
if (n-- == 0)
return bit;
__clear_bit(bit, &word);
}
return BITS_PER_LONG;
}
```
該函式的原理相當簡單,假設要找出 `01101011` 中第二個 `1` 的位置(從第零個開始算):
1. 迴圈透過 `__ffs()` 函式找到第零個 1 並將其刪除:
`word = 01101010`
2. 迴圈透過 `__ffs()` 函式找到第零個 1 並將其刪除,但是因為原本的第零個 1 已經在前一次迭代被刪除了,所以這次刪除的 1 其實是第一個 1 :
`word = 01101000`
3. 迴圈透過 `__ffs()` 函式找到第零個 1 ,因為前面已經刪掉兩個 1 了,所以這次找到的就是第二個 1 ,也就是我們的目標。
到這邊可以推測,當輸入 `find_nth_bit()` 的 `size` 參數為比較正常的數字時(介於 1 到 64 的數字,常數),這份程式碼並不會用特別的方式來處理問題。
現在來看當 `small_const_nbits()` 回傳值為零的時候程式碼的邏輯:
```c
return FIND_NTH_BIT(addr[idx], size, n);
```
`FIND_NTH_BIT()` 為一巨集,可以被展開成下面的程式碼:
```c
#define FIND_NTH_BIT(FETCH, size, num) \
({ \
unsigned long sz = (size), nr = (num), idx, w, tmp; \
\
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); \
found: \
sz = min(idx * BITS_PER_LONG + fns(tmp, nr), sz); \
out: \
sz; \
})
```
> `FETCH` 在這裡會被代換成 `addr[idx]` 。
並且以下是 `hweight_long()` 的程式碼:
```c
#define __const_hweight8(w) \
((unsigned int) ((!!((w) & (1ULL << 0))) + (!!((w) & (1ULL << 1))) + \
(!!((w) & (1ULL << 2))) + (!!((w) & (1ULL << 3))) + \
(!!((w) & (1ULL << 4))) + (!!((w) & (1ULL << 5))) + \
(!!((w) & (1ULL << 6))) + (!!((w) & (1ULL << 7)))))
#define __const_hweight16(w) (__const_hweight8(w) + __const_hweight8((w) >> 8))
#define __const_hweight32(w) \
(__const_hweight16(w) + __const_hweight16((w) >> 16))
#define __const_hweight64(w) \
(__const_hweight32(w) + __const_hweight32((w) >> 32))
static inline unsigned long hweight_long(unsigned long w)
{
return __const_hweight64(w);
}
```
先解析 `__const_hweight8()` ,先單獨取 `(!!((w) & (1ULL << 3)))` 這個程式碼片段來看:
- `(w) & (1ULL << 3)` 是在提取出 `w` 中第三個位元。
- 如果 `w` 的第三個位元為 1 的話,這個式子的結果就會是 `00001000`
- `!!` 的作用則是把原本非零的數字轉換成 0 跟 1
- `00001000` 會變成 1
- `00000000` 會變成 0
所以 `__const_hweight8()` 在做的事情其實就是計算這 8 個位元裡面有幾個 1 。
基於上面的推導,不難看出這系列的巨集以及 `hweight_long()` 的用途就是在算傳入的資料裡面有幾個位元是 1 。
回到 `FIND_NTH_BIT()` ,知道 `hweight_long()` 的用途後,就可以知道這裡的 `for` 迴圈是在找出第 n 個位元 1 是被放在 `addr` 指向的記憶體的哪個位置:
```c
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; \
} \
```
而 `for` 迴圈底下的 `if` 判斷式則是用來處理最後一個 word (也就是 `addr[]` 的最後一個),因為上面的 `for` 迴圈不會檢查到這一個 word 。
```c
if (sz CCCC BITS_PER_LONG) \
tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz); \
```
從程式碼上下文可以推斷 CCCC 代表的是 `<` :
```diff
- if (sz CCCC BITS_PER_LONG) \
+ if (sz < BITS_PER_LONG) \
tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz); \
```
在最後的 `found` 跟 `out` 程式區塊則會回傳最後的結果。
```c
found: \
sz = min(idx * BITS_PER_LONG + fns(tmp, nr), sz); \
out: \
sz; \
```