# 2024q1 Homework4 (quiz3+4)
contributed by < [Petakuo](https://github.com/Petakuo) >
## 第三週測驗題
## [測驗1](https://hackmd.io/@sysprog/linux2024-quiz3#%E6%B8%AC%E9%A9%97-1)
### 版本一
首先,利用 `log2()` 函式找到最高位的位元,接著由該位元開始測試,逐漸逼近答案,而逼近的方法為利用 `if` 函示判斷 `(result + a)` 的平方是否大於被要求開平方的值 `N` ,如果有,則繼續往低位進行逼近,如果沒有,則將原本的答案加上 `a` 進行更新,並同樣往低位進行逼近,如此做到 `a` 為 0 時即可得到 `N` 的平方根。
### 版本二
```
int msb = 0;
int n = N;
while (n > 1) {
n >>= 1;
msb++;
}
```
與版本一大致相同,唯改變了取最高位元的做法,將 `log2()` 用以上程式碼呈現,首先判斷 input 的 `n` 是否大於 1 ,有則將其右移一個位元,並將所求 +1 ,如此進行直到 `n` 只剩下 1 即代表該位元為最高位元,而所求也隨著每一輪迴圈的 +1 而得出。
### 版本三
利用 [Digit-by-digit calculation](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Digit-by-digit_calculation) 的方法計算,先將要求平方根的 $N^2$ 拆解成 :
$N^2 = (a_n+a_{n-1}+a_{n-2}+...+a_1+a_0)^2$
並且先觀察較簡單的例子 :
$(a_n+a_{n-1})^2 = a_n^2+2a_na_{n-1}+a_{n-1}^2 = a_n^2+[2a_n+a_{n-1}]a_{n-1}$
$(a_n+a_{n-1}+a_{n-2})^2 = a_n^2+[2a_n+a_{n-1}]a_{n-1}+[2(a_n+a_{n-1})+a_{n-2}]a_{n-2}$
依此模式進行,我們最終就可以得到 :
$N^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}^na_i)+a_0]a_0$
此時再令 $P_m = a_n+a_{n-1}+...+a_m$ ,則上述式子可以進一步被改寫為 :
$N^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_0$ 即為所求 $N$
為求 $P_0$ ,我們可以由 $P_m = P_{m-1}+a_m$ 進行迭代,每一輪皆去判斷 $P_m^2 \leq N^2$ 是否成立,若成立則將該位元設為 1 ,反之則設為 0 。
但此時由於去計算每一輪 $P_m^2$ 的成本太高,因此我們透過利用上一輪的資訊來做計算,首先定義
$X_m = N^2-P_m^2 = X_{m+1}-Y_m$
並且
$Y_m = P_m^2-P_{m+1}^2 = 2P_{m+1}a_m+a_m^2$
此時再將 $Y_m$ 改寫為 $c_m+d_m$ ,其中 $c_m = P_{m+1}a_{m+1} , d_m = a_m^2$ ,因此下一位元即可被更新為
$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} = d_m/4$
到這裡可以發現,將 m = 0 代入,可得 $c_{-1} = P_02^0 = P_0 = a_n+a_{n-1}+...+a_0 = 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;
}
```
在此程式碼中,利用 `__builtin_clz(x)` 找出 msb ,且 b 為上述推導過程中的 $Y_m$ ,因此 z 等同於 $c_m$ 而 m 等同於 $d_m$ 。由於每一輪 $d_m$ 都會除以 4 ,因此 `AAAA` 為 2 ,同樣道理, $c_m$ 每一輪會除以 2 ,所以 `BBBB` 為 1 。
延伸問題 : 利用 `ffs` 取代 `__builtin_clz` ,使程式不依賴 GNU extension
```c
static inline unsigned long __ffs(unsigned long word)
{
int num = 0;
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;
return num;
}
int i_sqrt(int x)
{
if (x <= 1) /* Assume x is always positive */
return x;
int z = 0;
for (int m = 1UL << ((31 - __ffs(x)) & ~1UL); m; m >>= 2) {
int b = z + m;
z >>= 1;
if (x >= b)
x -= b, z += m;
}
return z;
}
```
## [測驗2](https://hackmd.io/@sysprog/linux2024-quiz3#%E6%B8%AC%E9%A9%97-2)
```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);
```
此段程式碼嘗試利用 bitwise 操作來取代正整數的除法和 mod 運算,這裡以 10 為例進行解說。
從數學的角度切入,除以 10 等同於乘以 $\cfrac{1}{10}$ ,因此我們必須找到在二進制中 $\cfrac{1}{10}$ 的表示法,而因為不是所有數都能夠利用有限個 bits 來表達,所以只能取近似值來做運算,在此例中使用了 $\cfrac{128}{13}\approx 9.84$ 來計算,接著,就是要想辦法透過 bit operation 來得出原數乘上 $\cfrac{13}{128}$ 的結果。
首先, $\cfrac{13}{128}$ 可以被拆解成 $\cfrac{13}{8}$ 再除以 16 ,而 $\cfrac{13}{8}$ 又可以被拆解成 $1+\cfrac{1}{2}+\cfrac{1}{8}$ ,如此一來,原數乘上 $\cfrac{13}{8}$ 就可以用 `q = (tmp >> 3) + (tmp >> 1) + tmp` 計算出來,最後再透過 `q = q << 4` 來除以 16 即可。但若考慮到 mod 的運算,則在利用位元右移計算乘以 2 的冪時所被捨棄的位元就會造成計算上的錯誤,因此該程式碼利用了以下三行將原數最右邊的三個位元先保留下來。
```c
d0 = q & 0b1;
d1 = q & 0b11;
d2 = q & 0b111;
```
在計算完 $\cfrac{13tmp}{8}$ 後,再將其乘上 8 (`q = q << 3`),目的是在於要加回被捨棄掉的 bits ,而在加完之後,再直接除以 128 (`q = q >> 7`) 即可。
至於 mod 的部分,可以直接利用除法原理得到,因為除法原理告訴我們 $tmp = q\times 10 + r$ ,所以餘數可由 $r = tmp - q\times 10$ 得出,因此在程式碼的最後寫了 `((q << 2) + q) << 1` ,即 $(q\times 4 + q)\times 2 = q\times 10$。
將此概念包裝後,可以下方程式碼重新呈現 :
```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 >> CCCC);
*mod = in - ((q & ~0x7) + (*div << DDDD));
}
```
這段程式碼做的事情為將 `in` 這個數以 bitwise 操作同時除以和 mod 10 ,具體的想法為除以 10 等於乘以 0.1 ,因此可先乘上 0.8 再除以 8 。
第一行 `(in | 1) - (in >> 2)` 強制將 `in` 轉為奇數(不知道原因,且不管有沒有那個 `+1` 經過測試後的皆是正確的)並減去 $\cfrac{in}{4}$ 得到 $\cfrac{3in}{4}$ ,而第二行 `(x >> 4) + x` 則是在計算 $\cfrac{3in}{64} + \cfrac{3in}{4}$ ,此計算結果為 $\cfrac{51in}{64} \approx 0.79in$ 即代表前兩行做的事情為令一變數 `q` 等於 `0.8x` 。
而後續的 `q = (q >> 8) + x` 則是在對 0.8 逼近,做越多次就會越接近,最後需再將結果除以 8 ,因此 `CCCC` 為 3 。
餘數的部份,靠除法原理可知 `*mod = in - 10*div` ,因此 `(q & ~0x7) + (*div << DDDD)` 就會等於 `10*div` ,而 `(q & ~0x7)` 做的事情為將 `q` 最低位的 3 個位元清零,此做法等同於 `(q >> 3) << 3` ,即 `8*div` ,因此 `*div << DDDD` 就必須等於 `2*div` , 最終可知 `DDDD` 為 1 。
延伸問題 : 練習撰寫不依賴任何除法指令的 % 9 (modulo 9) 和 % 5 (modulo 5) 程式碼,並證明在 uint32_t 的有效範圍可達到等效。
```c
void divmod_9(uint32_t in, uint32_t *div, uint32_t *mod)
{
uint32_t x = in - (in >> 3); /*in = (in*8/9)/8 8/9 = 7/8 + 7/512 */
uint32_t q = (x >> 6) + x;
x = q;
q = (q >> 12) + x;
q = (q >> 12) + x;
q = (q >> 12) + x;
q = (q >> 12) + x;
*div = (q >> 3);
*mod = in - (q & ~0x7) - *div;
if(*mod == 10 || *mod == 9)
{
*div++;
*mod -= 9;
}
}
```
測試程式碼如下,首先撰寫出 `randnum` 來造一個隨機的 32 位元無號數,接下來利用實際的除法與 mod 運算來確認 `divmod_9` 是否正確 :
```c
uint32_t randnum()
{
uint32_t ran = 0;
ran |= (uint32_t)(rand() & 0xFFFF) << 16;
ran |= (uint32_t)rand() & 0xFFFF;
return ran;
}
int main()
{
uint32_t div, mod;
for(int i = 0; i < 10; i++){
uint32_t ran = randnum();
uint32_t a = ran/9;
uint32_t b = ran%9;
divmod_9(ran, &div, &mod);
printf("a = %d\ndiv = %u\nb = %d\nmod = %u\n", a, div, b, mod);
}
return 0;
}
```
列印出的 10 組數據如下 :
```
a = 129376363
div = 129376363
b = 3
mod = 3
a = 284115184
div = 284115184
b = 3
mod = 3
a = 410702193
div = 410702193
b = 6
mod = 6
a = 276433377
div = 276433377
b = 3
mod = 3
a = 58090291
div = 58090291
b = 2
mod = 2
a = 165404435
div = 165404435
b = 0
mod = 0
a = 122931853
div = 122931853
b = 6
mod = 6
a = 316698205
div = 316698205
b = 1
mod = 1
a = 905749
div = 905749
b = 5
mod = 5
a = 15525887
div = 15525887
b = 1
mod = 1
```
看起來是正確的,接著再改寫 main 的程式碼,把測試資料放大到 1000000 組 :
```c
int main()
{
uint32_t div, mod;
int correct = 0;
for(int i = 0; i < 1000000; i++){
uint32_t ran = randnum();
uint32_t a = ran/9;
uint32_t b = ran%9;
divmod_9(ran, &div, &mod);
if(a == div && b == mod)
{
correct++;
}
}
printf("%d/1000000\n", correct);
return 0;
}
```
輸出的結果為 :
```
1000000/1000000
```
因此可驗證 `divmod_9` 為可行的程式碼。
而對於 mod5 也可以用同樣的方式驗證,程式碼如下 :
```c
uint32_t randnum()
{
uint32_t ran = 0;
ran |= (uint32_t)(rand() & 0xFFFF) << 16;
ran |= (uint32_t)rand() & 0xFFFF;
return ran;
}
void divmod_5(uint32_t in, uint32_t *div, uint32_t *mod)
{
uint32_t x = in - (in >> 2); /*in = (in*4/5)/4 4/5 = 3/4 + 3/64 */
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 >> 2);
*mod = in - (q & ~0x3) - *div;
if(*mod == 6 || *mod == 5)
{
(*div)++;
*mod -= 5;
}
}
int main()
{
uint32_t div, mod;
int correct = 0;
for(int i = 0; i < 1000000; i++){
uint32_t ran = randnum();
uint32_t a = ran/5;
uint32_t b = ran%5;
divmod_5(ran, &div, &mod);
if(a == div && b == mod)
{
correct++;
}
}
printf("%d/1000000\n", correct);
return 0;
}
```
輸出的結果為 :
```
1000000/1000000
```
## [測驗3](https://hackmd.io/@sysprog/linux2024-quiz3#%E6%B8%AC%E9%A9%97-3)
### 版本一
首先將 `log` 設置為 -1 ,這是考慮到 input 為 0 的情況,因為 $2^0$ 為 1 ,接著進入迴圈,若 input 不為 0 ,則將 `log` + 1 ,並且同時將 input 右移一個位元,也就是除以 2 ,在離開迴圈時,答案就會是最高位元的 1 位置,也等於對 input 取 log 的整數值。
### 版本二
```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;
}
```
與版本一的概念相同,都是利用迴圈判斷是否將 input 右移,只是在這個版本利用多個判斷式子來加速進行,而判斷的依據為是否大於 $2^{16}$ $2^8$ $2^4$ 以及 $2^2$ ,因此 `AAAA` , `BBBB` , `CCCC` 分別為 65536 , 256 以及 16 。
#### 版本三
```c
int ilog32(uint32_t v)
{
return (31 - __builtin_clz(DDDD));
}
```
版本三利用了 GNU extension 中的函式 `__builtin_clz` 來改寫,該函式的功能為返回從最高位數來為 0 的個數,因此若利用總位元數減去該函數之結果就可以得到最高位為 1 的位置,即為所求,所以直覺上可以直接把 `v` 放入該函式中,但還必需注意到該函式在 input 為 0 時未定義,因此需將 `v` 改為 `v|1` ,已確保 `v` 不為 0 ,藉此避免未定義的情況發生,因此 `DDDD` 為 `v|1` 。
## [測驗4](https://hackmd.io/@sysprog/linux2024-quiz3#%E6%B8%AC%E9%A9%97-4)
```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](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average) ,具體的數學定義為 :
$S_t = \begin{cases}
Y_0 & t = 0\\
\alpha Y_t + (1 - \alpha)S_{t-1} & t>0
\end{cases}$
$S_t$ 為程式碼中的 `internal` , $Y_t$ 為 `val << avg->factor` ,會隨著 `factor` 值而改變,而 $\alpha$ 為 $\frac{1}{2^{avg->weight}}$ 。
首先,該程式碼應用了三元運算來對 `t` 是否為 0 的情況分類, 並且先將整個式子除以 $\alpha$ 做運算,因此在程式碼中若 `t` 不為 0 ,則需做 `((avg->internal << EEEE) - avg->internal) + (val << FFFF)` ,即表示 $\frac{1}{\alpha}S_{t-1} - S_{t-1} + Y_t$ ,因此 `EEEE` 為 `avg->weight` , `FFFF` 為 `avg->factor` ,算完後還需乘回原本的 $\alpha$ ,因此最後會有 `>> avg->weight` 。
## [測驗5](https://hackmd.io/@sysprog/linux2024-quiz3#%E6%B8%AC%E9%A9%97-5)
```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 > GGGG) + 1;
}
```
根據[測驗3](https://hackmd.io/e9bG8Gx9Qia7Un611VwFaw?view#%E6%B8%AC%E9%A9%973)我們可以知道,若要求某數取 log2 的值只要找到該數最高位元的 1 即可,此程式碼也在做相同的事情,利用 `r` 和 `shift` 記錄一共位移了幾個 bits ,最後再將他們相加得出結果。
首先,`(x > 0xFFFF) << 4` 是在判斷 `x` 的左半邊 16 個 bits 中是否有 1 存在,若有則將 `r` 設為 16 並且將 `x` 右半邊 16 個 bits 捨棄,若沒有則將 `r` 設為 0 , `x` 保持不變。後續就是不斷的對剩餘的位元數做相同的事情,也就是將該數切半並判斷,最後利用 or 運算將 `r` 和 `shift` 以及 `(x > 0x1)` 相加,因此 `GGGG` 為 1 。
在程式碼的最後寫了 `+1` 是對應到了程式碼一開始的 `x--` ,這裡考量了 input 本身為 2 的羃的情況,要注意到當沒有 `x--` 且 input 為 2 的羃時,最後的結果會多 1 ,因此最後利用 `+1` 來修正 : 若 input 為 2 的羃,則加減 1 後答案不變,若不為 2 的羃,則加 1 可達到取 ceiling 的效果。
延伸問題 : 改進程式碼,使其得以處理 x = 0 的狀況,並仍是 branchless 。
```c
int ceil_ilog2(uint32_t x)
{
uint32_t r, shift;
x -= !!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 > GGGG) + 1;
}
```
將 `x--` 改寫為 `x -= !!x` ,利用兩次的邏輯運算 `!` 來判斷 `x` 的值是否為 0 ,如此一來若 `x` 為 0 則 - 0 ,若 `x` 為 1 則 - 1 。
## 第四週測驗題
## [測驗1](https://hackmd.io/@sysprog/linux2024-quiz4#%E6%B8%AC%E9%A9%97-1)
```c
unsigned popcount_branchless(unsigned v)
{
unsigned n;
n = (v >> 1) & 0x77777777;
v -= n;
n = (n >> 1) & 0x77777777;
v -= n;
n = (n >> 1) & 0x77777777;
v -= n;
v = (v + (v >> 4)) & 0x0F0F0F0F;
v *= 0x01010101;
return v >> 24;
}
```
這段程式碼用來計算一個 32 bits 無號整數中 1 的個數,也就是在實作 popcount() 函式。
首先,假設一 32 bits 無號整數 $x = b_{31}b_{30}\cdots b_{2}b_{1}b_{0}$ ,則該數中 1 的個數可以將每個位元相加而得,也就是 $\sum_{n=0}^{31} b_n = \sum_{n=0}^{31} (2^n - \sum_{i=0}^{n-1} 2^i) b_n$ 。
而另一種做法為利用數學式去推導,可以得知 $x - \lfloor \cfrac{x}{2} \rfloor - \lfloor \cfrac{x}{4} \rfloor - \cdots - \lfloor \cfrac{x}{2^{31}} \rfloor$ 也是一種算法,該程式碼就是利用這個觀念求得。
直觀的做法為不斷的將 `v` 除以二,並且用原本的 `v` 去扣除它,就如上方程式碼中的 `n = (v >> 1)` 和 `v -= n` ,不過在 32 bits 的數值上運作就必須重複做 32 次循環,因此該程式碼將其分為 8 組,也就是將每 4 個位元 (nibble) 視為一個單位去做計算,如此本該重複 32 次迭代的程式碼就能簡寫為上方的形式。
首先從公式的部份討論每組 nibble 該如何計算,因為只有 4 個 bits ,所以可以將公式改寫為$\sum_{n=0}^{3}b_n = \sum_{n=0}^{3}(2^n-\sum_{i=0}^{n-1}2^i)b_n$
展開後可得
$(2^3-2^2-2^1-2^0)b_3+(2^2-2^1-2^0)b_2+(2^1-2^0)b_1+2^0b_0$
而若是以數學是推導,可以得知在 4 個 bits 的情況下可由 $x - \lfloor \cfrac{x}{2} \rfloor - \lfloor \cfrac{x}{4} \rfloor- \lfloor \cfrac{x}{8} \rfloor$ 得出答案,且該式等同於 $(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$ 並經過改寫後可得 $(2^3-2^2-2^1-2^0)b_3+(2^2-2^1-2^0)b_2+(2^1-2^0)b_1+2^0b_0$ ,與上述推導出的公式相同,驗證了其正確性。
此程式碼便是以這個概念進行運算,首先,`v >> 1` 是在進行 $\lfloor \cfrac{x}{2} \rfloor$ 的運算,而每多做一次就會再除以 2 並取其底,由於我們讓每 4 個 bits 為一組,因此需要做 3 次至 $\lfloor \cfrac{x}{2^3} \rfloor$ ,而因為每次在除以 2 時皆會將 `v` 右移一位,如此做法會造成前三組 nibble 的最高位元在移動後都會等於下一組的最低位元,計算出的結果就會錯誤。
直接列出來解釋如下 :
```
b31 b30 b29 b28 ... b3 b2 b1 b0 // v
0 b31 b30 b29 ... b4 b3 b2 b1 // v >> 1
```
右移一位後, b4 會成為第一組 nibble 的最高位元,但我們在運算時不需要 b4 ,因此需將其設定為 0 ,這裡的做法是和 `0x77777777` 做 `&` 運算,如此一來便可以同時將八組 nibble 的最高位歸零,不過這裡使用 `0xF7777777` 也可以達到相同效果,因為右移後最高位本身就會被 0 補上。
```
b31 b30 b29 b28 ... b3 b2 b1 b0 // v
0 b31 b30 b29 ... 0 b3 b2 b1 // (v >> 1) & 0x77777777
```
重複做四次後,可以得到一組新的 32 bits 表示,令為 $B_7B_6\cdots B_1B_0$ 其中每一 $B_n$ 皆表示一組 nibble ,接著就是要將八組 nibble 所求出來 1 的個數相加,因此我們想要得到的數值為 $B_7+B_6+B_5+B_4+B_3+B_2+B_1+B_0$ ,實際做法如下 :
```
B7 B6 B5 B4 B3 B2 B1 B0 // v
0 B7 B6 B5 B4 B3 B2 B1 // v >> 4
(B7+0) (B6+B7) (B5+B6) (B4+B5) (B3+B4) (B2+B3) (B1+B2) (B0+B1) // v + (v >> 4)
```
先將該數右移一個 nibble 的長度,接著與原數相加,從得到的數觀察可以發現我們想要的數值為偶數組的 nibble 相加,因此再與 `0x0F0F0F0F` 做 `&` 運算,可得 :
```
// & 0x0F0F0F0F
0 (B7+B6) 0 (B5+B4) 0 (B3+B2) 0 (B1+B0)
```
最後,為了把這些數值相加,我們讓 `v` 去和 `0x01010101` 相乘,以直式展開結果如下 :
```
0 (B7+B6) 0 (B5+B4) 0 (B3+B2) 0 (B1+B0)
x 0 1 0 1 0 1 0 1
------------------------------------------------------------------------------
0 (B7+B6) 0 (B5+B4) 0 (B3+B2) 0 (B1+B0)
0 (B7+B6) 0 (B5+B4) 0 (B3+B2) 0 (B1+B0)
0 (B7+B6) 0 (B5+B4) 0 (B3+B2) 0 (B1+B0)
0 (B7+B6) 0 (B5+B4) 0 (B3+B2) 0 (B1+B0)
------------------------------------------------------------------------------
↑________(B7+B6)+(B5+B4)+(B3+B2)+(B1+B0)
```
到此可以看到我們需要的結果在相乘後數值的第七個 nibble 的位置,也就是我們可以將前面 24 bits 捨棄掉,因此在程式碼的最後用了 `v >> 24` 來進行此一操作,最終就能夠得到 $(2^3-2^2-2^1-2^0)b_{31}+(2^2-2^1-2^0)b_{30}+(2^1-2^0)b_{29}+2^0b_{28}+\cdots+(2^3-2^2-2^1-2^0)b_3+(2^2-2^1-2^0)b_2+(2^1-2^0)b_1+2^0b_0$ ,即為所求。
## [測驗2](https://hackmd.io/@sysprog/linux2024-quiz4#%E6%B8%AC%E9%A9%97-2)
本測驗告訴我們如何不用除法就能得出某兩數相除之後的餘數,以下用 mod 3 當作例子進行操作和解釋 :
由數學上同餘 (modulo) 的特性可以知道,當 $a\equiv b(mod\ \ m)$ 且 $c\equiv d(mod\ \ m)$ ,則 $\ \ a+c\equiv b+d(mod\ \ m)$ 且 $ac\equiv bd(mod\ \ m)$ ,因此若我們有 $1\equiv 1(mod\ \ 3)$ 且 $2\equiv -1(mod\ \ 3)$ ,則可以推出 $2^k\equiv\begin{cases}
1(mod\ \ 3) &if\ \ k\ \ is\ \ even\\
-1(mod\ \ 3) &if\ \ k\ \ is\ \ odd\\
\end{cases}$ 。
假設一 32 bits 無號整數 $n = b_{31}b_{30}\cdots b_{2}b_{1}b_{0}$ ,則 n 的值就會等於 $\sum_{i=0}^{31}b_i2^i$ ,由上述推論可得 $n = \sum_{i=0}^{31}b_i(-1)^i\ \ (mod\ \ 3)$ 。到這裡我們可以發現對於以二進位表示的數來講, mod 3 其實就是將偶數位元相加再減去奇數位元,因此可以利用 population count 類型的函式來進行操作,其中一種做法為 `n = popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA)` ,因為 5 為 0101 , A 為 1010 ,所以如此運算便能將奇偶穿插著相減,達到預期的效果。
而上述做法其實還有別的寫法,那就是利用以下定理將其簡化 :
$popcount(x\& \overline m)-popcount(x\&m) = popcount(x \oplus m)-popcount(m)$
因此我們就能將其改寫為 `n = popcount(n ^ 0xAAAAAAAA) - 16` ,但這裡必須注意到 `popcount(n ^ 0xAAAAAAAA)` 這個值的範圍為 0 < n < 32 , -16 這個動作可能會導致算出來的值為負數,因此需透過加上 3 的倍數避免這樣的情況發生,而該數只要大於 16 即可,以下利用 39 做說明。
```c
int mod3(unsigned n)
{
n = popcount(n ^ 0xAAAAAAAA) + 23;
n = popcount(n ^ 0x2A) - 3;
return n + ((n >> 31) & 3);
}
```
程式碼第一行是由上述推倒而來的,也就是 `n = popcount(n ^ 0xAAAAAAAA) - 16 + 39` ,但此時 n 的範圍就會變成 23 < n < 55 ,而我們希望最終的結果會落在 0 < n < 2 (因為 mod 3 ),因此可以再次利用上述定理縮小其範圍。
在經過第一行的運算後,其範圍已經變為 23 < n < 55 ,只有最低位的 6 個 bits 可能會有 1 存在,因此根據定理,第二行即為 `popcount(n ^ 0x2A) - 3` (因為 0x2A 為 101010 ),此時 n 的範圍為 -3 < n < 2 ,我們只需再處理負數的部分即可。
處理的方法為得到負數就將其 + 3 ,首先,透過右移 31 位來判斷正負,若為 1 則代表是負數,需要 + 3 ,於是和 3 做 `&` 運算,此時會發現要用全 1 去做 `&` 才會是 + 3 的結果,也就是右移需要是算術位移而非邏輯位移,但上方的 input 卻是 `unsigned n` ,因此該程式碼有個小 bug 存在,可以透過將 input 宣告成 `int` 來解決,或者是修改 return 的部分,修改過後的程式碼如下 :
```c
int mod3(unsigned n)
{
n = __builtin_popcount(n ^ 0xAAAAAAAA) + 23;
n = __builtin_popcount(n ^ 0x2A) - 3;
return n + (((n >> 31) | (n >> 30)) & 3);
}
```
在最後一行的地方,我將 `(n >> 31)` 與 `(n >> 30)` 做 or 運算,由於 3 的二進位表示法為 011 ,因此只須考慮最後 2 個 bits 即可,並且利用 or 來達到算術位移的效果,也就是將負數的高位元填滿 1 。
而事實上若改寫為這樣也不需要再 `&3` 了,因為若是負數則位移完會得到 `011` ,若是正數則為 `0` ,因此最終的程式碼為 :
```c
int mod3(unsigned n)
{
n = __builtin_popcount(n ^ 0xAAAAAAAA) + 23;
n = __builtin_popcount(n ^ 0x2A) - 3;
return n + ((n >> 31) | (n >> 30));
}
```
還有另一種做法為利用 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];
}
```
首先要建立一個表格,該表格的內容為 mod 3 後可能的值,也就是 {012} ,而大小則取決於 `n` 的範圍,這裡是 0 ~ 32 ,因此表格大小為 33 。至於表格內值的排序則是由 `n` 所決定的,在這裡因為 `n = popcount(n ^ 0xAAAAAAAA)` ,可以想成 `n = popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA) + 16` ,因此對於 table[0] 來講,要找到當程式碼內的 `n = 0` 時,實際上 mod 3 為多少,而這裡只要找出前 3 項,然後依序填完整個 table 即可 :
`n = 0` : -16 mod 3 為 2 ,所以 table[0] = 2 ;
`n = 1` : -15 mod 3 為 0 ,所以 table[1] = 0 ;
`n = 2` : -14 mod 3 為 1 ,所以 table[2] = 1 ;
如此填完便可以得到正確的值。
將上述的概念應用於 [tictactoe.c](https://gist.github.com/jserv/9088f6f72a35a1fcaaf1c932399a5624) 程式碼,該程式碼用來模擬 100 萬次井字遊戲的對奕,並統計出第一次選擇 9 個不同位置的勝率,具體的輸出結果如下 :
```
Win probability for first move with random agents:
0.115 0.101 0.115
0.101 0.132 0.102
0.116 0.102 0.116
Player 1 won 585067 times
Player 2 won 288241 times
126692 ties
0.055166 seconds
18.127107 million games/sec
```
```c
static const uint32_t move_masks[9] = {
0x40040040, 0x20004000, 0x10000404, 0x04020000, 0x02002022,
0x01000200, 0x00410001, 0x00201000, 0x00100110,
};
```
至於程式碼的部份,首先可以看到位置是用 16 進位 32 bits 無號數來表示的,原因為 8 個 bits 正好可以對應到九宮格的 8 種連線方法,更具體一點的說,由最高位元開始依序往下會對應到的是九宮格中由上到下、由左到右的橫一、橫二、橫三、直一、直二、直三、右斜和左斜,而一組 nibble 中的前 3 個位元則代表了該位置是否已經被放置。以左上角第一個位置做說明,對於橫一、直一和右斜來講,就會是 100 ,其餘皆為 000 ,因此整體就會是 `0x40040040` 。
```c
static inline uint32_t is_win(uint32_t player_board)
{
return (player_board + BBBB) & 0x88888888;
}
```
此段程式碼是用來決定是否已經存在一方獲勝的狀態,也就是上述八種可能有其中一種被填滿,舉例來說,如果是橫二被填滿,那當下的狀態應為 `0x07022222` ,換句話說,被填滿的那條線必定會出現 `7(0b0111)` 這個數字,因此可以將當下的狀態加上 `0x11111111` 並和 `0x8888888` 做 `&` 運算,若有值則代表有存在著連線,否則會 `return 0` 。
`BBBB` 為 `0x11111111` 。
```c
static inline int mod7(uint32_t x)
{
x = (x >> CCCC) + (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);
}
```
此段程式碼想要得到 mod 7 的結果,在第一行利用了 Hacker's Delight 提出"若將 n 拆成兩部分並且將左半右移加上右半不會影響同餘的結果"的結論來進行範圍的縮小,可以看到 `x & UINT32_C(0x7FFF)` 是在計算右半邊 15 位,因此 `x >> CCCC` 就是在計算左半邊 17 位, `CCCC` 為 15 。
而這段程式碼也應用了另一定理 : $n\ \ (mod\ \ 7)\equiv\frac{8}{7}n\ \ (mod\ \ 8)$ ,因此在程式碼的最後出現了 `0x24924925` 這個數字,該數字即為 $\frac{2^{32}}{7}$ ,而再往右位移 29 位即可得到 $\frac{8}{7}$ 。
## [測驗3](https://hackmd.io/@sysprog/linux2024-quiz4#%E6%B8%AC%E9%A9%97-3)
[XTree](https://gist.github.com/jserv/7374b3e031ec3a81441b1b93b008c5d2) 為一種兼具 AVL tree 和 red-black tree 部分特性的結構,以下將說明節點刪除的部份 :
```c
static void __xt_remove(struct xt_node **root, struct xt_node *del)
{
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;
}
if (del == *root) {
*root = 0;
return;
}
/* empty node */
struct xt_node *parent = xt_parent(del);
if (xt_left(parent) == del)
xt_left(parent) = 0;
else
xt_right(parent) = 0;
xt_update(EEEE, FFFF);
}
```
在完整的程式碼中所定義的函式 :
* `xt_first(n)` : 尋找由節點 n 所代表的子樹中最左側的節點,也就是該子樹中最小的節點。
* `xt_last(n)` : 尋找由節點 n 所代表的子樹中最右側的節點,也就是該子樹中最大的節點。
* `xt_replace_right(n, r)` : 用右子樹中的節點 r 來替換調節點 n ,並保持樹狀結構。
* `xt_replace_left(n, l)` : 用左子樹中的節點 l 來替換調節點 n ,並保持樹狀結構。
* `xt_update(n, m)` : 用來更新插入或刪除節點後的樹,利用一參數 `hint` 檢查樹是否平衡,以此決定是否需要 rotate 。
從程式碼的第一段可以看到,該程式碼會先判斷欲刪除的節點是否存在右子樹,若存在,則必須找到該子樹中最小的節點與 del 互換,並且在換完後重新調整以保持平衡,因此 `AAAA` 為 `least` , `BBBB` 為 `xt_right(least)` 。而左子樹也是同樣道理,但由於 BST 的特性,若要交換則要找左子樹中最大的節點,因此使用了 `xt_last` 函式,其餘的部份就是將右子樹處理的方法稍做變換,因此 `CCCC` 為 `most` , `DDDD` 為 `xt_left(most)` 。
在程式碼的最後,還需檢查整棵樹是否達到平衡,因此 `EEEE` 為 `root` , `FFFF` 為 `parent` 。
---
## 問題記錄
* 第三周測驗二不懂為何要有 `(in | 1)` ,因為就算不強行轉為奇數即果還是正確的。
* 第四周測驗二的最後知道算出了 $\frac{8}{7}$ ,但為何這樣就同時做了 mod8 的動作。
* 在撰寫 mod9 和 mod5 的程式碼時,是由逼近的方法求解的,但數字一放大微小的差異也就跟著放大,因此我用了 if 來做調整,但好像可以不用這樣,應該從數值本身去探討其他寫法。
## 補充議題 : 浮點數乘法的實作探討
### float_mul2 探討
IEEE 754, float is single precision. Assume 32-bit
```c
float float_mul2(float x)
{
// using bitwise operation, no mul
int a = *((int*)&x);
int b = *((int*)&x);
a = (a & 0x7F800000) >> 23;
a++;
b = (b & 0x807FFFFF) | (a << 23);
return *((float*)&b);
}
```
考慮 overflow 後的做法 :
```c
float float_mul2(float x)
{
// using bitwise operation, no mul
int a = *((int*)&x);
int b = *((int*)&x);
a = (a & 0x7F800000) >> 23;
a++;
if(a == 0xFFFFFFFF)
return NAN;
else
b = (b & 0x807FFFFF) | (a << 23);
return *((float*)&b);
}
```
> https://godbolt.org/
* Instruction latency
從老師提供的 instruction tables 找到對應的處理器架構,並且將上方程式碼轉換成組合語言去逐行計算,得到的結果為 :
```c
float_mul2: # @float_mul2
push rbp
mov rbp, rsp
movss dword ptr [rbp - 4], xmm0
mov eax, dword ptr [rbp - 4]
mov dword ptr [rbp - 8], eax
mov eax, dword ptr [rbp - 4]
mov dword ptr [rbp - 12], eax
mov eax, dword ptr [rbp - 8]
and eax, 2139095040
sar eax, 23
mov dword ptr [rbp - 8], eax
mov eax, dword ptr [rbp - 8]
add eax, 1
mov dword ptr [rbp - 8], eax
mov eax, dword ptr [rbp - 12]
and eax, -2139095041
mov ecx, dword ptr [rbp - 8]
shl ecx, 23
or eax, ecx
mov dword ptr [rbp - 12], eax
movss xmm0, dword ptr [rbp - 12] # xmm0 = mem[0],zero,zero,zero
pop rbp
ret
```
|Move instructions|Arithmetic instructions|Logic|Control transfer instructions|
|-|-|-|-|
|push * 1, mov * 12, movss * 2, pop * 1|add * 1|and * 2, sar * 1, shl * 1, or * 1|ret * 1|
計算過後可知該程式總共需要 29 (2+12+2+1+1+2+7+1+1)個 clock cycle 。
以下為稍微改進過後的版本,嘗試只用一個變數來節省記憶體空間 :
```c
float float_mul2(float x)
{
// using bitwise operation, no mul
int a = *((int*)&x);
a = ((a & 0x7F800000) + 0x00800000) | (a & 0x807FFFFF);
return *((float*)&a);
}
```
而這麼做還是太複雜,因為要先得到 exponent 的那 8 個 bits 加 1 後再把其他部分補回,因此更簡單的想法為直接在 exponent 那項加 1 ,修改過後的程式碼如下 :
```c
float float_mul2(float x)
{
// using bitwise operation, no mul
*(int*)&x += 0x00800000;
return x;
}
```
轉換為組合語言 :
```c
float_mul2: # @float_mul2
push rbp
mov rbp, rsp
movss dword ptr [rbp - 4], xmm0
mov eax, dword ptr [rbp - 4]
add eax, 8388608
mov dword ptr [rbp - 4], eax
movss xmm0, dword ptr [rbp - 4] # xmm0 = mem[0],zero,zero,zero
pop rbp
ret
```
|Move instructions|Arithmetic instructions|Logic|Control transfer instructions|
|-|-|-|-|
|push * 1, mov * 3, movss * 2, pop * 1|add * 1|none|ret * 1|
計算過後可知該程式總共需要 9 (2+3+2+1+1)個 clock cycle ,可以發現確實比第一個版本少了許多。
參考 [類神經網路的 ReLU 及其常數時間複雜度實作](https://hackmd.io/@sysprog/constant-time-relu?fbclid=IwZXh0bgNhZW0CMTAAAR0rA_gp9TtuqOBuqAJE2uutnvVZcFOSFlBPO9HspdhAA27KtGx6_rdC1aQ_aem_AbKPshOIUtxP5VUsyxPO6IG4EvaWPm2n-8Idh7E0hXijU7Y45cxOfZIWXO1McstmzjxNWThJiOkVZLV_kqQ-1Fby) 教材,裡面提到了由於對浮點數的操作成本太高,因此可以使用 union 的技巧來優化, union 可以讓浮點數和整數共用一段記憶體空間,並且在任一時刻只有一個成員有效,宣告的方法如下 :
```c
union{
float f;
int i;
} out = {.f=x};
```
宣告一個名為 out 的 union ,裡面輸入需要操作的資料型態,而最後的 `{.f=x}` 是在做資料得初始化,將 input `x` 的值指派到 `f` ,後續則可以根據需求使用 `out.f` 及 `out.i` 來獲得兩種不同的資料型態。
以下給出一個簡單的例子 :
```c
//code
float example(float x){
union{
float f;
int i;
} out = {.f=x};
printf("out.i = %d\n", out.i);
printf("out.f = %f\n", out.f);
}
int main()
{
float x = 2;
example(x);
return 0;
}
//output
out.i = 1073741824
out.f = 2.000000
```
原因為在這段記憶體裡存著 `0x40000000` ,以浮點數表示為 2 ,以整數表示則為 $2^{30}$ = 1073741824 。
最後,將原本的程式碼改寫 :
```c
float float_mul2(float x)
{
// using bitwise operation, no mul
union{
float f;
int i;
} out = {.f=x};
out.i += 0x00800000;
return out.f;
}
```
轉換為組合語言 :
```c
float_mul2: # @float_mul2
push rbp
mov rbp, rsp
movss dword ptr [rbp - 4], xmm0
movss xmm0, dword ptr [rbp - 4] # xmm0 = mem[0],zero,zero,zero
movss dword ptr [rbp - 8], xmm0
mov eax, dword ptr [rbp - 8]
add eax, 8388608
mov dword ptr [rbp - 8], eax
movss xmm0, dword ptr [rbp - 8] # xmm0 = mem[0],zero,zero,zero
pop rbp
ret
```
|Move instructions|Arithmetic instructions|Logic|Control transfer instructions|
|-|-|-|-|
|push * 1, mov * 3, movss * 4, pop * 1|add * 1|none|ret * 1|
計算過後可知該程式總共需要 11 (2+3+4+1+1)個 clock cycle ,雖然相較於第二個版本多了 2 個 clock cycle ,但用 union 的寫法會比較安全,因為強制將資料型別轉換可能會造成 strict aliasing ,在 C 語言中會發生為定義的行為。
### float_mul_power_of_2 探討
```c
float float_mul_power_of_2(float x, int e)
{
// using bitwise operation, no mul
union{
float f;
int i;
} out = {.f=x};
out.i += (e << 23);
return out.f;
}
```
利用和上述 `float_mul2` 相同的概念,若要乘上 2 的冪,則把指數部分的 `+1` 改為 `+e` 即可,但若是直接用 `(e << 23)` 這樣的寫法會有問題 :
若該整數 `e` 夠大或夠小,則位移後會影響到最左方的 sign bit,因此我們需想辦法將其大小控制在 8 個 bits 以內,以符合 IEEE 754 的範圍。
解決方法 :
首先,由於浮點數的指數部分的範圍為 0 ~ 255 ,因此我們可以把指數部分單獨取出來看是否有在範圍內,對應的程式碼如下 :
```c
int exp = (out.i >> 23) & 0xFF;
exp += e;
if(exp < 0 || exp > 255)
return 0;
```
該程式碼的作用為先將 fraction bits 23 位過濾掉,然後再和 8 個 1 做 `&` 運算,以取得原數的指數部分,接著用整數型態的變數和 input `e` 相加,最後再判斷是否有在範圍內,完整的程式碼如下 :
```c
float float_mul_power_of_2(float x, int e)
{
// using bitwise operation, no mul
union{
float f;
int i;
} out = {.f=x};
int exp = (out.i >> 23) & 0xFF;
exp += e;
if(exp < 0 || exp > 255)
return 0;
else
out.i = (out.i & 0x807FFFFF) | (exp << 23);
return out.f;
}
```
其中 `return 0` 的部分若是引進 `math.h` 函式庫則可以再細分為不同的情況來表達,以下為修改過後的程式碼 :
```c
float float_mul_power_of_2(float x, int e)
{
// using bitwise operation, no mul
union{
float f;
int i;
} out = {.f=x};
int exp = (out.i >> 23) & 0xFF;
exp += e;
if(exp < 0)
return 0;
else if(exp >= 255)
return INFINITY;
else
out.i = (out.i & 0x807FFFFF) | (exp << 23);
return out.f;
}
```