# 2024q1 Homework4 (quiz3+4) contributed by < [allenliao666](https://github.com/allenliao666) > ## [Quiz3](https://hackmd.io/@sysprog/linux2024-quiz3) ### 測驗1 #### 解釋程式碼 思路:運用直式開方法的變形,找出要開平方的數的 `MSB` 並持續測試回傳值平方後是否超過原數,直到 `LSB` 。 `ffs` : 回傳第一個(Least significant) 被設定的位元 `fls` : 回傳最後一個(Most significant) 被設定的位元 可以用`fls` 取代題目中的`__builtin_clz()`,改寫如下: ```c int i_sqrt_fls(int x) { if (x <= 1) /* Assume x is always positive */ return x; int z = 0; for (int m = 1UL << (fls(x) & ~1UL); m; m >>= 2) { int b = z + m; z >>= 1; if (x >= b) x -= b, z += m; } return z; } ``` #### Linux 核心實例 在 Linux 原始程式碼以關鍵字 `sqrt` 搜尋,發現在 arch 、 driver 等目錄下有非常多有關平方根運算的程式碼,其中在 [linux/block/blk-wbt.c](https://github.com/torvalds/linux/blob/master/block/blk-wbt.c) 僅有一段程式碼有關 `sqrt` 如下: ```c static void rwb_arm_timer(struct rq_wb *rwb) { struct rq_depth *rqd = &rwb->rq_depth; if (rqd->scale_step > 0) { /* * We should speed this up, using some variant of a fast * integer inverse square root calculation. Since we only do * this for every window expiration, it's not a huge deal, * though. */ rwb->cur_win_nsec = div_u64(rwb->win_nsec << 4, int_sqrt((rqd->scale_step + 1) << 8)); } else { /* * For step < 0, we don't want to increase/decrease the * window size. */ rwb->cur_win_nsec = rwb->win_nsec; } blk_stat_activate_nsecs(rwb->cb, rwb->cur_win_nsec); } ``` ### 測驗2 #### 解釋程式碼 考量在相鄰敘述進行 mod 10 和 div 10 操作,觀察除法原理: $f = g \times Q + r$ 可以發現 $r = f - g \times Q$,若我們知道 div 10,就可以反推得出 mod 10。 ,因為10並非2的冪,所以我們使用近似值 $\frac{128}{13} \cong 9.84$ 實做。首先透過 bitweise operation 湊出13,因為13 = 8 + 4 + 1 ,等同計算 $\frac{13tmp}{8} \times8$ 如下 ```c (((tmp >> 3) + (tmp >> 1) + tmp) << 3) ``` 但 LSB 數來3個位元會被捨棄,因此在上述操作前要先記錄該位元。文中的 `0b1` 表示二進位表示法的1。 ```c d0 = q & 0b1; d1 = q & 0b11; d2 = q & 0b111; ``` 把上述兩段程式碼結果相加後,即可得到 $13tmp$ 。最後除以128達到 $\frac{tmp}{9.84}$,即 div10的效果。不過令人疑惑的是為什麼 $13tmp$ 不透過 bitwise 左移如下,而是使用右移和 d0, d1,d2 補回位元? ```c ((tmp << 3) + (tmp << 1) + tmp) ``` 完整程式碼如下 ```c void divmod_10(uint32_t in, uint32_t *div, uint32_t *mod) { unsigned d0 = x & 0b1; unsigned d1 = x & 0b11; unsigned d2 = x & 0b111; *div = ((((in >> 3) + (in >> 1) + in) << 3) + d0 + d1 + d2) >> 7; *mod = in - (((*div << 2) + *div) << 1); } ``` 題目程式碼如下 ```c void divmod_10(uint32_t in, uint32_t *div, uint32_t *mod) { uint32_t x = (in | 1) - (in >> 2); /* div = in/10 ==> div = 0.75*in/8 */ uint32_t q = (x >> 4) + x; x = q; q = (q >> 8) + x; q = (q >> 8) + x; q = (q >> 8) + x; q = (q >> 8) + x; *div = (q >> 3); *mod = in - ((q & ~0x7) + (*div << 1)); } ``` ### 測驗3 #### 解釋程式碼 ```c static size_t ilog2(size_t i) { size_t result = 0; while (i >= 65536) { result += 16; i >>= 16; } while (i >= 256) { result += 8; i >>= 8; } while (i >= 16) { result += 4; i >>= 4; } while (i >= 2) { result += 1; i >>= 1; } return result; } ``` 用 $2^{16}$、$2^{8}$、$2^{4}$、$2$ 和輸入值 $i$ 比大小,若 $i$ $\ge$ $2^{x}$,表示 $log i$ $\ge$ x。並對 $i$ 右移 x 位元。反之,若 $i$ $\lt$ $2^{x}$ ,則比較 $i$ 和 $2^{y}$ 的大小,${y}$ $\lt$ ${x}$ ,直到最後。 計算以2為底的對數,等同尋找 `fls` ,因此使用 `__builtin_clz` 改寫如下: ```c int ilog32(uint32_t v) { return (31 - __builtin_clz(v)); } ``` #### Linux 核心實例 在 [kernel/sched/fair.c](https://github.com/torvalds/linux/blob/master/kernel/sched/fair.c) 中有部分程式碼使用對數,例如`SCHED_TUNABLESCALING_LOG` 等排程器相關設定。 ### 測驗4 #### 解釋程式碼 首先 EWMA 使用結構體紀錄相關資訊如下: `internal` : 目前的 EWMA `factor` : 定點數的小數位元數 `weight`:EWMA 的衰變率 從 `ewma_read` 在輸出前執行`avg->internal >> avg->factor` 可以發現 EWMA 是以定點數的格式儲存,因此輸出時才要右移 `avg->factor` 個位元。 EWMA原式定義為: $$ s_t = \alpha x_t + (1 - \alpha)s_{t-1}, \ \ t>0 $$ 即隨著 $t$ 增加,前 $t-1$ 個數值的影響逐漸減少。但對應到 `ewma_add` 中: ```c avg->internal = avg->internal ? (((avg->internal << avg->weight) - avg->internal) + (val << avg->factor)) >> avg->weight : (val << avg->factor); ``` 和原式的計算方式略有不同,經過左右同乘 ${1/\alpha}$ 後: $$ s_t/\alpha = x_t + (1/\alpha - 1)s_{t-1} $$ 就會符合程式碼的計算方法,因此可知 `avg->weight` 和 $\alpha$ 的關係為:$$ \alpha = 1/2^{avg->weight}$$ #### Linux 核心實例 ### 測驗5 #### 解釋程式碼 完整程式碼: ```c int ceil_ilog2(uint32_t x) { uint32_t r, shift; x--; r = (x > 0xFFFF) << 4; //16 x >>= r; shift = (x > 0xFF) << 3; //8 x >>= shift; r |= shift; shift = (x > 0xF) << 2; //4 x >>= shift; r |= shift; shift = (x > 0x3) << 1; //2 x >>= shift; return (r | shift | x > 1) + 1; } ``` 首先 `x--` 確保後續的 x > $2^r - 1$ 等比較大小時,能輸出1的 x 必大於 $2^r$。換句話說,避免 x 等於2的冪時,最終輸出的對數取頂會比正確值多1。接著拿 x 和 $2^r - 1$ 比較大小,若 x 較大,則用 r 紀錄對應的 bitwise 右移量,同時 r 也代表 x 的對數,並且使用 shift 持續更新 r 。執行到 `return (r | shift | x > 1) + 1;` 時, x 已經右移30位元,只剩下最後2位元可能非零,即 x 可能為0到3其中之一,所以用 `x > 1` 檢測。最後因為要取頂,所以再加1。 ## [Quiz4](https://hackmd.io/@sysprog/linux2024-quiz4) ### 測驗1 #### 解釋程式碼 popcount 用於計算數值的二進為表示中,有多少位元設定為`1`,其數學式: $$popcount(x) = x - \lfloor{\frac{x}{2}}\rfloor - \lfloor{\frac{x}{4}}\rfloor - ... - \lfloor{\frac{x}{2^{31}}}\rfloor $$ 設 $x = {b_{31}}...{b_{2}}{b_{1}}{b_{0}}$,經過推導後,得到: $$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} $$ 依照上述數學式,我們以 4 個位元(nibble)為一組計算1的個數,即 $x - \lfloor{\frac{x}{2}}\rfloor - \lfloor{\frac{x}{4}}\rfloor - \lfloor{\frac{x}{8}}\rfloor$ ,實作程式碼如下 ```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; } ``` `v >> 1` : 即 $\lfloor{\frac{x}{2}}\rfloor$ 。例如令 v 為7,二進位表示式為0111,右移1位元後就得到3,等同 $\lfloor{\frac{7}{2}}\rfloor$ 。 `n & 0x77777777` : 因為以 nibble 為單位操作,所以要與0x77777777進行 bitwise & 。舉例如下 ```c b7 b6 b5 b4 b3 b2 b1 b0 //v 0 b7 b6 b5 b4 b3 b2 b1 //v >> 1 0 1 1 1 0 1 1 1 //0x77 ------------------------ 0 b7 b6 b5 0 b3 b2 b1 ``` 接著分析 `v = (v + (v >> 4)) & 0x0F0F0F0F` 的功能: 1. 假設 $B_n$ 為前面求出的第 n 個 nibble 的數值,並加總 v 和 v >> 4 ```c B7 B6 B5 B4 B3 B2 B1 B0 //v 0 B7 B6 B5 B4 B3 B2 B1 //v>>4 --------------------------------------------------- B7 (B7+B6) (B6+B5) (B5+B4) (B4+B3) (B3+B2) (B2+B1) (B1+B0) //v + v>>4 ``` 2. 以 `0x0F0F0F0F` 為 mask 得到 ```c B7 (B7+B6) (B6+B5) (B5+B4) (B4+B3) (B3+B2) (B2+B1) (B1+B0) //v + v>>4 0 F 0 F 0 F 0 F ---------------------------------------------------------- 0 (B7+B6) 0 (B5+B4) 0 (B3+B2) 0 (B1+B0) ``` 最後執行 `v *= 0x01010101` ,令 (B1+B0) 為 A0 ,依此類推 ```c 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 A6 0 A4 0 A2 0 A0 A6 0 A4 0 A2 0 A0 A6 0 A4 0 A2 0 A0 -------------------------------------------- ↑ A0+A2+A4+A6 ``` 可以發現在原本 A6 的位置即是 v 的 set bit 的個數,因此將 v 右移24位元即為所求。 #### Hamming Distance Hamming Distance 為兩個整數的二進位表示每個位元差的和,例如 0001 //1 1000 //4 兩數間的 Hamming Distance 為2。以下透過 A $\oplus$ B 計算 A 和 B 之間每個位元差,再用 GCC 內建函式 `__builtin_popcount(x)` 計算位元差的總和。 就 [LeetCode 477. Total Hamming Distance](https://leetcode.com/problems/total-hamming-distance/description/) 的其中一解的程式碼 ```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 >> 1; } ``` 上述程式碼自 num[] 中依序比較 i 和 j 的 Hamming Distance 並記錄在 total ,其時間複雜度為 $O(n^{2})$,因為 num[] 中每個元素都會被算到兩次,所以最後一行右移1位元。 #### 程式碼改進 從位元的角度出發,改進如下 ```c int totalHammingDistance(int* nums, int numsSize) { int total = 0;; for (int i = 0;i < 32;i++){ int bitTotal = 0; for (int j = 0; j < numsSize;j++) bitTotal += nums[j] >> i & 0b1; total += bitTotal * (numsSize - bitTotal); } return total; } ``` 以 nums[3] = {4, 14 ,2}為例,他們的二進位表示法如下 0100 //4 1110 //14 0010 //2 首先自 LSB 觀察,發現3個數值的 LSB 皆為0,所以此位元的 Hamming Distance = 0。 接著觀察 $1^{th}$位元,1有兩個,所以此位元的 Hamming Distance = 2 * numsize - 2。 因此我們可以歸納出每個位元的 Hamming Distance = 該位元1的數量 * numsize - 該位元1的數量,時間複雜度改進為 $O(n)$。 程式中 `i` 表示目前的位元位置, nums[j] 右移後和1執行 bitwise & 以檢測該位元是否為1,並用 `bitTotal` 紀錄此位元1的數量。最後再加總各位元的 `bitTotal * (numsSize - bitTotal)` 即為輸出值。 ### 測驗2 #### 解釋程式碼 ### 測驗3 #### 解釋程式碼 AVL tree 和 red-black tree 雖然樹高都屬於 $O(log n )$ ,但實際上因為 AVL tree 會確保左右子樹的樹高相差不超過`1`,因此 AVL tree 的樹會較 red-black tree 茂密(bushy),然而也必須花費更高成本作旋轉( rotation )。 XTree 嘗試兼具 AVL tree 和 red-black tree 的優點,其中 XTree 的平衡機制類似 AVL tree 。 XTree 的 hint 計算方式為,其左右節點 hint 最大值 +1 並且只有在被呼叫到 xt_update 時才會被更新。 XTree 有4個主要操作, `rotate_left`, `rotate_right`, `replace_right` 和 `replace_left` 。 XTree 在插入和刪除節點後會更新部分節點的 hint ,此時會用到 `rotate_left` 和 `rotate_right` ;移除節點時會用到 `replace_right` 和 `replace_left` 其中之一。 更新 hint 的規則為: `rotate_left` : `rotate_right` `replace_right` `replace_left` ## 閱讀教材心得 ### [Linux 核心的紅黑樹](https://hackmd.io/@sysprog/linux-rbtree) [include/linux/rbtree_types.h](https://github.com/torvalds/linux/blob/master/include/linux/rbtree_types.h) 中 : ```c struct rb_node { unsigned long __rb_parent_color; struct rb_node *rb_right; struct rb_node *rb_left; } __attribute__((aligned(sizeof(long)))); ``` #### 理解 [linux/rbtree.h](https://elixir.bootlin.com/linux/v5.10/source/include/linux/rbtree.h) 中的程式碼 ```c rb_parent(r) ((struct rb_node *)((r)->__rb_parent_color & ~3)) ``` 回傳該節點之親代節點的位置,使用 `__rb_parent_color & ~3` 清除 LSB 數來兩位元後,再將其強制轉型為紅黑數節點的指標。 ```c RB_EMPTY_NODE(node) ((node)->__rb_parent_color == (unsigned long)(node)) RB_CLEAR_NODE(node) ((node)->__rb_parent_color = (unsigned long)(node)) ``` 兩者功能皆為將節點的 `__rb_parent_color` 設定為自己。