--- 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 效能落差 :::