# 2024q1 Homework4 (quiz3+4)
contributed by < [MathewSu-001](https://github.com/MathewSu-001) >
## 第一周測驗1
### 程式碼理解
若要求得平方根 `N` ,以 2的冪相加的表示式為
$$N^2 =(a_n+a_{n-1}+a_{n-2}+....+a_0)^2$$
根據 [測驗一](https://hackmd.io/@sysprog/linux2024-quiz3) 以及 [Digit-by-digit calculation](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Digit-by-digit_calculation) 所提及的定義,$P_m=\displaystyle\sum_{i=0}^{n-m}a_{n-i}$ 是迄今為止找到的近似平方根,則 $P_0=a_n+a_{n-1}+...+a_0$ 即為所求平方根 $N$。
所以我們就是要去計算出所有 $a_{n-i}$ 的總和,從 $a_n$ 開始加到 $a_0$ ,而求得 $a_m$ 只要試驗 $P_m^2 \leq N^2$ 是否成立
\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}
考量到運算成本問題,一種想法是從上一輪的差值 $X_{m+1}$ 減去 $Y_{m}$ 得到 $X_{m}$,
- $X_m = N^2 -P_m^2 = (N^2 -P_{m+1}^2)-(P_{m}^2 -P_{m+1}^2)=X_{m+1}-Y_m$
- $Y_m=P_m^2 -P_{m+1}^2=(P_{m+1}+a_m)^2-P_{m+1}^2=2P_{m+1}a_m+a_m^2$
當 $X_{n}=0$ 時,找到精確的平方根;如果不是,則 $P_m$ 給出平方根的合適近似值,其中 $X_{n}$ 是近似誤差。
進一步調整,將 $Y_m$ 拆成 $c_m, d_m$
- $c_m = 2P_{m+1}a_m = P_{m+1}2^{m+1}(\text{if } a_m=2^m)$
- $d_m = a_m^2=(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}=P_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}=a_{m-1}^2=(2^{m-1})^2=\dfrac{d_m}{4}$
所以可以知道當 $c_{-1}$ 時,得到值 $P_0$ 就是平方根 N 所求。
結合上述方法撰寫演算法來尋求 $P_0=a_n+a_{n-1}+...+a_0$ ,初始化條件
- $X_{n+1}=N^2$
- $c_n=2P_{n+1}a_n$ 且 $a_n$ 已是已知最大,所以 $P_{n+1} = 0 \to c_n=0$
- $d_n=a_n^2=(2^n)^2=2^{2n}$,所以可以設 $d_n=m$ 得到以下程式碼:
```c
int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL)
```
上述程式碼可以得到 x 最近的偶數符合 $2^{2n}=4^n$。
```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;
}
return z;
```
在以上程式碼中,$z=c_n ,\ m=d_n,\ b=Y_n,\ x=X_n$。所以依照上述的演算法,不斷的迴圈直到 $m=d_n=0$ 的時候,那所求得的 z 即為 $c_{-1}=P_0$。
### 取代 `__builtin_clz`
使用 linux 提供的 [__fls](https://github.com/torvalds/linux/blob/master/tools/include/asm-generic/bitops/__fls.h) 來進行修改。因為他回傳的 return 值是找到 x 最高位數,所以引用方法如下:
```diff
int z = 0;
-for (int m = 1UL << ( (31 - __builtin_clz(x)) & ~1UL); m; m >>= 2) {
+for (int m = 1UL << ( __fls(x) & ~1UL); m; m >>= 2) {
int b = z + m;
z >>= 1;
if (x >= b)
x -= b, z += m;
}
```
> [58557c9](https://github.com/MathewSu-001/linux_hw4/commit/58557c9b1d4df0dcb7a31bf914c3fe75af9e0170)
## 第一周測驗2
### 程式碼解析
## 第一周測驗3
### 程式碼解析
從版本一開始看起,
```c
int ilog2(int i)
{
int log = -1;
while (i) {
i >>= 1;
log++;
}
return log;
}
```
可以知道找尋 $log_2$ 的做法就是取得最高位元的次方是多少。
所以在版本二中,
```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;
}
```
首先就是先看 i 的值有沒有超過 $2^{16}$(32 位元的一半),假如有的話將 result 加上 16,並且數值右移 16 位,然後再依次砍半到 $2^8、2^4、2^1$ 看數值有無超過,這相當於找 fls。
所以版本三中,
```c
int ilog32(size_t v)
{
return (31 - __builtin_clz(v | 1));
}
```
因為 `__builtin_clz`的功用就是回傳 v 中從最高有效位開始的前導零位數,且如果 v 為 0,則結果未定義。所以加上 `| 1` 就是定義如果 v 為零時,回傳 0。
從測驗一中,我學到 fls 的用途就是找到 v 最高位數,與 `31 - __builtin_clz(v | 1)` 作用相同,因此我有進行嘗試並且證實最後結果相同。
> [a1de600](https://github.com/MathewSu-001/linux_hw4/commit/a1de6005a63199a29e3ab48d15d378846c67287f)
## 第一周測驗4
### 程式碼解析
透過[Exponentially Weighted Moving Average](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average)(EWMA; 指數加權移動平均)的數學定義:
$$S_t = \left\{
\begin{array}{l}
Y_0& t = 0 \\
\alpha Y_t + (1 - \alpha)\cdot S_{t-1}& t > 0 \\
\end{array}
\right.$$
可以知道程式碼的 `ewma_add` 的計算方式
```c
struct ewma *ewma_add(struct ewma *avg, unsigned long val)
{
avg->internal = avg->internal
? (((avg->internal << avg->weight) - avg->internal) +
(val << avg->factor)) >> avg->weight
: (val << avg->factor);
return avg;
}
```
在變數上,`val` 為新的一筆資料;`avg` 中的 `internal` 為最後取的平均值,對應到公式中的 $S_t$ ; `weight` 為權重值,對應到公式中的 $\alpha$;最後 `factor` 與 $Y_t$ 的計算有關係,為縮放因子。
當 `avg->internal` 為零時,
$$S_t = Y_0,\ \ t = 0$$
當 `avg->internal` 不為零時,
$$S_t = \alpha Y_t + (1 - \alpha)\cdot S_{t-1}=[Y_t + (\frac{1}{\alpha} -1)\cdot S_{t-1}]\cdot\alpha$$
這邊需要注意的是,$\alpha$ 介在 0 ~ 1 之間,但這邊的 `weight` 為大於零的 2 的冪,所以在作法上,原本的除法要變成乘法( bitewise operation 做右移),乘法變成除法( bitewise operation 做左移)。
## 第一周測驗5
### 程式碼解析
在一開始我先將程式做改寫,想要知道 `x--` 的用途:
```diff
int ceil_ilog2(uint32_t x)
{
uint32_t r, shift;
- 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) ;
}
```
就發現如果做出這樣的改動的話,程式碼功能就會跟測驗三的 `ilog2` 相同。因此先做 `x--` ,然後 return 值時再加一的用途,就是利用 `ilog2` 會以 2 的冪做為一個區段,原本 $2^k\sim2^{k+1}-1$ return 的值為 k ,但改動後會變成 $2^k+1\sim2^{k+1}$ return 的值為 k+1,達到 [ceil](https://man7.org/linux/man-pages/man3/ceil.3.html) 效果。
### 處理 `x = 0` 的狀況
當 `x = 0` 時,做 `x--` 會取得 `x = 0xFFFFFFFF`,而導致最後回傳的值是不正確的,所以在做法上參考了 [vax-r](https://hackmd.io/@vax-r/linux2024-homework4#%E6%B8%AC%E9%A9%97%E4%BA%94) 同學的做法,只有在 x > 0 才進行減法。
```diff
int ceil_ilog2(uint32_t x)
{
uint32_t r, shift;
- x--;
+ x -= !!x;
```
> [20f56ef](https://github.com/MathewSu-001/linux_hw4/commit/20f56ef20110a67e15f6ee8a0ec2141436f8cab0)
## 第二周測驗1
### 程式碼解析