---
# System prepended metadata

title: 2024q1 第 4 週測驗題
tags: [linux2024]

---

---
tags: linux2024
---

# [2024q1](http://wiki.csie.ncku.edu.tw/linux/schedule) 第 4 週測驗題

:::info
目的: 檢驗學員對 bitwise 和 [rbtree](https://hackmd.io/@sysprog/linux-rbtree) 的認知
:::

==[作答表單: 測驗 1-2](https://docs.google.com/forms/d/e/1FAIpQLSe1JKHvdSyEWpkMux5YQ7uReAC4BJw0hp5LykxzF_M6mHwF_A/viewform)== (針對 Linux 核心「設計」課程)
==[作答表單: 測驗 3](https://docs.google.com/forms/d/e/1FAIpQLSfmtwv2i53ID3mlTF3ST4p1kHnP066ooTKc14KyUhY6hmeKtQ/viewform)== (針對 Linux 核心「實作」課程)

### 測驗 `1`

[population count](https://en.wikichip.org/wiki/population_count) 簡稱 popcount 或叫 sideways sum，是計算數值的二進位表示中，有多少位元是 `1`，在一些場合下很有用，例如計算 0-1 稀疏矩陣 (sparse matrix)或 bit array 中非 `0` 元素個數、計算兩個字串的 [Hamming distance](https://en.wikipedia.org/wiki/Hamming_weight)。Intel 在 2008 年 11 月 Nehalem 架構的處理器 Core i7 引入 SSE4.2 指令集，其中就有 `CRC32` 和 `popcount` 指令，`popcount` 可處理 16-bit, 32-bit, 64-bit 整數。

對應到 C 程式的實作:
```c
unsigned popcount_naive(unsigned v)
{
    unsigned n = 0;
    while (v)
        v &= (v - 1), n = -(~n);
    return n;
}
```

函式 `popcount_naive()` 利用不斷清除 LSB 直到輸入的數值 `v` 為 0。

`v &= (v - 1)` 的作用是將 LSB 設為 0 (**reset LSB**) 舉例來說，假設輸入數值為 `20`:
```c
0001 0100  # 20          ; LSB in bit position 2
0001 0011  # 20 - 1
0001 0000  # 20 & (20 - 1)
```
> 類似的操作還有 `x & -x`，將 `x` 的 LSB 取出 (**isolate LSB**)

`n = -(~n)` 等同 `n++`，因為在二補數系統中，
$-n =\ \sim n + 1$
$-(\sim n) = n + 1$

因此 `popcount_naive()` 的執行時間取決於輸入數值中 1 (set bit) 的個數。可改寫為以下常數時間複雜度的實作:
```c
unsigned popcount_branchless(unsigned v)
{
    unsigned n;
    n = (v >> 1) & 0x77777777;
    v -= n;
    n = (n >> 1) & 0x77777777;
    v -= n;
    n = (n >> 1) & 0x77777777;
    v -= n;

    v = (v + (v >> 4)) & 0x0F0F0F0F;
    v *= 0x01010101;                                     

    return v >> 24;
}
```

對於一個 32 bit 的無號整數，popcount 可以寫成以下數學式:

$popcount(x) = x - \left \lfloor{{\dfrac{x}{2}}}\right \rfloor - \left \lfloor{{\dfrac{x}{4}}}\right \rfloor - ... - \left \lfloor{{\dfrac{x}{2^{{31}}}}}\right \rfloor$ 

假設 $x = b_{31}...b_3b_2b_1b_0$，先看看 $x[3:0]$ 4 個位元，用以上公式可以計算得:

$(2^3b_3 + 2^2b_2 + 2^1b_1 + 2^0b_0) - 
(2^2b_3 + 2^1b_2 + 2^0b_1) - (2^1b_3 + 2^0b_2) - 2^0b_3$
> $\left \lfloor{{\dfrac{x}{2}}}\right \rfloor$ 相當於 C 表達式中 `x >> 1`

稍微改寫可得到:
$(2^3 - 2^2 - 2^1 - 2^0)b_3 + (2^2 - 2^1 - 2^0)b_2 + (2^1 - 2^0)b_1 + 2^0b_0$

因此 popcount 的一般式可改寫為:
$popcount(x) = \sum\limits_{n=0}^{31} {}(2^n - \sum\limits_{i=0}^{n-1} 2^{i})b_n = \sum\limits_{n=0}^{31}b_n$

因為 $2^n - \sum\limits_{i=0}^{n-1} 2^{i} = 1$，只要對應的 $b_n$ 為 1，這個 bit 就會在 popcount 的總和中加一，剛好對應 `popcount_naive()`，因此映證上述數學式確實可計算出 population count。

且一個 32 位元無號整數最多有 32 個 1 (set bit)，剛好可用一個 byte 表示，所以可分成幾個區塊平行計算，最後再全部加總到一個 byte 中，進行避免檢查 32 次。

`popcount_branchless` 實作一開始以**每 4 個位元 (nibble) 為一個單位**計算 1 的個數，利用最初的公式計算 $x - \left \lfloor{{\dfrac{x}{2}}}\right \rfloor - \left \lfloor{{\dfrac{x}{4}}}\right \rfloor - \left \lfloor{{\dfrac{x}{8}}}\right \rfloor$

關鍵的程式碼，逐行解釋:
```c=
n = (v >> 1) & 0x77777777;
v -= n;
n = (n >> 1) & 0x77777777;
v -= n;
n = (n >> 1) & 0x77777777;
v -= n;
```

1. `n = (v >> 1) & 0x77777777` : 將輸入數值 `v` 除以 2，得到 $\left \lfloor{{\dfrac{v}{2}}}\right \rfloor$
    ```c
    b_31 b_30 b_29 b_28 ... b7 b6 b5 b4 b3 b2 b1 b0  // v
       0 b_31 b_30 b_29 ...  0 b7 b6 b4  0 b3 b2 b1  // (v >> 1) & 0x77777777
    ```
2. `v -= n` : 計算結果相當於 $v - \left \lfloor{{\dfrac{v}{2}}}\right \rfloor$
3. `n = (n >> 1) & 0x77777777` : 再對 `n` 除以 2，得到 $\left \lfloor{{\dfrac{v}{4}}}\right \rfloor$
4. `v -= n` : 計算出 $v - \left \lfloor{{\dfrac{v}{2}}}\right \rfloor - \left \lfloor{{\dfrac{v}{4}}}\right \rfloor$
5. 和 6. 重複同樣動作

最後這段結束後計算出 $v - \left \lfloor{{\dfrac{v}{2}}}\right \rfloor - \left \lfloor{{\dfrac{v}{4}}}\right \rfloor - \left \lfloor{{\dfrac{v}{8}}}\right \rfloor$，得到每 4 個位元為一個單位中 set bit 的個數

**`v = (v + (v >> 4)) & 0x0F0F0F0F`** : 將每 4 個位元中 set bit 的個數加到 byte 中:
1. 假設 $B_n$ 代表第 n 個 nibble (4 位元) 中的數值
    ```c
    B7 B6 B5 B4 B3 B2 B1 B0  // v
     0 B7 B6 B5 B4 B3 B2 B1  // (v >> 4)
    ```
2. 加總可得到:
    ```c
    // (v + (v >> 4))
    B7 (B7+B6) (B6+B5) (B5+B4) (B4+B3) (B3+B2) (B2+B1) (B1+B0)
    ```
3. 最後使用 `0x0F0F0F0F` 做 mask 可得
    ```c
    // (v + (v >> 4)) & 0x0F0F0F0F
    0 (B7+B6) 0 (B5+B4) 0 (B3+B2) 0 (B1+B0)
    ```

**`v *= 0x01010101`** : 在最後一道敘述中，將 `v` 乘上 `0x01010101`。寫成直式如下
```
                         0 A6  0 A4  0 A2  0 A0
                      x  0  1  0  1  0  1  0  1
---------------------------------------------------
                         0 A6  0 A4  0 A2  0 A0
                   0 A6  0 A4  0 A2  0 A0  0
             0 A6  0 A4  0 A2  0 A0  0
       0 A6  0 A4  0 A2  0 A0  0
---------------------------------------------------
                            ↑_______________________A6+A4+A2+A0
```

我們可發現期望輸出就在原本 $A_6$ 的位置 ($2^7$)，因此將 `v` 右移 24 bits 即為所求，剩下的位數會 overflow ，右移後不影響結果。
* 假設 $A = B_7 + B_6$, $B = B_5 + B_4$, $C = B_3 + B_2$, $D = B_1 + B_0$, 根據分配律可得:
    ```
    v * 0x01010101 = 
    (A + B + C + D)  (B + C + D)      (C + D)          (D)
    |<-- 1 byte -->|<-- 1 byte -->|<-- 1 byte -->|<-- 1 byte -->|
    ```

**`return v >> 24`** : 最後得到的結果會放在 Most significant byte 中，因此向右位移 24 位元，即為所求的 popcount 值。

GCC 提供對應的內建函式:
> `__builtin_popcount(x)`: 計算 x 的二進位表示中，總共有幾個 `1`

使用示範：
```c
int x = 5328; // 00000000000000000001010011010000
printf("%d\n",  __builtin_popcount(x)); // 5
```

兩個整數間的 Hamming distance 為其二進位的每個位元的差

Example : 
1   (0  0  0  1)
4   (0  1  0  0)

hamming distance = 2

| A $\oplus$ B | B = 0 | B = 1 |
| -------- | -------- | -------- |
| A = 0    | 0     | 1     |
| A = 1    | 1     | 0     |

`A ^ B`　就為二進位中兩數字的每個位元的差，再透過　popcount()  就可以得到答案
```c
int hammingDistance(int x, int y)
{
    return __builtin_popcount(x ^ y);
}
```

針對 [LeetCode 477. Total Hamming Distance](https://leetcode.com/problems/total-hamming-distance/)，考慮以下程式碼:
```c
int totalHammingDistance(int* nums, int numsSize)
{
    int total = 0;;
    for (int i = 0;i < numsSize;i++)
        for (int j = 0; j < numsSize;j++)
            total += __builtin_popcount(nums[i] ^ nums[j]); 
    return total >> AAAA;
}
```

從 popcount 角度出發，時間複雜度就被限制在 $O(n^{2})$。 
以下嘗試從位元展現的樣貌，觀察 Total Hamming Distance 的規則。
```graphviz
digraph A{
b [shape=record  fontsize=24  label="{ n 'th bit|input 7|  input 5| input 10|input 17 }|{ 4|0|  0| 0|1 }|{ 3|0| 0| 1 |0}|{ 2|1| 1| 0 |0}|{ 1|1| 0| 1 |0}|{ 0|1| 1| 0 |1}"]
}
```
1. 第 0 個 bit 觀察 : 
    全部都是 1 -> Hamming Distance = 0
2. 第 1 個 bit 觀察 : 
    1 有一個，其餘皆為 0 
    可以直接用單個 bit 判斷 Hamming Distance (1)* (numsize - 1)
3. 第 2 個 bit 觀察 : 
    1 有兩個，其餘皆為 0 
    Hamming Distance (2)* (numsize - 2)

以 bit 找規則: 
> 所有 Number 中從第 0 個 bit 開始計算全部該 bit 的 Hamming Distance ，在全部累加起來就是 Total Hamming Distance

請補完程式碼。

:::success
延伸問題：
1. 參照《[Hacker's Delight](https://en.wikipedia.org/wiki/Hacker%27s_Delight)》和 [CS:APP 第二章](https://hackmd.io/@sysprog/CSAPP-ch2)，解釋上述程式碼運作原理。
2. 不依賴 popcount，嘗試以上述歸納的規則，針對 [LeetCode 477. Total Hamming Distance](https://leetcode.com/problems/total-hamming-distance/) 撰寫出更高效的程式碼。
:::

---

### 測驗 `2`

摘錄自 [Hacker's Delight](https://web.archive.org/web/20180517023231/http://www.hackersdelight.org/divcMore.pdf):

**Remainder by Summing digits**
如何不使用任何除法就算出某數除以另一個數的餘數呢？若除數符合 $2^k \pm 1$，則可運用以下手法來達成這個目的。

若 $a \equiv b (mod \ \ m)$ 且 $c \equiv d (mod \ \ m)$ ，則 $a + c \equiv b + d (mod \ \ m)$ 且 $ac \equiv bd (mod \ \ m )$

假設
$$
a = q_a m + r_1, b = q_b m + r_1, c = q_c m + r_2, d = q_d m + r_2
$$

再進行運算。

以除數為 3 為例， $1 \equiv 1 (mod \ \ 3)$ 且 $2 \equiv -1 (mod \ \ 3)$ ，將後者不斷的乘上前者可得
$$
2^k \equiv \begin{cases}
1 (mod \ \ 3), \ \ k \ \ even\\
-1 (mod \ \ 3), \ \ k \ \ odd\\
\end{cases}
$$

因此若 n 的二進位表示為 $b_{n-1}b_{n-2}\cdots b_1 b_0$ ，則 $n = \sum_{i=0}^{n-1} b_i\cdot 2^i$ ，由以上的推論可得 $n \equiv \sum_{i=0}^{n-1} b_i\cdot (-1)^i \ \ (mod \ \ 3)$ 。
將每次得到的位元和遞迴的應用上式直到得到一個小於 3 的數即是餘數，若變成負數則要加上 3 的倍數。
位元和通常又可以利用 population count 這類的函式來得到，因此寫成程式的話可以將上式表示為 `n = popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA)` 。
利用以下定理可以進一步化簡。

$popcount(x \And \overline{m}) - popcount(x \And m) = popcount(x \bigoplus m) - popcount(m)$

於是 `n = popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA)` 可以寫為 `n = popcount(n ^ 0xAAAAAAAA) - 16` ，將此式重複應用於得到的 `n` 上直到 `n` 介於 0~2 ，但為了避免 `n` 變成負數，我們要將它加上一個足夠大的 3 的倍數，文中的例子是加上 39 。範例程式如下
```c
int mod3(unsigned n)
{
    n = popcount(n ^ 0xAAAAAAAA) + 23;
    n = popcount(n ^ 0x2A) - 3;
    return n + ((n >> 31) & 3);
}
```

另一種變形是利用 lookup table 。
```c
int mod3(unsigned n)
{
    static char table[33] = {2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1 };
    n = popcount(n ^ 0xAAAAAAAA);
    return table[n];
}
```

`tictactoe.c` 程式統計隨機的井字遊戲，參考執行輸出如下:
```
Win probability for first move with random agents:
0.115 0.102 0.116 
0.102 0.131 0.101 
0.116 0.102 0.116 
Player 1 won 584650 times
Player 2 won 288379 times
126971 ties
0.047604 seconds
21.006638 million games/sec
```

程式碼可見: [tictactoe.c](https://gist.github.com/jserv/9088f6f72a35a1fcaaf1c932399a5624) (部分遮蔽)

其中 `BBBB` 是 hex literal，`CCCC` 是十進位整數。以最精簡的形式撰寫。

:::success
延伸問題：
1. 參照《[Hacker's Delight](https://en.wikipedia.org/wiki/Hacker%27s_Delight)》和 [CS:APP 第二章](https://hackmd.io/@sysprog/CSAPP-ch2)，解釋上述程式碼運作原理。
2. 將上述手法應用於[第三次作業](https://hackmd.io/@sysprog/linux2024-ttt)中，追求更有效的賽局判定機制。
:::

---

### 測驗 `3`

> 搭配 [Linux 核心原始程式碼巨集: `container_of`](https://hackmd.io/@sysprog/linux-macro-containerof), [Linux 核心的紅黑樹](https://hackmd.io/@sysprog/linux-rbtree)

考慮 XTree^名稱沒特別意思，不用去Google搜尋^ 是個嘗試兼具 AVL tree 和 red-black tree 部分特性的 binary search tree (BST) 實作，著重在快速的 insert/delete 操作且合理的 lookup 速度。

AVL tree 可確保每個節點的左右子樹高相差不超過 `1`，因此每個節點需要儲存目前高度來計算 balance factor 來決定是否需要旋轉 (rotate)。

red-black tree 則藉由以下規則來達到平衡：
1. 不能有兩個紅色節點相連;
2. 所有從該節點走到其子樹下的末端節點 (leaf) 的上之黑色節點數量必定

XTree 的平衡機制與 AVL tree 相似，利用 hint 來評估是否需要做 rotate，然而 xt_node 的 hint 的計算方式是，其左右節點 hint 最大值 +1 並且只有在被呼叫到 xt_update 時才會被更新。

以下是簡化的圖示，不區分左右節點。
```
1
∟———2
     ∟———8
          ∟———16
               ∟———33
                    ∟———73
                         ∟———82
                              ∟———83
                                   ∟———91
                                        ∟———1000
                                             ͱ———95
```

假設目前的 XTree 已經有 1000, 1, 2, 8 插入，接著插入 16 來觀察

- [ ] `xt_insert` 前
```graphviz 
digraph Tree{
    graph[ordering=out]
    null0 [shape=point][color=white]
    null1 [shape=point][color=white]
    null2 [shape=point][color=white]

    1 -> 2
    1 -> null0[style=invis]
    2 -> 1000
    2 -> null1[style=invis]
    1000 -> null2[style=invis]
    1000 -> 8
}
```

- [ ] `xt_update` 前
```graphviz 
digraph Tree{
    graph[ordering=out]
    null0 [shape=point][color=white]
    null1 [shape=point][color=white]
    null2 [shape=point][color=white]
    null3 [shape=point][color=white]

    1 -> 2
    1 -> null0[style=invis]
    2 -> 1000
    2 -> null1[style=invis]
    1000 -> null2[style=invis]
    1000 -> 8
    8 -> 16
    8 -> null3[style=invis]
}
```

- [ ] 對 16 進行 `xt_update`

由於 16 沒有左側或右側節點，因此不會做任何動作，但是由於 16 的 hint 為 0，所以會對其親代節點 8 進行更新。

- [ ] 對 8 做 `xt_update`

由於 `xt_balance` 為 1，因此不會做任何動作，但由於 8 的 hint 有經過更改 (0 -> 1) 所以會對其親代節點 1000 做進行更新。

- [ ] 對 1000 做 `xt_update`

旋轉完畢後，由於 1000 的 hint 沒有改變 (1->1)，因此結束。
```graphviz 
digraph Tree{
    graph[ordering=out]
    null0 [shape=point][color=white]
    null1 [shape=point][color=white]
    null2 [shape=point][color=white]
    null3 [shape=point][color=white]

    1 -> 2
    1 -> null0[style=invis]
    2 -> 8
    2 -> null1[style=invis]
    8 -> 1000
    8 -> null3[style=invis]
    1000 -> null2[style=invis]
    1000 -> 16
}
```

由上可知，由於 hint 本身只要沒有改變且不等於 0，就不會往上更新，代表 XTree 只要更新末端節點時，發生 AVL 的 LR 或是 RL，就不會再往上更新。

參見 [XTree 的程式碼](https://gist.github.com/jserv/7374b3e031ec3a81441b1b93b008c5d2) (部分遮蔽)。

作答規範：
* `AAAA`, `BBBB`, `CCCC`, `DDDD`, `EEEE`, `FFFF` 皆為表示式
* `BBBB` 和 `DDDD` 為 `xt_` 開頭的表示式
* 不包含空白，以最精簡的形式撰寫

:::success
延伸問題:
1. 解釋上述程式碼運作原理
2. 指出上述程式碼可改進之處，特別跟 AVL tree 和 red-black tree 相比，並予以實作
    > 考慮到循序輸入和亂序輸入的狀況
3. 設計效能評比程式，探討上述程式碼和 Linux 核心 red-black tree 效能落差
:::