# linux2024-homework4 contributed by < [SHChang-Anderson](https://github.com/SHChang-Anderson) > ## 第三週測驗題 > [完整題目](https://hackmd.io/@sysprog/linux2024-quiz3) ### 測驗一 #### 計算開平方根: (版本一) ```c #include <math.h> int i_sqrt(int N) { int msb = (int) log2(N); int a = 1 << msb; int result = 0; while (a != 0) { if ((result + a) * (result + a) <= N) result += a; a >>= 1; } return result; } ``` `int msb = (int) log2(N);` 找到變數 `N` 的最高有效位 (most significant bit) 位置。 計算最高有效位的值存入變數 `a` 中。 進入迴圈,使用**逐位元掃描**的方法,從最高有效位開始,逐位檢查是否可以將對應的值加入到 `result` 中,而不會讓 `result` 的平方超過 `N`。此方法的特色在於相較於逐一數值嘗試開平方根近似的方式,利用了二進制表示的特性,每次只需要檢查一個位元,因此能夠更有效地近似計算出整數平方根。 #### 計算開平方根: (版本二) ```diff int i_sqrt(int N) { + int msb = 0; + int n = N; + while (n > 1) { + n >>= 1; // 將 n 右移一位,相當於除以 2 + msb++; // 計數器加 1 + } - int msb = (int) log2(N); int a = 1 << msb; int result = 0; while (a != 0) { if ((result + a) * (result + a) <= N) result += a; a >>= 1; } return result; } ``` 與版本一不同於**不使用** `log2` 函式,而是使用迭代計算的方式找到最高位元。 這樣的優勢在於可以避免使用到浮點數運算,也可以在不支持 `log2` 的環境中運行。 #### 計算開平方根: (版本三) 這個版本的開方根利用 [Digit-by-digit calculation](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Digit-by-digit_calculation) 的概念實作開平方根。 首先若要對 $x$($x\geq0$) 做開平方根 ,可以假設 $x = N^2$ , $N$ 即為欲求得的平方根數值,接著,將 $N$ 改寫為 2 的羃次和,即: $N^2=(a_{n}+a_{n-1}+a_{n-2}+...+{a_0})^2, a_m=2^m\ or\ a_m=0$ 。 若將 $(a_{n}+a_{n-1}+a_{n-2}+...+{a_0})^2$ 做展開,透過矩陣觀察: $\left[ \begin{array}{ccccc} a_0a_0 & a_1a_0 & a_2a_0 & ... & a_na_0 \\ a_0a_1 & a_1a_1 & a_2a_1 & ... & a_na_1 \\ a_0a_2 & a_1a_2 & a_2a_2 & ... & a_na_2 \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ a_0a_n & a_1a_n & a_2a_n & ... & a_na_n \\ \end{array} \right]$ 主對角線元素: $\left[ \begin{array}{ccccc} a_0a_0 & \\ & a_1a_1 & \\ & & a_2a_2 & \\ \ & \ & \ & \ddots & \ \\ & & & & a_na_n \\ \end{array} \right]$ 其餘元素: $\left[ \begin{array}{ccccc} & a_1a_0 & a_2a_0 & ... & a_na_0 \\ a_0a_1 & & a_2a_1 & ... & a_na_1 \\ a_0a_2 & a_1a_2 & & ... & a_na_2 \\ \vdots & \vdots & \vdots & \ & \vdots \\ a_0a_n & a_1a_n & a_2a_n & ... & \\ \end{array} \right]$ 將主對角線元素與其於元素分開討論可將原式整理成: $N^2 = (\sum_{i=0}^n a_i)^2 = \sum_{i=0}^n a_i^2 + 2\sum_{0\leq i<j\leq n}a_ia_j$ 其中, $\sum_{i=0}^n a_i^2$ 為對角線上的平方項,另外 $2\sum_{0\leq i<j\leq n}a_ia_j$ 為其餘的元素交叉相乘展開項。 接著將等式拆解為: $$ \begin{align*} \sum_{i=0}^n a_i^2 &+ 2\sum_{0\leq i<j\leq n}a_ia_j \\ &= a_n^2 + \sum_{i=0}^{n-1}a_i^2 + 2a_n\sum_{i=0}^{n-1}a_i + 2\sum_{0\leq i<j\leq n-1}a_ia_j \end{align*} $$ 移項之後做觀察: $$ \begin{align*} \sum_{i=0}^n a_i^2 &+ 2\sum_{0\leq i<j\leq n}a_ia_j \\ &= a_n^2 + 2a_n\sum_{i=0}^{n-1}a_i + (\sum_{i=0}^{n-1}a_i^2 + 2\sum_{0\leq i<j\leq n-1}a_ia_j ) \end{align*} $$ 觀察括號內的數學式,可將 $(\sum_{i=0}^{n-1}a_i^2 + 2\sum_{0\leq i<j\leq n-1}a_ia_j)$ 改寫為:$(\sum_{i=1}^n a_i)^2$ 最終整理得到: $N^2 = a_0^2 + 2a_0(\sum_{i=1}^n a_i) + (\sum_{i=1}^n a_i)^2$ 令 $P_m = a_n+a_{n-1}+...+a_m$ 則所求 $N = P_0$ 。 接著將計算式整理成: $N^2 = P_0^2 = a_0^2 + 2a_0(P_1^2) + (P_1)^2$ 若推展成一般式可得: $P_m^2 = a_m^2 + 2a_mP_{m+1} + P_{m+1}^2 = P_{m+1}^2 + a_m(2P_{m+1} + a_m)$ $P_m^2 = P_{m+1}^2+a_m(2P_{m+1} + a_m)$ 可以將 $a_m(2P_{m+1} + a_m)$ 令為 $Y_m$ ,則 $P_m^2 = P_{m+1}^2+Y_m$ 。 若從從 $m=n$ 一路嘗試計算到 $m=0$ 每一輪透過 $Y_m$ 得到下一輪次的 $P_m^2$ 並測試 $P_m^2 \leq N^2$ 是否成立,最終即可找到所求。 然而,每輪計算 $P_m^2$ 的成本過高,若將 $N^2 - P_m^2$ 計算結果令為 $X_m$ ,則可推得 $X_{m} = N^2 - P_{m}^2 = N^2 - (P_{m+1}^2 +Y_{m})$ ,最終推得遞迴式: $X_{m}=X_{m+1} - Y_{m}$ 。 這樣一來透過方程式 $Y_m = P_m^2 - P_{m+1}^2 = 2P_{m+1}a_m+a_m^2$ ,紀錄上一輪的 $P_{m+1}$ 來計算 $Y_m$ 以這樣的方式避免較高的運算成本。 為了實現演算法設計,進一步將 $Y_m$ 拆成 $c_m$ 與 $d_m$ ,得到: $c_m = P_{m+1}2^{m+1}$ $d_m = (2^m)^2$ $Y_m=\left. \begin{cases} c_m+d_m & \text{if } a_m=2^m \\ 0 & \text{if } a_m=0 \end{cases} \right.$ 可以藉由位元運算從 $c_m, d_m$ 推出下一輪 $c_{m-1}, d_{m-1}$ ,再利用 $c_{m-1}, d_{m-1}$ 計算出 $Y_m$ 最終推得 $a_m$ 。 * $c_{m-1}=P_m2^m=(P_{m+1}+a_m)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}=\dfrac{d_m}{4}$ 綜合以上方法使用演算法尋求 $P_0$ ($a_n+a_{n-1}+...+a_0$) ,從 $P_n$ 的初始條件: * $X_{n+1} = N^2$ * $X_{n} = X_{n+1} - Y_n$ * $X_{n} = X_{n+1} - (c_n + d_n)$ * $c_n = 0$ * $P_{n+1} = 0 \to c_n=0$ * $d_n = a_n^2=(2^n)^2= 4^n$ -------- ```c int i_sqrt(int x) { if (x <= 1) /* Assume x is always positive */ return x; int z = 0; for (int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL); m; m >>= AAAA) { int b = z + m; z >>= BBBB; if (x >= b) x -= b, z += m; } return z; } ``` 演算法中 `z` 對應到上述推導的 $c_n$, 而 `m` 對應到上述推導的 $d_n$。 由於初始的 $c_n = 0$, 將 `z` 設為 0: `int z = 0;` 另一方面, $d_n = a_n^2 = (2^n)^2 = 4^n$ ,因此可以利用以下程式碼計算 `m`: `int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL);` ------ ```c for (int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL); m; m >>= AAAA) { int b = z + m; z >>= BBBB; if (x >= b) x -= b, z += m; } ``` 在迴圈中, `int b = z + m;` , `b` 對應到推導中的 $Y_m$ 。 而由上述推導 $c_{m-1}=\begin{cases} c_m/2+d_m & \text{if }a_m=2^m \ c_m/2 & \text{if }a_m=0 \end{cases}$ 可知,無論 $a_m$ 結果為何,都需要將 $c_m / 2$ ,因此 `z >>= BBBB;` 就是 $c_m / 2$ 的實作, `BBBB` 應替換為 1 。 另外,由 $d_{m-1}=\dfrac{d_m}{4}$ 知道每一輪迴圈,需要將變數 `m` 除以 4, `m >>= AAAA` 應改為 `m >>= 2` 。 ```c if (x >= b) x -= b, z += m; ``` 以上條件判斷中, `if (x >= b)` 表 $X_{m+1} > Y_{m}$ 又 $X_{m}=X_{m+1} - Y_{m}$ 則 $N^2 > P_m^2$ ,因此將 $a_m$ 加入 `z` 當中,因為 $c_{-1} = P_0 = a_n+a_{n-1}+...+a_0$ 因此最終所求即 `z` 。 ------ #### 嘗試用 ffs / fls 取代 `__builtin_clz` ```c int i_sqrt(int x) { if (x <= 1) /* Assume x is always positive */ return x; int z = 0; for (int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL); m; m >>= 2) { int b = z + m; z >>= 1; if (x >= b) x -= b, z += m; } return z; } ``` `__builtin_clz(x)` 函式回傳 `x` 的最高有效位前面連續的 `0` 位元的數量,那麼 `31 - __builtin_clz(x)` 就是最高有效位的位置。 同樣 `fls(x) - 1` 也可以找到最高有效位的位置,但 `fls()` 由索引值 1 開始計算,因此需要將 `fls(x) - 1`,從而計算需要左移多少位元才能得到最接近且不大於 `x` 的 2 的冪次數。 因此可以將程式碼改寫為: ```diff int i_sqrt(int x) { if (x <= 1) /* Assume x is always positive */ return x; int z = 0; + int shift = (fls(x) - 1); + for (int m = 1U << ((shift) & ~1U); m; m >>= 2) { - for (int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL); m; m >>= 2) { int b = z + m; z >>= 1; if (x >= b) x -= b, z += m; } return z; } ``` #### 在 Linux 核心找出對整數進行平方根運算的程式碼 在 [lib/math/int_sqrt.c](https://github.com/torvalds/linux/blob/master/lib/math/int_sqrt.c),找到整數平方根運算的程式碼: ```c unsigned long int_sqrt(unsigned long x) { unsigned long b, m, y = 0; if (x <= 1) return x; m = 1UL << (__fls(x) & ~1UL); while (m != 0) { b = y + m; y >>= 1; if (x >= b) { x -= b; y += m; } m >>= 2; } return y; } ``` 程式碼風格與實做原理與測驗一(版本三)類似,比較值得注意程式碼使用到 `__fls(x)` 來找到需要位移的位元數量,閱讀過去探討過關於 [ffs 及 __ffs 加雙底線與否的不同](https://hackmd.io/@sysprog/ByS9w_lHh#%E4%BB%BB%E5%8B%99%E7%B0%A1%E8%BF%B0) 了解`ffs` 及 `__ffs` (是否加雙底線) 的不同之處:參考 `bitops` 系列對外的介面: [arch/arc/include/asm/bitops.h](https://github.com/torvalds/linux/blob/master/arch/arc/include/asm/bitops.h) 中的註解得知: * __XXX 系列: 以 index 0 索引,範圍為 0 ~ {length of input type - 1} * XXX 系列: 以 index 1 索引,範圍為 1 ~ {length of input type} 由此可知使用 `__fls(x)` 來找到需要位移的位元數量,不需要 - 1 。 ### 測驗二 #### `mod 10` 和 `div 10` 連續操作 本測驗針對正整數在相鄰描述進行 `mod 10` 和 `div 10` 操作的場景進行探討。若要優化這個計算情況,最直觀的方式是使用餘式定理。 根據餘式定理: 被除數 = (商 * 除數) + 餘數 對應到程式碼就是: `tmp = (tmp / 10) * 10 + (tmp % 10)` 因此可以合併 mod 10 和 div 10 操作,改寫為以下程式碼: ```c carry = tmp / 10; tmp = tmp - carry * 10; ``` 若使用 bitwise operation 實作以上除法,會發現由於 10 存在因數 5 並非 2 的羃,因此可能會產生誤差。 測驗中題到了 `tmp` 不可能會大於 19 ,因此只需要考慮 19~0 的情況即可。其原因為: * tmp 是透過計算 `(b[i] - '0') + (a[i] - '0') + carry` 得到的。 `b[i]` 和 `a[i]` 分別是字符 '0' 到 '9' 之間的數字字符,對應的數值範圍是 0 到 9。 * 所以 (b[i] - '0') 和 (a[i] - '0') 的值範圍都是 0 到 9。 * 將兩個 0 到 9 之間的數相加,最大值為 9 + 9 = 18。 * 再加上最大可能的進位值 1,最大結果就是 18 + 1 = 19 接著,繼續針對方法繼續探究,針對此問題,提出了猜想: * 我們的目標是找到一個適當的除數 q ,使得 tmp / q 的結果至少在小數點後一位是精確的。 * 假設最大的被除數是 n ,我們設 l 是一個比 n 小的非負整數。 * 現在考慮兩個數 ab 和 cd ,其中 a 和 c 是十位數 , b 和 d 是個位數。 * 如果存在一個除數 q ,使得 cd / q 的結果在精確度範圍內,那麼 ab / q 的結果也應該在精確度範圍內。 假設: * $n = ab$ ($a$ 是十位數 $b$ 是個位數) * $l = cd$ ($c$ 是十位數 $d$ 是個位數) ---- 以下證明上述猜想: $a.b\leq\frac{n}{x}\leq a.b9\\ \Rightarrow\frac{n}{a.b9}\leq x\leq\frac{n}{a.b}$ 分別對左右不等式進行探討: $\frac{n}{a.b9}\leq x\leq\frac{n}{a.b}$ * 右不等式: * $x\leq\frac{n}{a.b}$ 得知 $x\leq10$ 必然再精度以內。 * 左不等式: * $\frac{n}{a.b9}\leq x$ 接著討論 $c.d\leq\frac{l}{x}\leq c.d9$,若我們將 $\frac{n}{a.b9}$ 代入 $x$ 可將不等式改寫為: $c.d\leq l\times\frac{a.b9}{n}\leq c.d9$ 分別將 $l$ 與 $n$ 替換為 $cd$ $ab$ 可表現為: $c.d\leq cd\times\frac{a.b9}{ab}\leq c.d9$ * 右不等式: * $cd\times\frac{a.b9}{ab}\leq c.d9$ , 首先將不等式改寫為: $cd\times\frac{a.b + 0.09}{ab}\leq c.d + 0.09\Rightarrow cd\times (\frac{1}{10}+ \frac{0.09}{a.b})\leq c.d + 0.09$ ,又透過分配律可得到:$c.d + \frac{0.09cd}{ab}\leq c.d + 0.09$,由於 $ab > cd$ 因此 $\frac{0.09cd}{ab}\leq 0.09$ ,上述不等式必成立。 * 左不等式: * $c.d\leq cd\times\frac{a.b9}{ab}\Rightarrow c.d \leq cd\times (\frac{1}{10}+ \frac{0.09}{a.b}) \Rightarrow c.d \leq c.d + \frac{0.09}{a.b}$ 明顯成立。 -------- 由上述證明可得知,若 `tmp` 不可能會大於 19 ,只須透過不等式: $1.9\leq \frac{19}{x}\leq 1.99\\ \Rightarrow 9.55\leq x\leq10$ 即可得知,除數介於 $9.55$ 至 $10$ 之間皆可程式中達到相同效果。 找除數的方法使用 bitwise operation $\frac{2^N}{a}$ 找到介於 $9.55\leq x\leq10$ 的除數,若欲處理得數字為$n$ 商式可以寫成 $\frac{an}{2^N}$ 。 $2^N=128, a=13,\frac{128}{13}\approx9.84$ 為一個可用的除數,由於 13 可以拆成 $13 = 8 + 4 + 1 = 2^3 + 2^2 + 2^0$ $2$ 的羃相加,因此範例程式中透過 `(tmp >> 3) + (tmp >> 1) + tmp` 得到 $\frac{13tmp}{8}$ 再將此式乘上 8 (向左位移 3 bits) 即可得到 $13tmp$ ,只要再將其除以 $128$ ($2^7$) 即可得到目標商式 $\frac{13tmp}{2^7}$ 。 #### 包裝後函式探討 ```c #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 >> CCCC); *mod = in - ((q & ~0x7) + (*div << DDDD)); } ``` `uint32_t x = (in | 1) - (in >> 2);`: 這行程式碼初始化 x。表達式 `(in | 1)` 確保 in 是奇數(將最低位設置為 1),然後減去向右移位 2 的 `in`(相當於將 `in` 除以 4),得到一個大約等於 $in \times 0.75$ ($\frac{3}{4}$) 的近似值。 `uint32_t q = (x >> 4) + x;` 相當於 $\frac{3}{4*2^4}in + \frac{3}{4}in$ 亦等於 $\frac{51}{64}in$ 若換算為小數約為 $0.79 \cdot in$ ,因此目前 `q` 值接近 $\frac{8}{10} \cdot in$ 。 ```c x = q; q = (q >> 8) + x; q = (q >> 8) + x; q = (q >> 8) + x; q = (q >> 8) + x; ``` 接著不斷透過持續的 bitwise 操作使的 `q` 值持續逼近 $\frac{8}{10} \cdot in$ 。 `*div = (q >> CCCC);` 為了使商值 `*div` 正確被指定,`CCCC` 應替換為 `3` 得到 $\frac{8}{10}in \times \frac{1}{8} = \frac{in}{10}$ 。 最後 `*mod = in - ((q & ~0x7) + (*div << DDDD)); ` 應為透過餘式定理的餘數計算。根據餘式定理: 餘數 = 被除數 - 除數\*商, `((q & ~0x7) + (*div << DDDD));` 計算的就是除數\*商的部份也就是 $quotient \cdot 10$ 。 `((q & ~0x7)` 的操作即 `q & 0xFFFFFFF8` 即將 `q` 最後三個位元清 0 ,這樣的作法使得目前 `q` 等價於 $quotient \cdot 8$ ,由於所求為 $quotient \cdot10 \Rightarrow quotient\cdot(8 + 2)$ 因此 `(*div << DDDD))` `DDDD` 應替換為 1 相當於 $quotient \cdot 2$ 以符合預期。 :::info TODO:撰寫不依賴任何除法指令的 % 9 (modulo 9) 和 % 5 (modulo 5) 程式碼。 ::: ### 測驗三 #### ilog2 以 2 為底的對數 (版本一) ```c int ilog2(int i) { int log = -1; while (i) { i >>= 1; log++; } return log; } ``` * 首先將 `log` 設為 -1。這樣做是為了在輸入值為 0 時,函式回傳 -1。 * 接著進入一個 `while` 迴圈,當 `i` 不為 0 時持續執行迴圈體內的操作。 * 在迴圈內, `i` 右移一位 (`i >>= 1`)。這個操作相當於將 `i` 除以2。 * 每執行一次右移操作,就將 `log` 的值加1。 * 當 `i` 變為 0 時,迴圈終止。 * 最後,函式即可求得輸入值 `i` 的對數值並回傳。 #### ilog2 以 2 為底的對數 (版本二) ```c static size_t ilog2(size_t i) { size_t result = 0; while (i >= AAAA) { result += 16; i >>= 16; } while (i >= BBBB) { result += 8; i >>= 8; } while (i >= CCCC) { result += 4; i >>= 4; } while (i >= 2) { result += 1; i >>= 1; } return result; } ``` 這段程式碼每一次檢查一半的位元數量並進行 bitwise 位移,這種作法的優點是計算速度更快,因為它將對數值的計算分成了多個階段,每個階段只需要處理一小部分位元,而不是像前一種方法那樣逐位處理整個數值。 分段計算:這段程式碼將對數值的計算分成了多個階段,每個階段對應不同的位移量(16, 8, 4, 1)。這種做法可以加速計算過程,尤其是在處理大數值時。 這段程式碼將對數值的計算分成了多個階段,每個階段對應不同的位移量。可以觀察程式碼從最高 16 位元進行判別,接著是 8 位元 4 位元,直到最後一個位元為止。透過這樣的觀察我們可以知道 `AAAA` 、`BBBB` 、 `CCCC` 分別對映為 $2^{16}$ 、$2^{8}$ 、$2^{4}$ 。 可以發現這樣的方法就是尋找 `i` 最高有效位的位置。 #### Linux 核心 log2 的相關程式碼 在 [linux/log2.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/log2.h) 中可以找到 log2 的相關實作。 ```c int __ilog2_u32(u32 n) { return fls(n) - 1; } int __ilog2_u64(u64 n) { return fls64(n) - 1; } ``` 可以看到 log2 使用到測驗一提到的 `fls` 也就是透過`fls(x) - 1` 找到最高有效位的位置達成與 ilog2 以 2 為底的對數 (版本二) 相同的效果。 ### 測驗四 #### EWMA 理解 EWMA (指數加權移動平均) 是一種統計資料取平均的手法,其數學定義如下: $S_t = \begin{cases} Y_0& t = 0 \\ \alpha Y_t + (1 - \alpha)\cdot S_{t-1}& t > 0 \end{cases}$ 其中: - $S_t$ 為第 $t$ 個時間點的 EWMA 值 - $Y_t$ 為第 $t$ 個時間點的觀測值 - $\alpha$ 為歷史資料加權常數,介於0與1之間 當 $\alpha$ 值越大時,EWMA 會給予較多的權重於最近的觀測值,因此計算出的平均曲線會較為敏感,能夠快速反映最新的數據變化。反之,若 $\alpha$ 值較小,則 EWMA 會給予較多權重於歷史數據,計算出的平均曲線會較為平滑,變化也相對較小。 #### EWMA 實作 閱讀並理解測驗四中對於 EWMA 實作。首先,先觀察結構體 `ewma`: ```c struct ewma { unsigned long internal; unsigned long factor; unsigned long weight; }; ``` 結構體中使用 `unsigned long` 除存所有參數,使用 2 的羃來除存所有參數以及權重。 ```c void ewma_init(struct ewma *avg, unsigned long factor, unsigned long weight) { if (!is_power_of_2(weight) || !is_power_of_2(factor)) assert(0 && "weight and factor have to be a power of two!"); avg->weight = ilog2(weight); avg->factor = ilog2(factor); avg->internal = 0; } ``` `ewma_init` 函式用於初始化結構體中的參數。在函式內部,我們觀察到對要初始化的參數進行了檢查,確保其值是 2 的冪次方。這樣做是為了後續使用位元操作來提高效能,代替乘除法的運算。接著,將這些參數轉換為對數形式,以便後續的處理。 值得注意的是,透過程式碼中對於 2 的冪次方的檢測,可以得知實作希望使用定點數進行加權平均的計算。此外,在函式的註解中提到,`factor` 參數被用作準備定點數運算所需的平移值。 ```c struct ewma *ewma_add(struct ewma *avg, unsigned long val) { avg->internal = avg->internal ? (((avg->internal << EEEE) - avg->internal) + (val << FFFF)) >> avg->weight : (val << avg->factor); return avg; } ``` `ewma_add` 是實際執行 EWMA 計算的函式,其中 `internal` 對應到 $S_t$;而 `val` 對應到 $Y_t$。我推測 `(((avg->internal << EEEE) - avg->internal) + (val << FFFF)) >> avg->weight` 即為上述數學定義:$\alpha Y_t + (1 - \alpha)\cdot S_{t-1}$ 的實作方式。 我注意到當 `avg->internal` 為 0 時,函式會執行 `(val << avg->factor);`,也就是說當初始計算 EWMA 時尚未有任何資料,直接將目前時間點的觀測值加入。這時函式將 `val` 向左位移,因此我推測 `(val << FFFF)` 也應該將 `val` 向左位移,由此可知 `FFFF` 應該替換為 `avg->factor`。 接著,我繼續探究 `((avg->internal << EEEE) - avg->internal)` 程式碼部份。假設將 `EEEE` 暫時設置為變數 $x$,則由於 `weight` 以對數方式儲存 `(((avg->internal << EEEE) - avg->internal) + (val << FFFF)) >> avg->weight` 在數學上的意義為:$((S_{t-1} \cdot2^x - S_{t-1})+Y_t) \times \frac{1}{2^{weight}}$。然而目標數學方程式應該為:$\alpha Y_t + (1 - \alpha)\cdot S_{t-1}$,觀察可發現後者的 $\alpha$ 應為前式的 $\frac{1}{2^{weight}}$。因此,$(S_{t-1} \cdot2^x - S_{t-1}) \times \frac{1}{2^{weight}}$ 應該與 $(1 - \frac{1}{2^{weight}})\cdot S_{t-1}$ 等價,由此可推得 $x=weight$,因此 `FFFF` 應該替換為 `avg->weight`。 #### 在 Linux 核心原始程式碼找出 EWMA 的相關程式碼 在 [linux/average.h](https://github.com/torvalds/linux/blob/master/include/linux/average.h) 可以找到 EWMA 實作程式碼。 ```c #define DECLARE_EWMA(name, _precision, _weight_rcp) ``` [linux/average.h](https://github.com/torvalds/linux/blob/master/include/linux/average.h) 定義了 `DECLARE_EWMA` 巨集,這個巨集接受了三個參數, name(用於生成的 struct 和函式名稱)、 _precision (表示用於儲存小數部分的位元數)和 _weight_rcp (一個 2 的冪,決定了新舊值的加權)。 ------ ```c struct ewma_##name { \ unsigned long internal; \ }; ``` [linux/average.h](https://github.com/torvalds/linux/blob/master/include/linux/average.h) 定義了一個結構體 ewma_,包含一個 unsigned long 型態的成員,用於儲存 EWMA 值。 -------- ```c static inline void ewma_##name##_init(struct ewma_##name *e) { \ BUILD_BUG_ON(!__builtin_constant_p(_precision)); \ BUILD_BUG_ON(!__builtin_constant_p(_weight_rcp)); \ /* \ * Even if you want to feed it just 0/1 you should have \ * some bits for the non-fractional part... \ */ \ BUILD_BUG_ON((_precision) > 30); \ BUILD_BUG_ON_NOT_POWER_OF_2(_weight_rcp); \ e->internal = 0; \ } ``` ewma_init() 函式用於初始化結構實例,將 internal 設為 0。並使用了 BUILD_BUG_ON 巨集,在編譯時檢查 _precision 和 _weight_rcp 參數是否符合要求。 ---- ```c ewma_##name##_read(struct ewma_##name *e) { BUILD_BUG_ON(!__builtin_constant_p(_precision)); BUILD_BUG_ON(!__builtin_constant_p(_weight_rcp)); BUILD_BUG_ON((_precision) > 30); BUILD_BUG_ON_NOT_POWER_OF_2(_weight_rcp); return e->internal >> (_precision); } ``` `ewma_read()` 函式用於讀取 EWMA 值。由於 EWMA 實作使用了定點數運算,因此 internal 成員儲存了一個經過左移_precision 位的值。`internal` 成員儲存了經過放大的 EWMA 值(包含整數和小數部分),而 `ewma_read()` 的右移操作則是為了將它縮小回原始的整數 EWMA 值。 ---- ```c static inline void ewma_##name##_add(struct ewma_##name *e, unsigned long val) { unsigned long internal = READ_ONCE(e->internal); unsigned long weight_rcp = ilog2(_weight_rcp); unsigned long precision = _precision; BUILD_BUG_ON(!__builtin_constant_p(_precision)); BUILD_BUG_ON(!__builtin_constant_p(_weight_rcp)); BUILD_BUG_ON((_precision) > 30); BUILD_BUG_ON_NOT_POWER_OF_2(_weight_rcp); WRITE_ONCE(e->internal, internal ? (((internal << weight_rcp) - internal) + (val << precision)) >> weight_rcp : (val << precision)); } ``` ewma_add() 函式是 EWMA 計算關鍵,用於將新值納入 EWMA 計算。它首先讀取 internal 值,根據 `_weight_rcp` 的值決定歷史資料與目前資料的加權。 #### 相關應用程式碼 [ath11k/core.h](https://github.com/torvalds/linux/blob/master/drivers/net/wireless/ath/ath11k/core.h) 找到 `DECLARE_EWMA(avg_rssi, 10, 8)` 定義了 EWMA 結構體,由 [linux/average.h](https://github.com/torvalds/linux/blob/master/include/linux/average.h) 可得知 `fixed-precision values` 為 10 而 $weight = \frac{1}{weight_{rcp}} = \frac{1}{8}$。 * **ath11k: 高通 IEEE 802.11ax 裝置的 Linux 驅動程式:** 根據 [Wireless Wiki](https://wireless.wiki.kernel.org/en/users/drivers/ath11k),ath11k 是針對高通的 IEEE 802.11ax 無線網路裝置所設計的 Linux 驅動程式。它能夠支援在 SoC 類型裝置中的 AHB 匯流排和 PCI 接口。ath11k 基於 mac80211,這是 Linux 核心中用於無線網路裝置的通用框架。 * **`avg_rssi` 和 EWMA :** 我參考了 [Received Signal Strength Indicator](https://en.wikipedia.org/wiki/Received_signal_strength_indicator) 一文來理解 RSSI。根據該資料,RSSI 是衡量設備從接收端接收訊號能力的指標,用於評估無線通訊中訊號的強度和品質。在 ath11k 的程式碼中,有一個名為 `avg_rssi` 的變數,被用來儲存指數加權移動平均值 (EWMA)。透過 EWMA,能夠平滑訊號強度的變化,提供更穩定的接收訊號強度指標 (RSSI)。 ### 測驗五 #### `ceil_ilog2` 程式碼理解 `ceil_log2` 這個程式碼實現了一個函式,用於計算給定的 32 位元無號整數 $x$ 的最小次方指數值向上進位的結果。也就是說,對於傳入的參數 $x$ ,回傳最小的整數 n,滿足 $2^n \geq x$。 可以注意到函式的最開始將 x 減 1,這樣可以確保當 x 是 2 的冪次時,計算出的指數正確。 ```c r = (x > 0xFFFF) << 4; x >>= r; shift = (x > 0xFF) << 3; x >>= shift; r |= shift; shift = (x > 0xF) << 2; x >>= shift; r |= shift; ``` 接著根據以上程式碼的操作,我們可以觀察到類似於測驗三中的使用以 [2 為底的對數 (版本二)](https://hackmd.io/ahBQ35SOQyu1CmzVljikLQ?view#ilog2-%E4%BB%A5-2-%E7%82%BA%E5%BA%95%E7%9A%84%E5%B0%8D%E6%95%B8-%E7%89%88%E6%9C%AC%E4%BA%8C) 的二分搜尋法來找到最高位元位置。然而,與[測驗三程式碼](https://hackmd.io/ahBQ35SOQyu1CmzVljikLQ?view#ilog2-%E4%BB%A5-2-%E7%82%BA%E5%BA%95%E7%9A%84%E5%B0%8D%E6%95%B8-%E7%89%88%E6%9C%AC%E4%BA%8C)不同的是,這裡使用變數 `r` 來記錄位移量,而 `(x > 0xFFFF) << 4;` 的操作等價於以下程式碼: ```c while (i >= 65536) { result += 16; i >>= 16; } ``` 在這裡,`(x > 0xFFFF)` 的結果是一個布林值。如果 `x` 的高 16 位元不為 0,則 `(x > 0xFFFF)` 的結果為 True,亦即等於 1。因此,位移量 `r` 被設定為 `1 << 4`,即 $16$,達到與[測驗三 log2 程式碼](https://hackmd.io/ahBQ35SOQyu1CmzVljikLQ?view#ilog2-%E4%BB%A5-2-%E7%82%BA%E5%BA%95%E7%9A%84%E5%B0%8D%E6%95%B8-%E7%89%88%E6%9C%AC%E4%BA%8C)相同的效果。 ```c shift = (x > 0xFF) << 3; x >>= shift; r |= shift; ``` 值得注意的是,在程式碼中除了執行位移操作外,還將目前的位移量與變數 `r` 做了 $OR$ 運算,即 `r |= shift;`。由於位移量都是 2 的冪次方,因此這樣的 $OR$ 運算等同於對位移量進行加法操作(`result +=`)。因此,在進行位移後,程式碼將持續累加位移量,以找到最高位元位置。 ```c return (r | shift | x > GGG) + 1; ``` 最終函式回傳,總位移量 + 1,然而若對照[測驗三 log2 程式碼](https://hackmd.io/ahBQ35SOQyu1CmzVljikLQ?view#ilog2-%E4%BB%A5-2-%E7%82%BA%E5%BA%95%E7%9A%84%E5%B0%8D%E6%95%B8-%E7%89%88%E6%9C%AC%E4%BA%8C)可發現,少了一個條件判斷: ```c while (i >= 2) { result += 1; i >>= 1; } ``` 因此 `| x > GGG` 即判斷 $x$ 是否 $\geq$ $2$,也因此 `GGG` 應填入 $1$ 以符合預期。 #### 改進程式碼 試想上述程式碼若傳入參數 $x = 0$ 時,程式碼的第一行, `x--` 會將 `x` 值改變為 `0xFFFFFFFF` ,這樣一來,會使得接下來的迴圈以及條件判斷不符合預期,因此需要對此做修正。 簡單的修正方法即為加入 `if` 條件判斷,避開 $x=0$ 時做減法,然而這樣的方式並不符合 [branchless](https://sdremthix.medium.com/branchless-programming-why-your-cpu-will-thank-you-5f405d97b0c8) 。 我嘗試將程式碼做以下更動: ```diff int ceil_ilog2(uint32_t x) { uint32_t r, shift; + x = x - (x > 0); - 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 > GGG) + 1; } ``` 加入 `x = x - (x > 0);` 後使得在 $x>0$ 時才會產生布林值 1 ,達到減 1 的效果,並仍是 [branchless](https://sdremthix.medium.com/branchless-programming-why-your-cpu-will-thank-you-5f405d97b0c8) 。 ## 第四週測驗題 > [完整題目](https://hackmd.io/@sysprog/linux2024-quiz4) ### 測驗一 #### Population count 程式碼理解 [population count](https://en.wikichip.org/wiki/population_count) 簡稱 popcount 或叫 sideways sum,是計算數值的二進位表示中,有多少位元是 1。 閱讀到關鍵程式碼: ```clike= n = (v >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; ``` 不了解為何 `n = (v >> 1) & 0x77777777` 即可將數值分為四個位元一個單位做減法,並透過 `v -= n` 即可求得 $v - \left \lfloor{{\dfrac{v}{2}}}\right \rfloor$ 。因此我將數學式列出,最一開始,傳入值 v 即為 $2^{31}b_3 + 2^{30}b_2 + 2^{29}b_1 + ... + 2^0b_0$ 而將 `v >> 1` 可得 $0 + 2^{30}b_2 + 2^{29}b_1 + ... + 2^1b_1$ 。 而我們可以得知 `(v >> 1) & 0x77777777;` 結果為: ``` 0 b_31 b_30 b_29 b_28 b_27 ... b_4 b_3 b_2 b_1 & 0 1 1 1 0 1 0 1 1 1 --------------------------------------------------- 0 b_31 b_30 b_29 0 b_27 ... 0 b_3 b_2 b_1 ``` 寫成數學式為: $(0+2^{30}b_{31}+2^{29}b_{30}+2^{28}b_{29}) + (0+2^{26}b_{27}+2^{25}b_{26}+2^{24}b_{25})+ ... + (0+ 2^{2}b_3+ 2^{1}b_2+ 2^{0}b_1)$ 若持續重複執行 `n = (n >> 1) & 0x77777777; v -= n;` 可以分別得到: $(0+0+2^{29}b_{31}+2^{28}b_{30}) + (0+0+2^{25}b_{27}+2^{24}b_{26})+ ... + (0+ 0+2^{1}b_3+ 2^{0}b_2)$ $(0+0+ 0+2^{28}b_{31}) + (0+0+0+2^{24}b_{27})+ ... + (0+ 0+ 0+ 2^{0}b_3)$ 因此,若以四項為單位做相減即可得到: $(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$ 並對應到以**每 4 個位元 (nibble) 為一個單位**計算 1 的個數。 接著透過一系列位移以及 bitmask 操作可得所求的 popcount 值。 #### Hamming Distance ```c int hammingDistance(int x, int y) { return __builtin_popcount(x ^ y); } ``` Hamming Distance 是指這兩個數字對應位元的不同位置的個數。例如:數字 3 (二進制為 $0011$ )和數字 5 (二進制為 $0101$ )的漢明距離為 2,因為它們在第一位和第三位不同。 在程式碼中,使用了位元運算子 XOR( $\oplus$ ) 來找出兩個數字在哪些位置不同。對於任何兩個位元,只有當它們不同時, XOR 的結果才會是 1。因此, $x \oplus y$ 的結果會是一個數字,其二進制表示中 1 的位置就是 x 和 y 不同的位置。 接著程式使用 `__builtin_popcount` 函式來計算 $x \oplus y$ 中有多少個 1,也就是 x 和 y 有多少個不同的位元。這個函數的實作方式是直接對輸入的數字計算其二進制表示中 1 的個數,效率相當快。因此,這一行程式碼實際上就是在快速計算出 x 和 y 的漢明距離。 ```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; } ``` 以上程式碼用於計算 `nums` 陣列中所有數字之間的漢明距離總和。需特別注意的是,程式中重複考慮了兩個數字之間的距離,因此最終結果為總距離的兩倍。為了將結果除以 2,使用右移操作符將總和向右移動1位。因此,`AAAA` 應填入 1。 #### Total Hamming Distance 程式碼改進 從位元展現的樣貌,觀察 Total Hamming Distance 的規則: |n 'th bit|4|3|2|1|0| |-|-|-|-|-|-| |Input 7|0|0|1|1|**1**| |Input 5|0|0|1|0|**1**| |Input 10|0|1|0|1|**0**| |Input 17|1|0|0|0|**1**| 首先,我們觀察第 0 個位元位置。在這個位置上,數字 7、5、17 都是 1,而數字 10 是 0。進一步探究 Hamming Distance: ```graphviz digraph so { rankdir=TB; 1->0 [dir=none] } ``` ```graphviz digraph so { rankdir=TB; 1 [label="1";] 11 [label="1";] 111 [label="1";] 0 [label="0";] 1->0 [dir=none;color=red] 11->0 [dir=none;color=blue] 111->0 [dir=none;color=orange] {rank= 1 11 111} {rank= 0} } ``` 從上圖可理解為:每個 1 位元可以與 1 個 0 位元產生距離為 1 的 Hamming Distance。因此,由於有 3 個 1 位元,總 Hamming Distance 為 $1 \times 3$ 。 接下來,我們觀察第 1 個位元位置: ```graphviz digraph so { rankdir=TB; 1 [label="1";] 0 [label="0";] 00 [label="0";] 1->0 [dir=none] 1->00 [dir=none] {rank= 0} } ``` ```graphviz digraph so { rankdir=TB; 1 [label="1";] 11 [label="1";] 0 [label="0";] 00 [label="0";] 1->0 [dir=none;color="red"] 1->00 [dir=none;color="red"] {rank= 0} 11 ->0 [dir=none;color="blue"] 11 ->00 [dir=none;color="blue"] } ``` 在第 1 個位元位置上,數字 7 和 10 都是 1,而數字 5 和 17 是 0。觀察上圖可理解為:每個 1 位元可以與 2 個 0 位元產生距離為 1 的 Hamming Distance。因此,由於有 2 個 1 位元,總 Hamming Distance 為 $2 \times 2$。 總結來說,可以計算每個位置上的 1 位元數量,並將每個位置的 1 位元數量乘以 0 位元數量,以求得 Total Hamming Distance。 根據以上方法實作改進後的程式碼: ```c int totalHammingDistance_(int* nums, int numsSize) { int total = 0; for (int i = 0;i < 32;i++) { int ct = 0; for (int j = 0; j < numsSize;j++) ct += ((nums[j] >> i) & 1); total += ct * (numsSize - ct); } return total; } ``` 撰寫程式驗證其正確性: > commit [3e675c3](https://github.com/SHChang-Anderson/Lab4/commit/3e675c35c13b45d2a54f16167e8b72b8d672b941) 使用 `perf` 分析其效能差異,針對 10000 筆數字進行 Total Hamming Distance 計算。 改進前: ``` Performance counter stats for './totalHammingDistance': 1,141,370,997 cycles 0.425123945 seconds time elapsed 0.421050000 seconds user 0.003972000 seconds sys ``` 改進後: ``` Performance counter stats for './totalHammingDistance_': 4,986,285 cycles 0.003329801 seconds time elapsed 0.000000000 seconds user 0.003289000 seconds sys ``` 可以發現改進後的程式碼大幅減少了 clock cycles 數量,同時也縮減了執行時間。 ### 測驗二 #### Remainder by Summing digits 為了在不使用除法的情況下計算某數除以另一個數的餘數,使用了模同餘的概念。 當除數為 3 時,我們可以觀察到 $1 \equiv 1 (mod \ \ 3)$ 和 $2 \equiv -1 (mod \ \ 3)$。 根據 $ac \equiv bd (mod \ \ m )$ 的性質,我們可以進行以下推導: $2^k \equiv \begin{cases} 1 (mod \ \ 3), \ \ k \ \text{為偶數}\\ -1 (mod \ \ 3), \ \ k \ \text{為奇數}\\ \end{cases}$ 當我們將 $n$ 以二進位表示時,可以寫為 $b_{n-1}b_{n-2}b_{n-3}...b_1b_0$。 根據前述推導,我們得知當 $k$ 為偶數時,同餘為 1;當 $k$ 為奇數時,同餘為 -1。因此,我們可以得到以下表達式: $n=b_{n-1} \cdot 2^{n-1} + b_{n-2} \cdot 2^{n-2} + \ldots + b_3 \cdot 2^{3} + b_2 \cdot 2^{2} + b_1 \cdot 2^{1} + b_0 \equiv \ldots - b_3 + b_2 - b_1 + b_0 \ (mod \ 3)$ 接著,我們使用以下定理進行化簡: $popcount(x \land \overline{m}) - popcount(x \land m) = popcount(x \oplus m) - popcount(m)$ 因此,`n = popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA)` 可以寫為 `n = popcount(n ^ 0xAAAAAAAA) - 16`。 然而,以上計算結果的範圍會落在 -16 至 16 之間。考慮到希望餘數為正數的情況,我們需要加上一個 3 的倍數以確保餘數在同餘情況下為正數。 至於為何要加上 39 ? 參閱 《[Hacker's Delight](https://web.archive.org/web/20180517023231/http://www.hackersdelight.org/divcMore.pdf)》中的說明: >We want to apply this transformation again, until n is in the range 0 to 2, if possible. But it is best to avoid producing a negative value of n, because the sign bit would not be treated properly on the next round. A negative value can be avoided by adding a sufficiently large multiple of 3 to n. Bonzini’s code, shown in Figure 10–21, increases the constant by 39. This is larger than necessary to make n nonnegative, but it causes n to range from –3 to 2 (rather than –3 to 3) after the second round of reduction. This simplifies the code on the return statement, which is adding 3 if n is negative. The function executes in 11 instructions, counting two to load the large constant. 在文中指出,將常數增加了 39。這個值比僅使非負數所需的常數值更大,可使得在第二輪計算後,值落在 -3 到 2 的範圍內(而非 -3 到 3),也因此簡化了程式碼,只需在 n 為負數時加 3 即可。 另一種方法是直接將 0 到 32 的所有數字除以 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]; } ``` #### 井字遊戲程式碼理解 此程式實現的是一個井字遊戲的變體,該變體將遊戲目標從傳統的在3x3棋盤上實現三個連續的棋子,擴展到了在任何八條可能的直線上達到三個連續的棋子。這使得玩家的策略更加多樣化,因為一步棋可能會影響多條直線。 程式中設計了 `available_moves[]` 陣列,此陣列即為 3x3 井字遊戲上的九個位置選擇,因此我們可以看到 `play_random_game` 函式中,每做出一個選擇將其選擇從陣列中移除: ```c uint32_t move = available_moves[i]; available_moves[i] = available_moves[n_moves - 1]; ``` 實作方式就是將選擇的走法用最後一個可用的走法來替換,然後將最後一個可用的走法移到了被選擇的走法所在的位置。 在 `board |= move_masks[move];` 這行程式碼中,將選定的走法 `move` 對應的連線狀態更新到了玩家的棋盤狀態中。在這個井字遊戲變體中,棋盤狀態不是按照傳統的九宮格形式表示,而是以8條可能的連線來表示。這意味著玩家的棋盤狀態存儲了對應於這8條連線的狀態,而不僅僅是傳統的棋子擺放狀態。 對於 `board |= move_masks[move];` 這行程式碼,`move_masks[move]` 取得了選定走法 `move` 對應的連線狀態,然後將它與玩家的棋盤狀態 `board` 做位元或運算,從而將選定走法的影響更新到了玩家的棋盤狀態中。由於每個 `move_masks` 元素都包含了對應位置下棋可能影響的所有連線,因此這個操作有效地更新了玩家棋盤狀態中的所有可能連線。 ```c static const uint32_t move_masks[9] = { 0x40040040, 0x20004000, 0x10000404, 0x04020000, 0x02002022, 0x01000200, 0x00410001, 0x00201000, 0x00100110, }; ``` `move_masks` 陣列中的每個元素代表了在將棋子放置到特定位置後,對於所有可能連線狀態的影響。每個元素的二進位表示描述了在該位置放置棋子後,連線狀態發生變化的情況。 以 `0x40040040` 為例,用圖示來進行說明: 考慮 `0x40040040` 九宮格棋盤中為左上角 (0) 的選擇: ||1|2|3| |-|-|-|-| |**1**|==**0**==|1|2| |**2**|3|4|5| |**3**|6|7|8| 此位置將有三種可能連線: ||1|2|3| |-|-|-|-| |**1**|==**0**==|==1==|==2==| |**2**|3|4|5| |**3**|6|7|8| 對應到 `board` 二進位表示即: 0**1**11 0000 0000 0000 0000 0000 0000 0000 ||1|2|3| |-|-|-|-| |**1**|==**0**==|1|2| |**2**|==3==|4|5| |**3**|==6==|7|8| 對應到 `board` 二進位表示即: 0000 0000 0000 0**1**11 0000 0000 0000 0000 ||1|2|3| |-|-|-|-| |**1**|==**0**==|1|2| |**2**|3|==4==|5| |**3**|6|7|==8==| 對應到 `board` 二進位表示即: 0000 0000 0000 0000 0000 0000 0**1**11 0000 `0x40040040` 以二進位可表示為: 0**1**00 0000 0000 0**1**00 0000 0000 0**1**00 0000 玩家棋盤狀態與其做 $or$ 運算可更新棋子擺上棋盤 0 位置後對連線狀態的影響。 ```c static inline uint32_t is_win(uint32_t player_board) { return (player_board + BBBB) & 0x88888888; } ``` 勝利的條件判斷即 `player_board` 以四個位元為單位出現 0111 即判斷該玩家獲勝,可以看到程式碼將 `(player_board + BBBB)` 與 `0x88888888` 做 $and$ 運算,由此可知,當出現 0111 時需要將棋結果轉為 1000 ,而將 0111 + 1 即可達成此效果,因此 `BBBB` 應填入 `0x11111111` 。 #### Modulo 7 程式碼理解 :::info TODO ::: ### 測驗三 #### XTree `treeint.c` 為二元樹測試程式,用來測量在不同的操作下,如插入、查找和刪除,二元樹的性能表現。 * `treeint_ops` 結構,該結構包含指向各種樹操作函式的指標。 * `xt_ops` 為 `treeint_ops` 的實例 ,並將其函式指標設定為特定的實作函式。 `xtree.[ch]` 二元搜尋樹的實現,它採用了一些特定的策略來保持樹的平衡。 二元樹的結構定義包含 `xt_tree` 以及 `xt_node` ,值得注意的是在 `xt_node` 中加入了 `hint` 作為平衡參數。 程式使用不同函式實現二元樹的不同功能: * xt_create 函數創建一個空的樹。 * xt_destroy 和 __xt_destroy 函數用來遞迴釋放樹中所有節點的記憶體。 * xt_rotate_left 和 xt_rotate_right 函數用於節點的左旋和右旋操作,這是平衡樹的關鍵操作之一。 * xt_update 函數根據節點的平衡因子進行相應的旋轉操作,以維持樹的平衡。 * __xt_find 和 __xt_find2 函數實現了在樹中查找特定鍵值的節點。 * __xt_insert 和 xt_insert 函數實現了向樹中插入新節點的功能。 樹的刪除操作: 刪除操作相對複雜,尋找替代節點(右子樹的最小節點或左子樹的最大節點)並進行替換。 ```c if (xt_right(del)) { struct xt_node *least = xt_first(xt_right(del)); if (del == *root) *root = least; xt_replace_right(del, AAAA); xt_update(root, BBBB); return; } if (xt_left(del)) { struct xt_node *most = xt_last(xt_left(del)); if (del == *root) *root = most; xt_replace_left(del, CCCC); xt_update(root, DDDD); return; } ``` * 函式首先檢查被刪除的節點 `del` 是否有右子節點 `(xt_right(del))`。 * 若存在,它會找到右子樹中最小的節點`(xt_first(xt_right(del)))`,這個最小節點將會取代要被刪除的節點。 * 如果被刪除的節點是根節點`(del == *root)`,則將根節點更新為這個最小節點 `(*root = least)`。 * 接著,函式呼叫 `xt_replace_right` 找到的最小節點來替換被刪除的節點。因此 `AAAA` 應該替換為 `least`。 * 最後,對替換後的新樹結構進行 `xt_update` ,以維持樹的平衡。由於替換操作後,`least` 節點被移動到了 `del` 的位置,而 `least` 的原位置現在由它的右子節點所取代,針對原位置的節點進行更新,因此`BBBB` 應替換為 `xt_right(least)`。 同樣的,若刪除的節點有左子節點, `CCCC` 應該被替換為 `most`,表示將 `most` 節點放到 `del` 節點的位置。 如果被刪除的節點 `del` 有左子節點,會找到這個左子樹中的最大節點 `most` 來替代 `del`。而 `DDDD` 應替換為 `xt_left(most)` 。 最後,當欲刪除的節點沒有子節點時分為兩種情況進行處理: * 節點為根節點:如果要刪除的節點 `del` 正好是根節點,直接將根節點指標設置為 `NULL`,樹將變為空。 * 節點非根節點:如果 `del` 不是根節點,那麼首先找到 `del` 的親代節點 `parent` ,接著判斷 `del` 是其親代節點的左子節點還是右子節點。如果 `del` 是左子節點,則將 `parent` 的左子節點指針設置為 `NULL`。如果 `del` 是右子節點,則將 `parent` 的右子節點指針設置為 `NULL`。此舉使得節點從二元樹中斷開。 * 平衡更新:`xt_update(EEEE, FFFF)` 來更新樹的平衡。 `EEEE` 應傳入 `root`。由於刪除 `del` 後,需要重新平衡的是 `del` 的親代節點 `parent`。因此 `FFFF` 應該是 `parent` 節點。