# 2024q1 Homework4 (quiz3+4)
contributed by < `ssheep773` >
## 第 3 週測驗題
[2024q1 第 3 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz3)
### 測驗 `1`
目的: 計算 `N` 的整數平方根
方法一 :
<!-- 首先用計算輸入 `N` 的二進位最高非 0 位元,其中 `msb` 儲存最高位元位置,而 `a` 代表 N 的最大位元的數值。
並在第二個 while 迴圈判斷逼近數 `reesult` 加上 $2^a$ 後的平方是否超過原數字`N` ,透過逐步減少 `a` 來逼近 `N` 的平方根。 -->
通過逐步逼近來計算 `N` 的平方根。它首先找到 `N` 的最大位元位置 `msb`,並將最大位元位置對應的數值儲存在 `a` $(a = 2^{msb})$ 。然後逐步減小 `a` 的值,同時檢查 $(result + a)^2$ 是否小於或等於 `N`。如果是的話,就將 `result` 加上 `a`,以逼近 `N` 的平方根。這樣的操作會持續進行,直到 `a` 的值為 0。
```c
int i_sqrt(int N)
{
int msb = 0;
int n = N;
while (n > 1) {
n >>= 1;
msb++;
}
int a = 1 << msb;
int result = 0;
while (a != 0) {
if ((result + a) * (result + a) <= N)
result += a;
a >>= 1;
}
return result;
}
```
方法二 :
利用 [Digit-by-digit calculation](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Digit-by-digit_calculation) 提及常見的直式開方法 $(x + y)^2 = x^2 + 2xy + y^2$ 的變形實作開平方根。
若要求 $x$ 的平方根,可以假設 $x = N^2$ ,那麼 $N$ 即為我們要求的平方根數值,接著,將 $N^2$ 拆成
$$
N^2=(a_{n}+a_{n-1}+a_{n-2}+...+{a_0})^2 = [(a_{n}+a_{n-1}+...+{a_1})+({a_0})]^2
$$
經過整理
$$
N^2 = (\sum_{i=0}^na_i)^2 = (\sum_{i=1}^{n} a_i)^2 + 2a_0(\sum_{i=1}^{n}a_i) + a_0^2
$$
這裡假設 $P_m = \sum_{i=m}^n a_i$ ,那麼 $P_0 = a_{n}+a_{n-1}+a_{n-2}+...+{a_0}$ 即為我們要求的 $N$, $P_0$ 可在分解為 $P_0 = (a_{n}+a_{n-1}+...+{a_1})+({a_0}) =P_1 + a_0$
可以得到一般式
$$
P_m = P_{m+1} +a_m
$$
我們的目的是要試出所有 $a_m$ ,$a_m = 2^m or 0$。從 $m = n$ 一路試到 $m = 0$ ,試驗 $P_m^2 \le N^2$ 是否成立來求得 $a_m$
$$
\begin{cases}
P_m = P_{m+1} +2^m \ \ ,if \ \ P_m^2 \le N^2\\
P_m = P_{m+1} \ \ ,if \ \ a_m=0\\
\end{cases}
$$
然而如果每次都試驗 $P_m^2 \le N^2$ 的運算成本會太高,一種想法是透過上一輪$N^2 - P_{m+1}^2$ 的差值推估 $N^2 - P_m$ 相關的證明如下:
因為前面 $P_m$ 的假設 $N$ 可以整理為
$$
N^2 = P_0^2 = a_0^2 + 2a_0P_1 + P_1^2
$$
並可推展出一般式
$$
P_m^2 = a_m^2 + 2a_mP_{m+1} + P_{m+1}^2
$$
經過移項
$$
P_m^2 -P_{m+1}^2 = 2a_mP_{m+1} + a_m^2
$$
假設 $Y_m = P_m^2 -P_{m+1}^2 = 2a_mP_{m+1} + a_m^2$
以及假設 $X_m = N^2 -P_m^2$ ,$X_m\geq0$ 即為每次判斷 $a^m$ 的條件,可以推廣 $X_{m+1} = N^2 -P_{m+1}^2$ 為前一次的差值
$$
Y_m = P_m^2 -P_{m+1}^2 = (N^2 -X_m) - (N^2 -X_{m+1}) = X_{m+1}-X_m
$$
我們就可以得到,從上一輪的差值 $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 = 2a_mP_{m+1} + a_m^2$
-->
將原本 $X_m \geq 0$ 的問題轉換為 $(X_{m+1} - Y_m) \geq 0$ 即使用到上一輪的差值來計算。
進一步調整,將 $Y_m$ 拆成 $c_m$ 和 $d_m$
- $c_m = 2P_{m+1}a_{m} = P_{m+1}2^{m+1}$
- $d_m = a_{m}^2 = (2^{m})^2$
- $Y_m =
\begin{cases}
c_m + d_m , if a_m = 2^m \\
0 , if a_m = 0 \\
\end{cases}$
以此推出下一輪
- $c_{m-1} = 2P_m a_{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 , if a_m = 2^m\\
c_m/2 , if a_m = 0 \\
\end{cases}$
- $d_{m-1} = (2^{m-1})^2 = \dfrac{d_m}{4}$
這裡說明初始與中止條件,因為是判斷 $(X_{m+1} - Y_m) \geq 0$ ,所以我們需要準備兩者的初始條件,在程式進行時,會由 $a_n$ 往下試
- $X_{n+1} = N$
- $n$ 是最大的位數,使的 $d_n^2 = a_n^2 = (2^n)^2 = 4^n \leq N^2$ 所以用迴圈逼近 $d_n$
- $c_n = 0$ 因為 $c_n = 2P_{n+1}a_{n}$ 而 $a_n$ 是最高位數所以 $P_{n+1} = 0$
因為 $d_m$ 恆大於 $0$ 所以終止條件可以設為 `d != 0` ,又 $c_{-1} = P_02^0 = P_0 = a_n + a_{n-1} +...+ a_0$ 故求出 $c_{-1}$ 則等於求出 $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 >>= 2) {
int b = z + m;
z >>= 1;
if (x >= b)
x -= b, z += m;
}
return z;
}
```
首先排除 `x` 為負數及 0 或 1 的情況
```
if (x <= 1) /* Assume x is always positive */
return x;
```
變數 `m` 對應到 $d_m$ ,其中因為 m = $d_m = a_{m}^2 = (2^{m})^2$ ,所以 m 不可能為奇數,因此要 `& ~1UL` ,同時再前一個步驟也先處理 `x = 1` 的情況
因為 $d_{m-1} = \dfrac{d_m}{4}$ 所以每次迴圈 `m` 右移 2 。
```c
for (int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL); m; m >>= 2)
```
`x` 對應到 $X_m$ 呼應初始條件 $X_{n+1} = N$,變數 `b` 對應到 $Y_m$ ,`z` 對應到 $c_m$ 同時也說明 $Y_m = c_m + d_m$
而計算 $c_{m-1} =
\begin{cases}
c_m/2 +d_m , if a_m = 2^m\\
c_m/2 , if a_m = 0 \\
\end{cases}$ ,不管 $a_m = 2^m$ 是否成立 $c_m$ 都需要除以二,這裡以 `z >>= 1` 實作
且若 則減去 $Y_m$ 得 $X_{m-1}$。
前面提到我們將原本 $X_m \geq 0$ 的問題轉換為 $(X_{m+1} - Y_m) \geq 0$ 同時也等價 $X_{m+1} \geq Y_m$ ,若成立則 $X_{m-1} = X_m - Y_m$
```c
int b = z + m;
z >>= 1;
if (x >= b)
x -= b, z += m;
```
#### ffs / fls 取代 __builtin_clz
`ffs` 是找最低為 1 的位元,所以透過迴圈的方式由低往高位元找,到最高的有效位元的位置為止,並且還需要將其減一才會與 `(31 - __builtin_clz(x)` 相等,若沒有減一,雖對於結果沒有影響,但是會多花費一輪的運算成本。
```diff
int i_sqrt(int x)
{
if (x <= 1) /* Assume x is always positive */
return x;
+ int tmp = x,
+ int msb = 0;
+ while (tmp) {
+ int i = ffs(tmp);
+ msb += i;
+ tmp >>= i;
+ }
int z = 0;
+ for (int m = 1UL << ((msb-1) & ~1UL); 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;
}
```
### 測驗 `2`
針對正整數在相鄰敘述進行 `mod 10` 和 `div 10` 操作,減少運算成本
原始的方法:
```c
static void str_add(char *b, char *a, char *res, size_t size)
{
int carry = 0;
for (int i = 0; i < size; i++) {
int tmp = (b[i] - '0') + (a[i] - '0') + carry;
carry = tmp / 10;
tmp = tmp % 10;
res[i] = tmp + '0';
}
}
```
可利用除法原理將 mod 10 和 div 10 合併。根據除法原理:
$f = g \times Q +r$
其中 $f:$ 被除數, $g:$ 除數, $Q:$ 商, $r:$ 餘數
將程式碼改寫為
```diff
carry = tmp / 10;
- tmp = tmp % 10;
+ tmp = tmp - carry * 10
```
這邊參考《Hacker's Delight》,採用 bitwise operation 來實作 10 的除法,但是因為 10 有 5 這個因數,無法完全用 2 的冪項來表示,因此結果會是不精確的。
我們的目標是,得到 tmp / 10 且直到小數點後第一位都是精確的。
為此,提出一個猜想,假設我目標的最大數是 $n$ ,而 $l$ 是小於 $n$ 的非負整數,則只要以 $n$ 算出來的除數在 $n$ 除以該除數後在精確度內,則 $l$ 除與該除數仍然會在精確度內,證明如下:
假設 $n=ab$ ( $a$ 是十位數 $b$ 是個位數),$l=cd$ ( $c$ 是十位數 $d$ 是個位數)。
首先看到 $\frac{n}{x}$,因為我們是要確保小數點後第一位是精確的,所以 $\frac{n}{x}$ 可以表示如下 :
$a.b\leq\frac{n}{x}\leq a.b9\\
\Rightarrow\frac{n}{a.b9}\leq x\leq\frac{n}{a.b}$
透過上面的轉換,我們將問題轉換為確保 $x$ 的範圍,來達到 $\frac{n}{x}$ 在精度以內,下面是對 $x$ 範圍的探討
$x$ 的右不等式 $x \leq \frac{n}{a.b} = \frac{ab}{a.b} = 10$ ,因此一定在精確度內。
$x$ 的左不等式 $\frac{n}{a.b9}\leq x$ 無法直接證明,透過代入 $l$ 來證明,$l$的不等式如下:
$c.d\leq\frac{l}{x}\leq c.d9$ ,透過將 $\frac{n}{a.b9}\leq x$ 代入可表示為
$$
c.d\leq l \times \frac{a.b9}{n} \leq c.d9 \Rightarrow c.d\leq cd \times \frac{a.b9}{ab} \leq c.d9
$$
左不等式:
$$c.d\leq cd\times\frac{a.b9}{ab} = cd\times (\frac{a.b}{ab}+ \frac{0.09}{ab}) = c.d + \frac{0.09cd}{ab}
$$
最後推得 $c.d\leq c.d + \frac{0.09cd}{ab}$ 左式顯然成立。
右不等式:
$$cd \times \frac{a.b9}{ab} \leq c.d9 \Rightarrow c.d + \frac{0.09cd}{ab}\leq c.d9 \Rightarrow c.d + \frac{0.09cd}{ab}\leq c.d + 0.09
$$
其中 $\frac{0.09cd}{ab}\leq 0.09$ ,因為 $ab > cd$ 所以右不等式也成立
於是前述猜想也成立,接下來只需要針對 $n$ 來做討論即可,而因為 `int tmp = (b[i] - '0') + (a[i] - '0') + carry` 所以 `tmp` 最大是 9+9+1=19,$n$ 也即為 19 ,下面針對 `19` 做討論即可
$$
1.9\leq \frac{19}{x}\leq 1.99\\
\Rightarrow 9.55\leq x\leq10
$$
上述不等式可得知除數介於 $9.55$ 至 $10$ 之間皆可程式中達到相同效果,即除數直到小數點後第一位都是精確的,而我們的商會比實際的還要大有正偏差。
我們透過 bitwise operation 找到介於 $9.55$ 至 $10$ 之間的除數,除數的算式必定是 $\frac{2^N}{a}$ ,其中 $N$ 是非負整數, $a$ 是正整數,而計算 $n$ 的商可以寫成 $\frac{an}{2^N}$。
因此只需要透過查看 $2^N$ 在配對適合的 $a$ 即可
這裡 $2^N = 128, a= 13 ,\frac{128}{13} \approx 9.84$ 即為可用的除數解
接著看到實際的 bitwise operation
```c
unsigned d2, d1, d0, q, r;
d0 = q & 0b1;
d1 = q & 0b11;
d2 = q & 0b111;
q = ((((tmp >> 3) + (tmp >> 1) + tmp) << 3) + d0 + d1 + d2) >> 7;
r = tmp - (((q << 2) + q) << 1);
```
`q` 是要求的商 $\frac{an}{2^N}$ ,在這裡是 $\frac{13tmp}{2^7}$,我們首先要得到 13 ,透過 `(tmp >> 3) + (tmp >> 1) + tmp) << 3` 得到 $\frac{13tmp}{8} = \frac{tmp}{8}+\frac{4tmp}{8}+tmp$ 再 `<< 3` 得到 $13tmp$,最後再除以 128 即得到我們要的商 $\frac{13tmp}{128}$
`(((q << 2) + q) << 1)` 這部分是將 `q * 10` 透過 `(q*4 + q) * 2` 達到
而因為原有的 `tmp` 在右移後,會有部分位元被捨棄,因此要另行處理。
```c
d0 = q & 0b1;
d1 = q & 0b11;
d2 = q & 0b111;
```
然而我覺得這步驟可以省略,因為後續的操作會在將 `tmp >> 7` 等於是補回來的位元馬上又被捨棄,而我思考若是在產生 13 而右移的位數大於 $2^N$ 的 $N$ 時,則需要處理,而目前分別是 3 跟 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 >> 3);
*mod = in - ((q & ~0x7) + (*div << 1));
}
```
`uint32_t x = (in | 1) - (in >> 2)` 將 $x =\frac{3}{4}in$
再透過 `uint32_t q = (x >> 4) + x` 等於是 $q = \frac{3}{4*2^4}in + \frac{3}{4}in = \frac{102}{128}in$ ,而其中 $\frac{102}{128} \approx 0.797 \approx \frac{8}{10}$
後續再用 `q = (q >> 8) + x` 對 `q` 做逼近增加精度更靠近 $\frac{8}{10}$
`*div = (q >> 3)` 是計算商,而我們前面求的 `q` 是 $\frac{8}{10}in$ ,所以需要再除以 8 也就是 `q >> 3` 。
`*mod = in - ((q & ~0x7) + (*div << 1))` 是計算餘數,其中的 `q & ~0x7` 等於是 `*div << 3` ,那麼就程式碼可以轉換成 `*mod = in - (*(div << 3) + (*div << 1))` 後半部的 `*(div << 3) + (*div << 1)` 等於是 $商\times8 + 商\times2 = 商 \times (8+2) = 商 \times10$ ,根據餘數定理 $餘數 = 被除數(in) - 商(div) \times10 除數(10)$
### 測驗 `3`
ilog2 可計算以 2 為底的對數,且其輸入和輸出皆為整數,這裡注意到因為輸出為整數,所以輸入的最高二進位有效位元位置即為結果。
方法一 :
由最低位元往高位元處找最高二進位有效位元位置
```c
int log = -1;
while (i) {
i >>= 1;
log++;
}
```
方法二 :
透過二元搜尋的方法尋找
```c
static size_t ilog2(size_t i)
{
size_t result = 0;
while (i >= 2^16) {
result += 16;
i >>= 16;
}
while (i >= 2^8) {
result += 8;
i >>= 8;
}
while (i >= 2^4) {
result += 4;
i >>= 4;
}
while (i >= 2) {
result += 1;
i >>= 1;
}
return result;
}
```
方法三:
利用 GNU extension `__builtin_clz` ,找出最高有效位元前面 0 的數量,因此 `31 - __builtin_clz(v | 1)` 即為最高有效位元的位置,而因為 `__builtin_clz` 輸入若是 0 則無定義,所以使用 `v | 1` 確保輸入不為 0 。
```c
int ilog32(uint32_t v)
{
return (31 - __builtin_clz(v | 1));
}
```
在 [linux/log2.h](https://github.com/torvalds/linux/blob/39cd87c4eb2b893354f3b850f916353f2658ae6f/include/linux/log2.h#L30) 中使用 `fls` 找出最高有效位元並且說明輸入 0 是未定義的。
```c
/*
* non-constant log of base 2 calculators
* - the arch may override these in asm/bitops.h if they can be implemented
* more efficiently than using fls() and fls64()
* - the arch is not required to handle n==0 if implementing the fallback
*/
#ifndef CONFIG_ARCH_HAS_ILOG2_U32
static __always_inline __attribute__((const))
int __ilog2_u32(u32 n)
{
return fls(n) - 1;
}
#endif
#ifndef CONFIG_ARCH_HAS_ILOG2_U64
static __always_inline __attribute__((const))
int __ilog2_u64(u64 n)
{
return fls64(n) - 1;
}
#endif
```
### 測驗 `4`
[Exponentially Weighted Moving Average](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average) (EWMA; 指數加權移動平均) 是種取平均的統計手法,並且使經過時間越久的歷史資料的權重也會越低,以下為 EWMA 的數學定義:
$$
S_t =
\begin{cases}
\ Y_0 \qquad\qquad\qquad\qquad ,t=0\\
\ \alpha Y_t +(1-\alpha) \cdot S_{t-1}
\ \ ,t>0\\
\end{cases}
$$
$\alpha$ 表示歷史資料加權降低的程度,介在 0 ~ 1 之間,越高的 $\alpha$ 會使歷史資料減少的越快
$Y_t$ 表示在時間 $t$ 時的資料點
$S_t$ 表示在時間 $t$ 時計算出的 EWMA
首先看到 ewma 結構,`internal` 儲存我們的 EWMA 值也就是 $S_t$ ,值得注意的是它的型態是 `unsigned long` 是無號整數,所以我們可以知道這裡是使用定點數,並在 `ewma_init()` 註解處中有說明 `factor` 是 Scaling factor , 其中註解處有提到`internal` 可記錄最大值的計算公式為 `ULONG_MAX / (factor * weight)` ,其中 `factor` 是因為做 scaling 提高數值精度變相會犧牲可容納的對大值,而 `weight` 的部分不太理解,只能夠推測因為程式中有做 `avg->internal << avg->weight` 與 `avg->internal >> avg->weight` 與 `factor` 一樣。
並在 `ewma_read` 可以看到輸出時還原 scaling ,可見 `avg->internal >> avg->factor`
```c
struct ewma {
unsigned long internal;
unsigned long factor;
unsigned long weight;
};
```
接著看到 `ewma_init` 函式,目的是初始化 `ewma`,其中使用 `is_power_of_2` 來確保 `weight` 跟 `factor` 是 2 的冪,因為 `internal` 是定點數若是與 2 的冪做乘除法,可以用位元運算。
`is_power_of_2` 是運用當數值 `n` 是 2 的冪時,`n` 跟 `n-1` 在二進位時,不會有相同的位數會是錯開的,利用這個特點檢測
```c
static inline int is_power_of_2(unsigned long n)
{
return (n != 0 && ((n & (n - 1)) == 0));
}
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 =
\begin{cases}
\ Y_0 \qquad\qquad\qquad\qquad ,t=0\\
\ \alpha Y_t +(1-\alpha) \cdot S_{t-1}
\ \ ,t>0\\
\end{cases}
$$
最後看到 `ewma_add` ,這裡可想而知會用到前面提到的 $S_t$ ,先說明 `avg->internal` 等於 0 的情況,將 `avg->internal` 設為輸入的 `val` 對應到 $Y_0$ ,但別忘了 scaling 所以還要 `val << avg->factor`
在來看到大於 0 的情況,第一次看到 `(((avg->internal << avg->weight) - avg->internal) +(val << avg->factor)) >> avg->weight` 時感覺跟 $\alpha Y_t +(1-\alpha) \cdot S_{t-1}$ 公式有關,但是仔細看時發現正負號會有錯,這邊再看過 [ShchangAnderson](https://hackmd.io/@ShchangAnderson/linux2024-homework4#%E6%B8%AC%E9%A9%97%E5%9B%9B) 同學的筆記後才了解,其實 $\alpha = \frac{1}{2^{avg->weight}}$,所以程式是先將
$$
\alpha Y_t +(1-\alpha) \cdot S_{t-1}
= [ \alpha Y_t +(1-\alpha) \cdot S_{t-1} ] \times 2^{avg->weight} \times \frac{1}{2^{avg->weight}}
$$
將 $2^{avg->weight}$ 乘進去原始公式,再簡單地移項方便對證程式碼,可以得到
$$
[ 2^{avg->weight}\cdot S_{t-1} - S_{t-1} + Y_t] \times \frac{1}{2^{avg->weight}}
$$
其中先乘以 $2^{avg->weight}$ 再乘 $\frac{1}{2^{avg->weight}}$ 是為了提高精度,程式即是使用上述的公式做計算,並且在每次輸入 `val` 都會 scaling
```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;
}
```
### 測驗 `5`
這個程式碼實做一個函式,用來計算輸入的 32 位元無號整數 $x$ 的 2 次方指數值向上進位的結果。也就是說,對於傳入的參數 $x$ ,回傳最小的整數 $n$,滿足 $x\leq2^n$ 。
```c
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;
```
在函式的開始將 x 減 1,這樣可以確保當 x 是 2 的冪時的正確性,因為 $x\leq2^n$,當等於時不用進位。
接著看到
```c
shift = (x > 0xFF) << 3;
x >>= shift;
r |= shift;
```
這裡其實跟測驗 `3` 是相同的概念,將測驗三的變數調整方便比較,其中 `0xFFFF = 2^16` ,並且將數字都以 2 的冪表示。
此方法與測驗三的差異在於紀錄 `r` 的方式是透過 bitwise 操作而非加法運算, `r | shift` 等效於 `r + shift` 。
```c
/* ilog2 */
while (x >= 2^8) {
r += 2^3;
x >>= 2^3;
}
```
最後 `return (r | shift | x > 1) + 1` 的部分我們分開看,其中 `r | shift` 是將前一部分的 `r |= shift` 合併進來, `x > 1` 是為了要處理前面 `shift = (x > 0x3) << 1` 會忽略 `x= 0x2` 的情況,最後再加上一作為取上界 (ceil) 。
#### 改進程式碼,使其得以處理 x = 0 的狀況,並仍是 branchless
當 `x=0` 時會因為減一變成 `0xFFFFFFFF` ,而這不是我們想要的結果。
我想到在 `likely / unlikely` 中有用到 `!!(x)` 將整數輸入結果控制在 `0` 跟 `1` , 那麼就可以將 `x--` 變為 `x = x - !!x` ,當 `x > 0` 時減一,而當 `x = 0` 時則不變。
```diff
int ceil_ilog2(uint32_t x)
{
uint32_t r, shift;
- x--;
+ x = x - !!x
r = (x > 0xFFFF) << 4;
x >>= r;
```
## 第 4 週測驗題
[2024q1 第 4 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz4)
### 測驗 `1`
[population count](https://en.wikichip.org/wiki/population_count) 簡稱 popcount 或叫 sideways sum,計算數值的二進位表示中 1 的個數。
```c
unsigned popcount_naive(unsigned v)
{
unsigned n = 0;
while (v)
v &= (v - 1), n = -(~n);
return n;
}
```
`v &= (v - 1)` : reset LSB 是將 最低有效位 (LSB) 設為 0 。這讓我想到這跟前面提到的 `is_power_of_2` 觀念類似,都是判斷 `(n & (n - 1)) == 0` 只是 `is_power_of_2` 只判斷一次,而 `popcount_naive` 是重複判斷來計算 1 的個數。
`n = -(~n)` 等同 `n++`,因為在二補數系統中 $-n = \sim n+1$ ,經過移項 $-(\sim n) = n +1$
#### 針對 [LeetCode 477. Total Hamming Distance](https://leetcode.com/problems/total-hamming-distance/description/) 撰寫出更高效的程式碼。
透過觀察 Total Hamming Distance 的規則
可以得到一般式:
觀察每個 Number 的第 i 個 bit ,若有 `cnt` 個 1 ,其餘皆為 0 ,則 Hamming Distance : `(cnt) * (numsize - cnt)`
我們要走訪所有的 bit ,在每次走訪時計算第 i 個 bit 為 1 的 number 個數(`cnt`),其中使用 `(nums[j] >> i) & 1` 得知目前的number 第 i 個 bit 是否為 1 ,最後透過公式 `(cnt) * (numsize - cnt)` 計算得到第 i 個 bit 的 Hamming Distance ,最後加總所有位元的 Hamming Distance 即為 nums 中所有整數的 Hamming Distance 。
```c
int totalHammingDistance(int* nums, int numsSize) {
int total = 0;;
for (int i = 0;i < 32;i++){
int cnt = 0;
for (int j = 0; j < numsSize;j++)
cnt += ((nums[j] >> i) & 1);
total += cnt *(numsSize - count);
}
return total;
}
```
### 測驗 `2`
Remainder by Summing digits
如何不使用任何除法就算出某數除以另一個數的餘數呢?若除數符合 $2^k \pm 1$ ,則可運用以下手法來達成這個目的。
以除數為 3 為例,我們可以觀察到 $1 \equiv 1 (mod \ \ 3)$ 且 $2 \equiv -1 (mod \ \ 3)$。 將後者不斷的乘上前者可得
$$2^k \equiv \begin{cases}
1 (mod \ \ 3), \ \ k \ even\\
-1 (mod \ \ 3), \ \ k \ odd \\
\end{cases}$$
因此若 n 的二進位表示為 $b_{n-1}b_{n-2}b_{n-3}...b_1b_0$ ,則 $n = \sum_{i=0}^{n-1} b_i \cdot 2^i$ ,由以上的推論可得 $n \equiv \sum_{i=0}^{n-1} b_i \cdot (-1)^i \ \ (mod \ \ 3)$ 將奇偶數分開 $n \equiv \sum_{i=even} b_i - \sum_{i=odd} b_i \ \ (mod \ \ 3)$
可以利用 population count 來計算位元和,因此可以將上式表示為 `n = popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA)` ,其中 `& 0x55555555` 是留下 n 的奇數位元, `& 0xAAAAAAAA` 是留下 n 的偶數位元,並利用以下定理可以進一步化簡。
$$
popcount(x \& \overline{m}) - popcount(x \& m) = popcount(x \oplus m) - popcount(m)
$$
於是可以寫為 `n = popcount(n ^ 0xAAAAAAAA) - 16` ,計算結果的範圍會落在 -16 至 16 之間 ,但為了避免 n 變成負數,我們要將它加上一個足夠大的 3 的倍數,文中的例子是加上 39 讓數值介於 23 到 55 ,我們再做一次與上面相同的操作,只是這次的數值較小,相差 32 所以跟 `0x2A = 0b101010` 做運算而 `popcount(0x2A) = 3` 所以是減三,最後區間會介於 `-3 <= n <= 2` ,還需要對負數加 3 ,所以最後 `(n >> 31) & 3` 判斷最高位元判斷正負來決定是否加 3 。
```c
int mod3(unsigned n)
{
n = popcount(n ^ 0xAAAAAAAA) + 23; // Now 23 <= n <= 55.
n = popcount(n ^ 0x2A) - 3; // Now -3 <= n <= 2
return n + ((n >> 31) & 3);
}
```
另一種變形是利用 lookup table ,其中 n 介於 0 到 32 透過查表直接得知。
```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];
}
```
然而在 [HackerDelight](https://web.archive.org/web/20180517023231/http://www.hackersdelight.org/divcMore.pdf) 中提到不使用指令的方法
```c
int remu3(unsigned n) {
n = (n >> 16) + (n & 0xFFFF); // Max 0x1FFFE.
n = (n >> 8) + (n & 0x00FF); // Max 0x2FD.
n = (n >> 4) + (n & 0x000F); // Max 0x3D.
n = (n >> 2) + (n & 0x0003); // Max 0x11.
n = (n >> 2) + (n & 0x0003); // Max 0x6.
return (0x0924 >> (n << 1)) & 3;
}
```
這段程式碼主要是利用 $4^k \equiv 1 (mod \ \ 3)$ 簡化求 $n \ (mod \ \ 3)$
首先假設 n 前的 16 位元是 $c$ ,後 16 位元是 $d$ ,那麼我們就可以將 n 的值寫成 $n=c\cdot2^{16}+d$ ,再透過 $4^k \equiv 1 (mod \ \ 3)$ 可以將 $n (mod \ \ 3)$表示為
$$
n (mod \ \ 3) =
c\cdot2^{16}+d(mod \ \ 3) =
c+d(mod \ \ 3)
$$
上述的數學式可轉換成 `n = (n >> 16) + (n & 0xFFFF)` ,以此類推重複 5 次將 n 縮小至最大 `0x6`
最後一行類似查表,將餘數值存在 `0x0924` ,透過位元操作取值,其中因為 3 的餘數是 {0,1,2} 可以用 2 個位元即可表示,所以 `0x0924` 是每隔 2 位元存一個餘數值,並透過 `(n << 1)` 與 `& 3` 搭配取得。
```
0x0924 = 00 00 10 01 00 10 01 00
(n << 1) = 12 10 8 6 4 2 0
n = 6 5 4 3 2 1 0
```
`tictactoe.c` 中使用到的 mod7 方法說明
上述提到利用 $4^k \equiv 2^{2k}\equiv 1 (mod \ \ 3)$ 簡化求 $n \ (mod \ \ 3)$ ,同理利用 $8^k \equiv 2^{3k} \equiv 1 (mod \ \ 7)$ 簡化求 $n \ (mod \ \ 7)$ ,這裡 `(x >> 15)` 使用 15 是因為 $2^{3k}$ ,所以是每隔 3 位元有同餘的結果,而我們要將原 32 位元的數縮小,找 16 附近的 3 的倍數 15 。
在這個方法中不像 [HackerDelight](https://web.archive.org/web/20180517023231/http://www.hackersdelight.org/divcMore.pdf) 提到的例子,繼續縮小輸入的值後查表求餘數,而是透過 $n (mod \ 7) \equiv \lfloor\frac{8}{7}n\rfloor (mod \ 8)$ 求 7 的餘數,先將數字乘以 $\lceil\frac{2^{32}}{7}\rceil$ 也就是 `0x2492492` 再右移 29 得到剩餘的三位數即為解。
:::info
不太明白為何 $n (mod \ 7) \equiv \lfloor\frac{8}{7}n\rfloor (mod \ 8)$
:::
```c
static inline int mod7(uint32_t x)
{
x = (x >> 15) + (x & UINT32_C(0x7FFF));
/* Take reminder as (mod 8) by mul/shift. Since the multiplier
* was calculated using ceil() instead of floor(), it skips the
* value '7' properly.
* M <- ceil(ldexp(8/7, 29))
*/
return (int) ((x * UINT32_C(0x24924925)) >> 29);
}
```
`tictactoe.c` 是一個井字遊戲,他的特別之處在於儲存棋子的方式,以往可能是用一個矩陣儲存井字中每個位置的棋子狀態,這個程式是用棋子可以連成的線來儲存,這個方法的優點在於可以更快速的判斷贏家,也可以更方便取得連線的資訊,
### 測驗 `3`