# 2022q1 Homework2 (quiz2)
contributed by <`curlyw819` >
###### tags: `linux2022`
[測驗2題目](https://hackmd.io/@sysprog/linux2022-quiz2)
## 測驗 1
:::info
- [x] 解釋上述程式碼運作的原理
- [ ] 比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 CS:APP 第 3 章)
- [ ] 研讀 Linux 核心原始程式碼 [include/linux/average.h](https://github.com/torvalds/linux/blob/master/include/linux/average.h),探討其 [Exponentially weighted moving average (EWMA)](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average) 實作,以及在 Linux 核心的應用
> 移動平均(Moving average),又稱滾動平均值、滑動平均,在統計學中是種藉由建立整個資料集合中不同子集的一系列平均數,來分析資料點的計算方法。
> 移動平均通常與時間序列資料一起使用,以消除短期波動,突出長期趨勢或周期。短期和長期之間的閾值取決於應用,移動平均的參數將相應地設置。例如,它通常用於對財務數據進行技術分析,如股票價格、收益率或交易量。它也用於經濟學中研究國內生產總值、就業或其他宏觀經濟時間序列。
:::
### 程式碼
```c
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (EXP1);
}
```
```c
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (EXP2) + ((EXP3) >> 1);
}
```
### 延伸問題 1
#### `EXP1 : a & b & 1`
說明 : 當 a 和 b 分別左移 1bit 後,若是 a 和 b 原本都是奇數,則原本的算法先加再除時,從二進制的觀點來看 LSB 會產生進位,但位移法卻直接移除LSB,實際上位移法在兩數都為奇數的情況下應該要再 + 1才是正確結果。所以要使用 a&b 來確認 a 與 b 是不是皆為奇數,若是則 and 的結果 LSB 為 1 ,此結果再與 1 做 and 得到數字 1,就能達到判斷是否兩數皆偶數,是的話要再 + 1。
#### `EXP2 : a & b` `EXP3 : a ^ b`
說明 : `a & b` 表進位,`a ^ b` 表位元和。
(x + y) / 2
若用二進制的角度去運算則算式如下 :
= `(x + y) >> 1`
上式的加法可以使用加法器的角度去分解。加法器使用一個 and 閘與 xor 閘進行加法運算,得到位元和與進位 :
![](https://i.imgur.com/heGv5X5.png)
將得到的進位左移1bit再相加來使進位加法運算成立,則我們可以再將上式拆分成如下算式 :
= `(x ^ y + (x & y) << 1) >> 1`
上式的位元右移具有分配性,並與括弧內的算術左移作抵銷,得到以下算式 :
= `(x & y) + ((x ^ y) >> 1)`
## 測驗 2
:::info
- [x] 解釋上述程式碼運作的原理
- [ ] 針對 32 位元無號/有號整數,撰寫同樣 [branchless](https://en.wikipedia.org/wiki/Branch_(computer_science)) 的實作
- [ ] Linux 核心也有若干 branchless / branch-free 的實作,例如 [lib/sort.c](https://github.com/torvalds/linux/blob/master/lib/sort.c):
```c
/*
* Logically, we're doing "if (i & lsbit) i -= size;", but since the
* branch is unpredictable, it's done with a bit of clever branch-free
* code instead.
*/
__attribute_const__ __always_inline
static size_t parent(size_t i, unsigned int lsbit, size_t size)
{
i -= size;
i -= size & -(i & lsbit);
return i / 2;
}
```
請在 Linux 核心原始程式碼中,找出更多類似的實作手法。請善用 git log 檢索。
:::
### 程式碼
```c
#include <stdint.h>
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((EXP4) & -(EXP5));
}
```
### 延伸問題 1
#### `EXP4 : a ^ b` `EXP5 : a < b`
說明 : 從互斥或的性質來推斷,由於
`a ^ 0 = a`
`a ^ a = 0`
由以上兩式可推出
`a ^ 0 = a ^ b ^ b = a`
`a ^ a ^ b == b`
若想 return a,則 `((EXP4) & -(EXP5)) 需為 0`
若想 return b,則 `((EXP4) & -(EXP5))須為 a ^ b`
`-EXP5` 只會是 `0x00000000` 或 `0xffffffff`,而和 `-EXP5` 做運算的下場只會是0或是保持不變,所以`EXP4 = a ^ b`。
接下來觀察真值表推出`EXP5`
|a|b|說明|-EXP5|EXP5|
|:-:|:-:|:-:|:-:|:-:|
|0|0|return `a` 即可,也就是希望 `a ^ ((EXP4) & -(EXP5)) = a`,所以 `-EXP5 = 0`|0|0|
|0|1|`b` 大,所以希望 `((EXP4) & -(EXP5)) = a ^ b`,所以 `-EXP5 = -1`|-1|1|
|1|0|`a` 大,所以希望 `((EXP4) & -(EXP5)) = 0`,所以 `-EXP5 = 0`|0|0
|1|1|同 `a = 0`,`b = 0`|0|0|
由以上真值表可得當 `a < b` 時 `EXP5 = 1` ,所以 `EXP5 = a < b`
## 測驗 3
:::info
- [x] 解釋上述程式運作原理;
- [ ] 在 x86_64 上透過 `__builtin_ctz` 改寫 GCD,分析對效能的提升;
- [ ] Linux 核心中也內建 GCD (而且還不只一種實作),例如 [lib/math/gcd.c](https://github.com/torvalds/linux/blob/master/lib/math/gcd.c),請解釋其實作手法和探討在核心內的應用場景。
:::
### 程式碼
```c
#include <stdint.h>
uint64_t gcd64(uint64_t u, uint64_t v)
{
if (!u || !v) return u | v;
int shift;
for (shift = 0; !((u | v) & 1); shift++) {
u /= 2, v /= 2;
}
while (!(u & 1))
u /= 2;
do {
while (!(v & 1))
v /= 2;
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (COND);
return RET;
}
```
### 延伸問題 1
#### `COND = v`, `RET = u << shift`
說明 : 首先可以從GDB改寫前的程式碼做參考依據
```c
#include <stdint.h>
uint64_t gcd64(uint64_t u, uint64_t v)
{
if (!u || !v) return u | v;
while (v) {
uint64_t t = v;
v = u % v;
u = t;
}
return u;
}
```
觀察改寫後程式碼,可以發現之間的差別在於改寫後的演算法強調減少演算法iteratioin次數而先將兩數字中具有2的倍數的公因數的部分給除掉,也就是邏輯右移,但為了不影響最後輸出的結果,得在 `return` 前將要 `return` 的數字乘回前面除掉的公因數,而在改寫後程式碼中都沒有看到這樣的操作,所以可以推斷是在 return 那行計算的。而至於要乘幾次 2,在程式碼一開始
```c
int shift;
for (shift = 0; !((u | v) & 1); shift++) {
u /= 2, v /= 2;
}
```
就用 `shift` 記錄了除以2的次數,所以 return 那行勢必會有 `2 * shift` 或 `<< shift` ,使用 `<< shift` 可減少CPU運算量,故使用之。
```c
while (!(u & 1))
u /= 2;
```
這邊將不是公因數部分的2的倍數剔除,以減少之後iteration的次數
```c
do {
while (!(v & 1))
v /= 2;
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (v);
```
這部分是很值觀的輾轉相除演算法,其中` while (!(v & 1)) v /= 2;`這段是將相除的過程中產生的2的倍數去除,以減少iteration的次數,由於本質上是標準的輾轉相除,因此可得 `COND = v` , 且最後 `return u`,再結合前文推導,得出 `RET = u << shift`。
## 測驗 4
:::info
- [ ] 解釋上述程式運作原理,並舉出這樣的程式碼用在哪些真實案例中;
- [ ] 設計實驗,檢驗 ctz/clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 [bitmap density](http://www.vki.com/2013/Support/docs/vgltools-3.html);
- [ ] 思考進一步的改進空間;
- [ ] 閱讀 [Data Structures in the Linux Kernel](https://0xax.gitbooks.io/linux-insides/content/DataStructures/linux-datastructures-3.html) 並舉出 Linux 核心使用 bitmap 的案例;
:::
### 程式碼
```c
#include <stddef.h>
size_t improved(uint64_t *bitmap, size_t bitmapsize, uint32_t *out)
{
size_t pos = 0;
uint64_t bitset;
for (size_t k = 0; k < bitmapsize; ++k) {
bitset = bitmap[k];
while (bitset != 0) {
uint64_t t = EXP6;
int r = __builtin_ctzll(bitset);
out[pos++] = k * 64 + r;
bitset ^= t;
}
}
return pos;
}
```
### 延伸問題 1
#### `EXP6 = bitset & (~bitset + 1)`
說明 : 參考網站 [[C 語言] Bitwise operation note](https://m033010041.github.io/2020/02/28/bitwise-operation-in-C/),要取 `bit array` 中最低位元是 1 的位置,方法為和 `bit array` 本身的 2 的補數作 AND。
## 參考資料
加法器 : [維基百科](https://zh.wikipedia.org/wiki/%E5%8A%A0%E6%B3%95%E5%99%A8)