# 2024q1 Homework4 (quiz3+4)
contributed by < `yeh-sudo` >
## 第三周測驗題
### 測驗 `1`
#### 解釋程式碼運作原理
假設要求的平方根為 $x$ ,並且將 $x^2$ 表示為以下形式:
$$x^2=(000b_0b_1b_2...b_{n-1}b_n)^2$$
其中 $000b_0b_1b_2...b_{n-1}b_n$ 為 bit pattern , $b_0$ 為最高位的 1 。所以 $x^2$ 可以寫成:
$$x^2=(b_02^n+b_12^{n-1}+b_22^{n-2}+...+b_{n-1}2^1+b_n2^0)^2$$
設 $a_0=b_02^n,a_1=b_12^{n-1},a_2=b_22^{n-2}...a_{n-1}=b_{n-1}2^1,a_n=b_n2^0$, $x^2$ 可以表示成:
$$x^2=(a_0+a_1+a_2+...+a_{n-1}+a_n)^2$$
根據 [Digit-by-digit calculation](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Digit-by-digit_calculation) :
\begin{align*}
x^2&=(a_n+a_{n-1}+a_{n-2}+...+a_1+a_0)^2 \\
&=a_n^2+2a_na_{n-1}+a_{n-1}^2+2(a_n+a_{n-1})a_{n-2}+a_{n-2}^2+...+a_1^2+2(\sum_{i=1}^{n} a_i)a_0+a_0^2 \\
&=a_n^2+[2a_n+a_{n-1}]a_{n-1}+[2(a_n+a_{n-1})+a_{n-2}]a_{n-2}+...+[2(\sum_{i=1}^{n} a_i)+a_0]a_0
\end{align*}
設 $P_m=\sum_{i=m}^{n} a_i$ ,則 $x^2$ 可以表示為:
$$x^2=a_n^2+[2P_n+a_{n-1}]a_{n-1}+[2P_{n-1}+a_{n-2}]a_{n-2}+...+[2P_1+a_0]a_0$$
而 $P_0=a_n+a_{n-1}+a_{n-2}+...+a_1+a_0$ 即為要求出的平方根。所以,從 $m=n$ 一路試到 $m=0$ ,測試 $P_m^2\le x^2$ 是否成立,分為兩種情況:
\begin{cases}
P_m=P_{m+1}+a_m & \quad \text{if } P_m^2\le x^2\\
P_m=P_{m+1} & \quad \text{otherwise}
\end{cases}
在檢驗時,如果每次都要計算 $P_m$ ,會需要很高的成本,所以可以利用重複利用每一次計算的差值 $X$ ,可以得出:
\begin{align*}
X_n&=x^2-P_n^2 \\
&=x^2-a_n^2
\end{align*}
\begin{align*}
X_{n-1}&=x^2-P_{n-1}^2 \\
&=x^2-(a_n+a_{n-1})^2 \\
&=x^2-a_n^2-a_{n-1}^2-2a_na_{n-1} \\
&=(x^2-a_n^2)-a_{n-1}^2-2a_na_{n-1} \\
&=X_n-a_{n-1}^2-2a_na_{n-1} \\
&=X_n-[2a_n+a_{n-1}]a_{n-1} \\
&=X_n-[2P_n+a_{n-1}]a_{n-1}
\end{align*}
\begin{align*}
X_{n-2}&=x^2-P_{n-2}^2 \\
&=x^2-(a_n+a_{n-1}+a_{n-2})^2 \\
&=x^2-a_n^2-a_{n-1}^2-a_{n-2}^2-2a_na_{n-1}-2a_na_{n-2}-2a_{n-1}a_{n-2} \\
&=X_n-[2P_n+a_{n-1}]a_{n-1}-2a_na_{n-2}-2a_{n-1}a_{n-2}-a_{n-2}^2 \\
&=X_{n-1}-[2(a_n+a_{n-1})+a_{n-2}]a_{n-2} \\
&=X_{n-1}-[2P_{n-1}+a_{n-2}]a_{n-2}
\end{align*}
\begin{align*}
...
\end{align*}
\begin{align*}
X_{m}&=X_{m+1}-[2P_{m+1}+a_m]a_m
\end{align*}
設 $Y_m=[2P_{m+1}+a_m]a_m$ ,則可以寫成以下遞迴關係:
$$X_{m}=X_{m+1}-Y_m$$
將 $Y_m$ 拆成 $c_m$ 與 $d_m$ 兩部份,
\begin{align*}
Y_m&=[2P_{m+1}+a_m]a_m \\
&=2P_{m+1}a_m+a_m^2 \\
&=2^{m+1}P_{m+1}+{(2^m)}^2 \\
&=c_m+d_m
\end{align*}
\begin{cases}
Y_m=c_m+d_m & \quad \text{if } a_m=2^m \\
Y_m=0 & \quad \text{otherwise}
\end{cases}
\begin{cases}
c_m=2^{m+1}P_{m+1} \\
d_m={(2^m)}^2
\end{cases}
可以從 $c_m$ 與 $d_m$ 推出下一輪的 $c_{m-1}$ 與 $d_{m-1}$ ,再用 $c_{m-1}$ 與 $d_{m-1}$ 推出 $c_{m-2}$ 與 $d_{m-2}$ ,直到推出 $c_{-1}=P_0$ 即為所求的平方根 $x$ ,而首項 $c_n=0, d_n={(2^n)}^2$ 。
\begin{align*}
c_{m-1}&=2^mP_m \\
d_{m-1}&={(2^{m-1})}^2={(2^m)}^22^{-2}=d_m/4
\end{align*}
此處的 $P_m=P_{m+1}+a_m$ 成立的條件為 $P_m^2\le x^2$ ,若不成立,則 $P_m=P_{m+1}$ ,所以當 $P_m^2\le x^2$ 時, $a_m=2^m$ ,反之則 $a_m=0$ 。所以最後可以將 $c_m$寫成以下關係:
\begin{gather*}
c_{m-1}=
\begin{cases}
c_m/2+d_m & \quad \text{if } a_m=2^m \\
c_m/2 & \quad \text{if } a_m=0
\end{cases}
\end{gather*}
在程式碼中, $c_m$ 對應到變數 `z` , $d_m$ 對應到變數 `m` , 差值 $X$ 對應到變數 `x` 。由於是紀錄差值,所以每當差值大於等於 $c_m+d_m$ ,也就是 `b = z + m` 時,就將差值扣掉 `b` ,並且,因為差值為正,代表 $P_m^2\le x^2$ ,也就是 $a_m=2^m$ ,所以 $c_{m-1}=c_m/2+d_m$ ,對應到的程式碼操作為 `z += m` ,持續進行迴圈直到 `m` 為 0 時,結束迴圈。
```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;
}
```
#### 以 ffs / fls 取代 __builtin_clz
程式碼 `(31 - __builtin_clz(x))` 的作用是找到最大的 bit 所在的位置,而使用第二週測驗題提供的 `__ffs` ,可以實作出找出對大 bit 的 `fls` 函式,實作方法是當 `x` 不為 0 時,持續尋找 `ffs` ,找到時就使用測驗題提供的 `__clear_bit` 將找到的 bit 清為 0 ,當 `x` 為 0 時,代表最後找到的 `ffs` 為最大的 bit 。
```c
static inline unsigned long fls(unsigned long x)
{
int result = 0;
while (x) {
unsigned int bit = __ffs(x);
if (bit > result)
result = bit;
__clear_bit(bit, &x);
}
return result;
}
```
另外還有一個方法可以找出 `fls` ,將 `x` 的逐位元反轉,再用 31 減掉 `ffs` 即可得到 `fls` 。
```c
static inline unsigned long fls(unsigned long x)
{
unsigned long result = x;
result = ((result & 0xffff0000) >> 16) | ((result & 0x0000ffff) << 16);
result = ((result & 0xff00ff00) >> 8) | ((result & 0x00ff00ff) << 8);
result = ((result & 0xf0f0f0f0) >> 4) | ((result & 0x0f0f0f0f) << 4);
result = ((result & 0xcccccccc) >> 2) | ((result & 0x33333333) << 2);
result = ((result & 0xaaaaaaaa) >> 1) | ((result & 0x55555555) << 1);
return 31 - __ffs(result);
}
```
#### 提供分支和無分支 (branchless) 的實作
在教材〈[解讀計算機編碼](https://hackmd.io/@sysprog/binary-representation#%E4%B8%8D%E9%9C%80%E8%A6%81%E5%88%86%E6%94%AF%E7%9A%84%E8%A8%AD%E8%A8%88)〉中,有描述不需要分支的設計,當 `INT32_MIN <= (b - a) <= INT32_MAX` 條件成立時,可以使用 bitwise 操作改寫分支。
- [ ] 改寫 `i_sqrt`
```diff
- if (x >= b)
- x -= b, z += m;
+ int tmp = x;
+ x -= (~(tmp - b) >> 31) & b;
+ z += (~(tmp - b) >> 31) & m;
```
- [ ] 改寫 `__ffs`
```diff
#if BITS_PER_LONG == 64
- if ((word & 0xffffffff) == 0) {
- num += 32;
- word >>= 32;
- }
+ num += !(word & 0xffffffff) & 32;
+ word >>= 32;
#endif
- if ((word & 0xffff) == 0) {
- num += 16;
- word >>= 16;
- }
- if ((word & 0xff) == 0) {
- num += 8;
- word >>= 8;
- }
- if ((word & 0xf) == 0) {
- num += 4;
- word >>= 4;
- }
- if ((word & 0x3) == 0) {
- num += 2;
- word >>= 2;
- }
- if ((word & 0x1) == 0)
- num += 1;
+ num += !(word & 0xffff) & 16;
+ word >>= 16;
+ num += !(word & 0xff) & 8;
+ word >>= 8;
+ num += !(word & 0xf) & 4;
+ word >>= 4;
+ num += !(word & 0x3) & 2;
+ word >>= 2;
+ num += !(word & 0x1) & 1;
return num;
```
#### Linux 核心原始程式碼
```shell
~/linux/block master ❯ grep sqrt *.c
blk-wbt.c: * window is then shrunk to 100 / sqrt(scaling step + 1).
blk-wbt.c: int_sqrt((rqd->scale_step + 1) << 8));
```
在 Linux 核心中, block 這個目錄下的 `blk-wbt.c` 有使用到整數平方根的操作,使用的函式為 `rwb_arm_timer` 。
- [ ] `blk-wbt.c`
```c=397
static void rwb_arm_timer(struct rq_wb *rwb)
{
struct rq_depth *rqd = &rwb->rq_depth;
if (rqd->scale_step > 0) {
/*
* We should speed this up, using some variant of a fast
* integer inverse square root calculation. Since we only do
* this for every window expiration, it's not a huge deal,
* though.
*/
rwb->cur_win_nsec = div_u64(rwb->win_nsec << 4,
int_sqrt((rqd->scale_step + 1) << 8));
} else {
```
閱讀實作 `writeback throttling` 這個機制的作者 Jens Axboe 的 [commit messages](https://git.kernel.dk/cgit/linux-block/commit/?h=wb-buf-throttle) , throttling 這個機制最主要是讓背景執行的 buffered writeback 不會影響到其他正在執行的程式,作者發現在執行大量的 buffered writeback 時,他想要開啟 Chrome 會打不開。
> But for as long as I can remember, heavy buffered writers have not behaved like that. For instance, if I do something like this:
>
> $ dd if=/dev/zero of=foo bs=1M count=10k
>
> on my laptop, and then try and start chrome, it basically won't start
before the buffered writeback is done. Or, for server oriented
workloads, where installation of a big RPM (or similar) adversely
impacts database reads or sync writes. When that happens, I get people
yelling at me.
這個機制有點像是 [CoDel](https://en.wikipedia.org/wiki/CoDel) networking scheduling algorithm , `blk-wb` 監控一段時間中的最小延遲,如果這段時間中,有最小延遲超過設定的目標,就增加 `scaling step` 與壓縮佇列的深度,接著將監控的時間縮小 `100 / sqrt(scaling step + 1)` 。其中的整數平方根運算對應的程式碼在 `/lib/math/int_sqrt.c` ,其實作的演算法也是使用 digit-by-digit calculation 。
- [ ] `int_sqrt`
```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;
}
```
#### 閱讀〈Analysis of the lookup table size for square-rooting〉
#### 閱讀〈Timing Square Root〉