# 2024q1 Homework4 (quiz3+4) contributed by < `shlin41` > --- ## 第三週測驗題 --- ### 測驗一 #### 運作原理 原理是將欲求平方根的 $N^2$ 拆成: \begin{aligned} N^2 = (a_n + a_{n-1} + a_{n-2} + ... + a_0)^2,\ where\ a_m = 2^m\ or\ 0 \end{aligned} 進一步擴充: \begin{aligned} Def: P_m \triangleq a_n + a_{n-1} + a_{n-2} + ... + a_m \end{aligned} 則可以得到: \begin{aligned} P_m^2 &= (a_n + a_{n-1} + a_{n-2} + ... + a_{m+1} + a_m)^2\\ &= (P_{m+1} + a_m)^2\\ &= P_{m+1}^2 + 2 \times P_{m+1} \times a_m + a_m^2\\ &= P_{m+1}^2 + (2P_{m+1} + a_m)a_m\\ &= P_{m+1}^2 + Y_m\\ \\ &with\ Y_m \triangleq (2P_{m+1} + a_m)a_m \end{aligned} 我們的目的是從試出所有 $a_m$, $a_m=2^m$or $a_m=0$。從 $m=n$ 一路試到 $m=0$,而求得 $a_m$ 只要試驗 $P_m^2 \leq N^2$ 是否成立。但是計算 $N^2 - P_m^2 \geq 0$ 成本太高,所以在這個基礎上,希望可以找另外一種比較方法。 先定義: \begin{aligned} Def: X_m \triangleq N^2 - P_m^2 \end{aligned} 則可以得到: \begin{aligned} X_m &= N^2 - P_m^2\\ &= N^2 - (P_{m+1}^2 + Y_m)\\ &= N^2 - P_{m+1}^2 - Y_m\\ &= X_{m+1} - Y_m \end{aligned} 再對 $Y_m$ 進行處理: * 定義: \begin{aligned} c_m &\triangleq P_{m+1} \times 2^{m+1}\\ d_m &\triangleq (2^m)^2 \end{aligned} * 然後我們可以改寫 $Y_m$: \begin{aligned} Y_m &= \begin{cases} (2P_{m+1} + a_m)a_m &\text{ if } a_m=2^m \\ 0 &\text{ if } a_m=0 \\ \end{cases}\\ &= \begin{cases} c_m + d_m &\text{ if } a_m=2^m \\ 0 &\text{ if } a_m=0 \\ \end{cases} \end{aligned} * 再改寫 $c_m$ 和 $d_m$ 成遞迴式: \begin{aligned} &c_{m-1} = P_m \times 2^m = (P_{m+1} + a_m) \times 2^m = P_{m+1}2^m + a_m2^m = \begin{cases} c_m/2 + d_m &\text{ if } a_m=2^m\\ c_m/2 &\text{ if } a_m=0 \end{cases}\\ &d_{m-1} = d_m/4 \end{aligned} 結合上述方法撰寫演算法來尋求 $a_n + a_{n-1}+...+a_0$,假設從 $a_n$ 開始往下試。 因為是從 $a_n$ 開始,所以: * $X_{n+1} = N$ * $n$ 是最大的位數,使得 $a_n^2 = (2^n)^2 = d_n^2 = 4^n \leq N^2$,所以用 $d_n$ 迴圈逼近 * $c_n = 0$ `int m` 對應上述理論中的 $d_m$,由於首項 $d_n = (2^n)^2 = 2^{2n}$,可知指數項是 2n,所以程式中的 `& ~1UL` 是為了取偶數 `int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL)` #### ffs / fls 取代 __builtin_clz 用 `fls()` 會相對容易,但是 C POSIX library 只有 `ffs()`。因此直接借用 linux-kernel 原始碼中的 `__fls()`。 查找過程中發現 `__fls()` 有分成硬體實做與軟體實做 * 硬體實做以 x86 為例: ```cpp static __always_inline unsigned long __fls(unsigned long word) { asm("bsr %1,%0" : "=r"(word) : "rm"(word)); return word; } ``` * 軟體實做如下: ```cpp #define BITS_PER_LONG 64 static __always_inline unsigned long __fls(unsigned long word) { int num = BITS_PER_LONG - 1; #if BITS_PER_LONG == 64 if (!(word & (~0ul << 32))) { num -= 32; word <<= 32; } #endif if (!(word & (~0ul << (BITS_PER_LONG - 16)))) { num -= 16; word <<= 16; } if (!(word & (~0ul << (BITS_PER_LONG - 8)))) { num -= 8; word <<= 8; } if (!(word & (~0ul << (BITS_PER_LONG - 4)))) { num -= 4; word <<= 4; } if (!(word & (~0ul << (BITS_PER_LONG - 2)))) { num -= 2; word <<= 2; } if (!(word & (~0ul << (BITS_PER_LONG - 1)))) num -= 1; return num; } ``` 取代部份 ```diff -- for (int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL); m; m >>= 2) { ++ for (int m = 1UL << (__fls(x) & ~1UL); m; m >>= 2) { ``` #### Linux 核心原始程式碼找出對整數進行平方根運算的程式碼 [TODO] #### 嘗試撰寫更快的整數開平分根運算程式 [TODO] --- ### 測驗二 #### 運作原理 題目已經證明:假設我目標的最大數是 $n$ , $l$ 則是比 $n$ 還要小的非負整數。假設 $n = ab$ ($a$ 是十位數 $b$ 是個位數),且 $l = cd$ ($c$ 是十位數,$d$ 是個位數),則只要以 $n$ 算出來的除數 $t$,在 $\dfrac{n}{t}$ 後能保持在精確度內,則 $l$ 除以該除數 $t$ 仍然會在精確度內。 我們的目標 `n = UINT_MAX = 4294967295`,一樣是要找出 $t = \dfrac{a}{2^N}$,並滿足下式 \begin{aligned} 429496729.5 \leq \dfrac{4294967295}{t} \leq 429496729.59 \Rightarrow 9.999999997904524 \leq t \leq 10 \end{aligned} 整段程式還看不懂: ```cpp= #include <stdint.h> 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)); } ``` * line 4 ~ 12: 應該就是模擬 $\dfrac{in}{t} = \dfrac{in \times a}{2^N}$ * 具體怎麼做還看不出來 * 小實驗: `(in | 1)` 改成 `(in)` 的話,答案會在 20 的倍數的 `in` 出錯 * line13: `*mod = in - *dev * 10` * 其中 `q & ~0x7 = *div << 3 = *div * 8` #### 比較 CPU 週期數量 [TODO] #### % 9 (modulo 9) 和 % 5 (modulo 5) 程式碼 [TODO] --- ### 測驗三 #### ilog2() 運作原理 答案的部份 * AAAA = $2^{16} = 32768$ * BBBB = $2^8 = 64$ * CCCC = $2^4 = 16$ * 程式少一個 while (i >= 4) * DDDD = v | 1 整個程式的目標是找出最開頭的 `bit = 1` 的位置。作法是使用二分搜尋法。 解釋第一個 while: 輸入是一個 32 個位元的無號整數 `i`,分成前 16 位元和後 16 位元。如果 $i \geq 2^{16}$,則表示前 16 位元有 `bit = 1` 存在。所以 result += 16,並且把原數字 `i >> 16`,繼續看前 16 位元。反之,如果 $i < 2^{16}$,則表示前 16 位元都是 0。所以 result 保持不變,繼續看後 16 位元。 其他 while 比照處理。 #### 在 Linux 核心原始程式碼找出 log2 的相關程式碼 [TODO] --- ### 測驗四 #### 運作原理 概念上還是 \begin{aligned} S_t = \begin{cases} Y_0 &\text{ if } t = 0\\ \alpha Y_t + (1-\alpha) S_{t-1}&\text{ if } t > 0 \end{cases}\\ \end{aligned} 差別在於存 internal 時,會把 $val \times 2^{factor}$ 後,再存入。讀取平均值時,會把 $\frac{internal}{2^{factor}}$ 後,再讀出。 解析這一段: ```cpp avg->internal = (((avg->internal << avg->weight) - avg->internal) + (val << avg->factor)) >> avg->weight; ``` 上式可改寫為 ```cpp unsigned long S = avg->internal; unsigned long loga = avg->weight; unsigned long logf = avg->factor; S = ((S << loga) - S) >> loga + (val << logf) >> loga; = (S - S >> loga) + (val << logf) >> loga; = S * (1 - 1 >> loga) + (val << logf) >> loga; /* Note: "1 >> loga" is alpha */ ``` 至於為什麼要引入 factor,我尚未了解其目的。 #### 在 Linux 核心原始程式碼找出 EWMA 的相關程式碼 [TODO] --- ### 測驗五 #### 運作原理 答案的部份: GGGG = 1 程式的最後部份 ```cpp shift = (x > 0x3) << 1; x >>= shift; return (r | shift | x > 1) + 1; ``` 等於 ```cpp shift = (x > 0x3) << 1; x >>= shift; r |= shift; shift = (x > 0x1) << 0; x >>= shift; r |= shift; return r + 1; ``` 只是把 $2^0$ 的 shift 部份補上。程式的概念上和測驗三是一樣的。 #### 改進程式碼,使其得以處理 x = 0 的狀況,並仍是 branchless 原程式在 `ceil_ilog2(1) = 1` 且 `ceil_ilog2(0) = 32` 為錯誤值。正確的`ceil_ilog2(1) = 0` 且 `ceil_ilog2(0)` 應該是沒有定義。因此在 `x == 0` 自行定義回傳值為 0。程式碼改進如下: ```diff int ceil_ilog2(uint32_t x) { uint32_t r, shift; + uint32_t mask = ((x > 1) << 5) | ((x > 1) << 4) | ((x > 1) << 3) | + ((x > 1) << 2) | ((x > 1) << 1) | (x > 1); x--; r = (x > 0xFFFF) << 4; x >>= r; shift = (x > 0xFF) << 3; x >>= shift; r |= shift; shift = (x > 0xF) << 2; x >>= shift; r |= shift; shift = (x > 0x3) << 1; x >>= shift; - return (r | shift | x > 1) + 1; + return ((r | shift | x > 1) + 1) & mask; } ``` 使用一個 `mask`,在 `x <= 1` 時, `mask = 0x00000000`,函式回傳 0。`x > 1` 時,`mask = 0x0000003f`,函式回傳舊有的值。 `mask = 0x0000003f` 是因為函式回傳值最大為 $2^5 = 32$。 #### 在 Linux 核心原始程式碼找出 ceil 和 log2 的組合 [TODO] --- ## 第四週測驗題 --- ### 測驗一 #### 運作原理 答案的部份: AAAA = 1 運作原理已在題目講解清楚說明。 #### LeetCode 477 ```cpp int totalHammingDistance(int* nums, int numsSize) { int ans = 0; for(int i = 0; i < 32; i++) { int zeros = 0; int ones = 0; unsigned int mask = 1U << i; for(int j = 0; j < numsSize; j++) (nums[j] & mask) ? ones++ : zeros++; ans += ones * zeros; } return ans; } ``` Time Complexity: O(n) Space Complexity: O(1) --- ### 測驗二 --- ### 測驗三 #### 運作原理 參考之前的作業 [reference](https://hackmd.io/@shlin41/linux2023-summer-quiz) **1. 找 subtree 中的 max/minminmin** ``` xt_first(root) --> 找出 root 這個 subtree 中的最小 node xt_last(root) --> 找出root 這個 subtree 種的最大 node ``` **2. Rotation:調整樹高的基本操作** ``` xt_rotate_left(node) --> 把 Lchild 轉到 node 位置上 (演算法書的 rotate_right) xt_rotate_right(node) --> 把 Rchild 轉到 node 位置上 (演算法書的 rotate_left) ``` **3. 計算樹高與平衡性:用來決定要不要調整的依據** ``` xt_balance(node) --> L_hint - R_hint (左樹高 - 右樹高) xt_max_hint(node) --> 算 hint(回傳樹高) ``` **4. 決定是否調整樹的架構:這個 function 可以快速 implement 成 AVL-Tree** ``` xt_update(**root, *node) --> 調整樹的架構 ``` > AVL-Tree Implementation: >     *~~if (n->hint == 0 || n->hint != prev_hint)~~ >       xt_update(root, p);* > 把條件式刪除讓 root 到 node 間的所有 xt_node 都 xt_update() 一遍,就變成 AVL-Tree。但是這樣會增加 st_update() 的時間。 **5. 把 nodeA 用 nodeB 取代:屬於一種 node 刪除法**  (a) 概念上等於 swap(nodeA, nodeB) 再砍掉 nodeA  (b) nodeB 原本的位置屬於好刪除的,所以先把 nodeA 換過去 ``` xt_replace_left(*node, *left) --> 拿 left 取代 node,left 的位置在 node 左子樹 xt_replace_right(*node, *right) --> 拿 right 取代 node,right 的位置在 node 右子樹 ``` **6. Insert node 和 Remove node 到 XTree 中** ``` __xt_insert(**root, *p, *n, L/R) --> 在 p 的 L/R 加入 child node n xt_insert(*tree, *key) --> 在 tree 加入一個 node(key) __xt_remove(**root, *del) --> 在 tree 刪除 node del xt_remove(*tree, *key) --> 先找到 node(key),再刪除 ``` #### 指出上述程式碼可改進之處 [TODO] #### 設計效能評比程式 [TODO]