# 2024q1 Homework4 (quiz3+4) contributed by < [gawei1206](https://github.com/gawei1206) > ## 第三周測驗題 ### 測驗 1 以 Digit-by-digit calculation 方式求得 $x$ 的平方根     $x$ = $N^2$ = $(a_n + a_{n-1} + a_{n-2} + ... + a_0)^2$, $a_m$ = $2^n$ $or$ $0$ $P_m$ = $a_n + a_{n-1} + a_{n-2} + ... + a_{m+1} + a_m$,而 $P_0$ 即為 $x$ 的平方根 $N$ 再來可以觀察到:     $P_m$ = $P_{m+1}$ + $a_m$ 因為現在的目的是要求出 $P_0$,也就是要從 $a_n$ 累加到 $a_0$,在每回合判斷是否加入 $a_m$ \begin{cases} P_m = P_{m+1} + 2^m, & \text{if $P_m^2 \leq N^2$}\\ P_m = P_{m+1}, & \text{otherwise} \end{cases} 同先前版本實作的效果 ```c if ((result + a) * (result + a) <= N) ``` 由於每輪計算 $N^2 - P_m^2$ 的成本過高,因此改為利用上輪的差值 $X_{m+1}$ 減去 $Y_m$ 可得 $X_{m}$ $X_m = N^2 - P_m^2 = X_{m+1} - Y_m$ $Y_m = P_m^2 - P_{m+1}^2 = (P_{m+1} + a_m)^2 - P_{m+1} = 2P_{m+1}a_m+a_m^2$ 也就是紀錄上一輪的 $P_{m+1}$ 來計算 $Y_m$。 再進一步將 $Y_m$ 拆成兩個變數相加,$2P_{m+1}a_m+a_m^2 = P_{m+1}2^{m+1} + (2^m)^2$ $$ c_m = P_{m+1}2^{m+1}, \quad d_m = (2^m)^2, \quad Y_m = \begin{cases} c_m+d_m \quad &if\quad a_m = 2^m \\ 0 \quad &if\quad a_m = 0 \end{cases} $$ 藉由 $c_m, d_m$ 算出 $c_{m-1}, d_{m-1}$ $c_{m-1} = P_{m}2^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}, \quad$ $d_{m-1}=2^{2m-2}=\dfrac{d_m}{4}$ 最後以上述的推導求出 $P_0$ 即從 $a_n$ 累加到 $a_0$ 的和,以下為參數的對應: z : $c_m$ m : $d_m$ b : $Y_m$ x : $X_{m+1}$ ```c 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; } ``` 其中 `if (x >= b)` 就是以 $X_{m+1}$ 和 $Y_m$ 判斷 $X_{m}$,即 $N^2 - P_m^2$ #### 延伸問題 以 `ffs` 取代 `__builtin_clz`: 先前的方法是以 `(31 - __builtin_clz(x))` 來計算 MSB,`ffs()`的回傳是目前 LSB 的位置,以每次求得的 LSB 位置累加並右移,也可以計算出 MSB。 :::info 但不知道不依賴 GNU extension 會有什麼優點 ::: ```c int tmp = x, msb = 0; while (tmp) { int s = ffs(tmp); msb += s; tmp >>= s; } msb--; for (int m = 1UL << (msb & ~1UL); m; m >>= 2) ``` 提供分支和無分支 (branchless) 的實作: 參考 `LindaTing0106` 同學的作法,將迴圈中的判別式改寫,達到無分支的實作。 ```diff= -if (x >= b) - x -= b, z += m; +int mask = (x >= b); +x -= b * mask; +z += m * mask; ``` ### 測驗 2 採用 bitwise operation 來實作除法,但是除數無法直接用 2 的冪來表示的話結果會是不精確的,因此我們的目標是計算新的除數,使 `tmp / 10` 直到小數點後第一位都是精確的。 題目的除數為 `10`,再來因為`a[i]`, `b[i]` 的範圍是 `0 ~ 9`,`carry` 則是 `0 ~ 1`,,所以 `tmp` 的範圍是 `0 ~ 19`。 ```c int tmp = (b[i] - '0') + (a[i] - '0') + carry; carry = tmp / 10; tmp = tmp - carry * 10; ``` $x = \frac{2^N}{a}$ 為這邊想要求得的新除數,可以用 2 的冪來表示且 $\frac{tmp}{x}$ 仍然會在精確度內,其中 $N$ 為非負整數 $a$ 為正整數。 證明猜想成立後,接下來只需要針對 `19` 來做討論即可。 $a.b \le \frac{n}{x} \le a.b9 \quad\Rightarrow\quad 1.9 \le \frac{19}{x} \le 1.99 \quad\Rightarrow\quad 9.55 \le x \le 10$ 接下來需要找出可以符合條件的 $N$ 和 $a$,題目以 $2^N = 128,a = 13$ 為其中一解,使得 $\frac{128}{13}\approx9.84$ 符合條件。 嘗試透過 bitwise operation 來配對數值,首先要得到 $\frac{13tmp}{8} = \frac{tmp}{8}+\frac{tmp}{2}+tmp$ ```c (tmp >> 3) + (tmp >> 1) + tmp ``` 接下來再左移 3 bit 後就可以得到 $13tmp$,但在右移時會有部分位元被捨棄,因此要另行處理。 ```c ((((tmp >> 3) + (tmp >> 1) + tmp) << 3) + d0 + d1 + d2) ``` :::info 但在這部分我有些疑惑,因為上述程式碼所計算出來的結果有時可能不是 $13tmp$,而只是接近 $13tmp$ ::: 雖然最後要再右移 7 bit 結果應該是不影響,但如果真的要將被捨棄的位元加回去,應該如下較為正確: ```c q = ((((x >> 3) + (x >> 1) + x) << 3) + (d0 << 2) + d2) >> 7; r = tmp - (((q << 2) + q) << 1); ``` 再透過 $r = f - g\cdot Q$ 算出餘數。 最後包裝成題目的程式碼如下 ```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)); } ``` 首先 `x = (in | 1) - (in >> 2)` 算出 $x = \frac{3}{4} \cdot in$,其中 `(in | 1)` 表示當 `in` 為偶數時會將它加一,再來 `q = (x >> 4) + x` 的意思是 $\frac{1}{16} \cdot \frac{3}{4} \cdot in + \frac{3}{4} \cdot in = \frac{51}{64} \cdot in \approx 0.79 \cdot in$,後續四行重複的操作是為了將結果逼近 $0.8 \cdot in$,最後右移 3 bit 得到 $\frac{1}{10} \cdot in$。 一樣透過 $r = f - g\cdot Q$ 算出餘數,`q & ~0x7` 與 `*div << 3` 是一樣意思的,再加上 `*div << 1` 求得 $10 \cdot div$,即可計算出餘數。 ### 測驗 3 實作 `ilog2` 的目標是找出二進位表示的 msb,在第二種實作方法中的想法是以二元搜尋快速找到 msb,相較於方法一逐位元的判斷來說更有效率。 運用 GNU extension `__builtin_clz` 來改寫,為了避免 `0` 的例外狀況需將輸入與 `1` 做 or。 ```c int ilog32(uint32_t v) { return (31 - __builtin_clz(v | 1)); } ``` ### 測驗 4 在對 `ewma` 結構體初始化時,會確保 `weight` 及 `factor` 的值是 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; } ``` 在後續的程式碼中需要更新 $S_t$ 的值,在等號右邊的 `internal` 代表的是 $S_{t-1}$,而 `val` 代表的是 $Y_t$,由下方的程式碼可以發現,在 `internal` 是 0 的時候程式會執行 `(val << avg->factor)`,所以這邊推測上方的 FFFF 也是做相同的操作,再來觀察 `(((avg->internal << EEEE) - avg->internal) + (val << avg->factor)) >> avg->weight` 這段程式碼與 $\alpha Y_t + (1 - \alpha)\cdot S_{t-1}$ 的關係,可以轉換成 $((S_{t-1} \cdot2^E - S_{t-1})+Y_t) \times \ 2^{-weight}$,可以發現到 $2^{-weight}$ 就是 $\alpha$ ,再將 $2^{-weight}$ 乘進去想要算出 $(1 - \alpha)\cdot S_{t-1}$,所以 EEEE 就是 `avg->weight`。 ```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; } ``` ### 測驗 5 與第三題類似,目的是要算出 MSB,但這邊要求出 $\lceil log_2(x) \rceil$,所以當 `x` 為二的冪時需要先將 `x--` ,使得最後結果正確。 ```c ... r = (x > 0xFFFF) << 4; x >>= r; shift = (x > 0xFF) << 3; x >>= shift; r |= shift; ... ``` 後續步驟與第三題找出 MSB 的部分相同,不過是以 branchless 的方式實作,以變數 `r` 紀錄位移量,因為位移量都是二的冪,所以 `r |= shift` 可以看做 `r += shift`, 最後 `return (r | shift | x > 1) + 1;`可以看做兩部分,`r | shift` 是將前一次的 `shift` 加上,而在最後一次位移後 `x` 的值可能會是 0x3 或是 0x2,所以後半部會判斷 `x` 是否大於 1 並將它加上,最後再加上 1 達到取 ceil 的效果。 #### 延伸問題 改進程式碼,使其得以處理 x = 0 的狀況,並仍是 branchless: 我們需要判斷 `x` 是否大於 0 再將其減一,在之前的作業中有看過 likely 還有 unlikely,其中這兩個巨集的實作中有用到 `!!(x)` 的用法,目的是透過兩次 NOT 來確保值一定是 0 或 1,所以可將程式中 `x--` 的部分修改如下: ```diff + x -= !!(x); - x--; ```