---
tags: linux2022
---
# 2022q1 Homework2 (quiz2)
contributed by **yyyyuwen**
> [測驗題](https://hackmd.io/@sysprog/linux2022-quiz2)
## [測驗1](https://hackmd.io/@sysprog/linux2022-quiz2#%E6%B8%AC%E9%A9%97-1)
* 題目要求:求兩整數之平均值,並將有可能 overflow 的程式碼改成避免 overflow 的形式。
以下為有可能造成 overflow 的程式碼:
```c=
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a + b) / 2;
}
```
#### 版本1
```c=
#include <stdint.h>
uint32_t average(uint32_t low, uint32_t high)
{
return low + (high - low) / 2;
}
```
注意:確保大小數的順序,不然也有可能會導致輸出結果不同的問題。
> 當 `(high - low) < 0` 時,值會是 $⌈(high - low)⌉$
> 當 `(high - low) > 0` 時,值會是 $⌊(high - low)⌋$
#### 版本2
```c=
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (EXP1);
// EXP1 = a & b & 1
}
```
其中 `EXP1` 為 `a`, `b`, `1` (數字) 進行某種特別 bitwise 操作,限定用 OR, AND, XOR 這三個運算子。
* `a >> 1` 和 `b >> 1` 的意思是將數值除以2。而 `a` 和 `b` 都是 `uint32_t` 的型態,若是奇數無法整除小數會直接被省去。因此若遇上這種狀況時,做完 `(a >> 1) + (b >> 1)` 之後還要再加上 1。
* ` a & 1 ` :檢查 a 的 Least Significant Bit 是否為 1 (即檢查是否為奇數),若是則回傳 1
。
* EXP1 : 根據以上推論,需要檢查 a 和 b 是否為奇數,填入 ` a & b & 1 ` 。
舉例:
```
a = 5 = 0101, b = 7 = 0111
取平均為 6 -> 0110
a >> 1 = 0010
b >> 1 = 0011
0010 | 0011 = 0011 != 0111
```
#### 版本3
```c=
uint32_t average(uint32_t a, uint32_t b)
{
return (EXP2) + ((EXP3) >> 1);
}
```
其中 `EXP2` 和 `EXP3` 為 `a` 和 `b` 進行某種特別 bitwise 操作,限定用 OR, AND, XOR 這三個運算子。
參考 [加法運算](https://kopu.chat/%E4%BB%A5c%E5%AF%A6%E4%BD%9C%E4%BA%8C%E9%80%B2%E4%BD%8D%E5%8A%A0%E6%B3%95/)

`a + b` 可以變成是 `a XOR b + (a AND b) << 1`
* `a XOR b` : a + b 但不進位
* `(a AND b) << 1` : 檢查 `a`, `b` Most Significant Bit是不是同為 1,如果是則進位。
:::info
**SUM** :取得未進位時的SUM
| a | b | a ^ b | a + b |
|:-:|:-:| :---: | :---------: |
| 0 | 0 | 0 | $00_b$ |
| 0 | 1 | 1 | $01_b$ |
| 1 | 0 | 1 | $01_b$ |
| 1 | 1 | 0 | $10_b$ (需要進位)|
**CARRY** :取得是否進位的資訊
| a | b | a & b | a + b |
|:-:|:-:| :---: | :---------: |
| 0 | 0 | 0 | $00_b$ -> 不進位 |
| 0 | 1 | 0 | $01_b$ -> 不進位 |
| 1 | 0 | 0 | $01_b$ -> 不進位 |
| 1 | 1 | 1 | $10_b$ -> 進位 |
:::
**(a + b) / 2**
式子分解:
= `( a + b ) >> 1`
= `(( a ^ b ) + ( a & b ) << 1) >> 1`
= `a & b + ( a ^ b ) >> 1`
* EXP2 : `a & b`
* EXP3 : `a ^ b`
:::warning
再做實驗時遇到一個問題:當用 `uint32_t`的時候做 `(a + b)/2`會 overflow 沒問題,但當如果使用 `uint8_t` 的時候就不會有 overflow的問題。目前還沒找到原因。
:::
* [x64 Cheat Sheet](https://cs.brown.edu/courses/cs033/docs/guides/x64_cheatsheet.pdf)
* [EAX, ECX, EDX, EBX 應用](https://www.cnblogs.com/qq78292959/archive/2012/07/20/2600865.html)
* [MOV 指令基本格式](https://www.itread01.com/content/1548768066.html)
* [除法指令](https://cjting.me/2021/03/16/the-missing-div-instruction-part1/)
* [Shift (sal, shl, sar, shr)](https://docs.oracle.com/cd/E19455-01/806-3773/instructionset-27/index.html)
:::success
延伸問題:
- [x] 1. 解釋上述程式碼運作的原理
- [ ] 2. 比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 [CS:APP 第 3 章](https://hackmd.io/@sysprog/CSAPP-ch3))
- [ ] 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),又稱滾動平均值、滑動平均,在統計學中是種藉由建立整個資料集合中不同子集的一系列平均數,來分析資料點的計算方法。
移動平均通常與時間序列資料一起使用,以消除短期波動,突出長期趨勢或周期。短期和長期之間的閾值取決於應用,移動平均的參數將相應地設置。例如,它通常用於對財務數據進行技術分析,如股票價格、收益率或交易量。它也用於經濟學中研究國內生產總值、就業或其他宏觀經濟時間序列。
:::
## [測驗2](https://hackmd.io/@sysprog/linux2022-quiz2#%E6%B8%AC%E9%A9%97-2)
* 改寫〈[解讀計算機編碼](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)〉一文的「不需要分支的設計」一節提供的程式碼 min,並實作 (`max`):
以下是該文章提出的 min 程式碼實作
```c=
int32_t min(int32_t a, int32_t b) {
int32_t diff = (a - b);
return b + (diff & (diff >> 31));
}
```
注意:這實作僅在 INT32_MIN <= (a - b) <= INT32_MAX 成立時,才會發揮作用。
而我們要實作 `max` 的方法
```c=
#include <stdint.h>
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((EXP4) & -(EXP5));
}
```
[XOR 的幾個重要功用](https://florian.github.io/xor-trick/)
* x ^ 0 = x
* x ^ x = 0
* x ^ y ^ x = y
以下為了方便,將 `((EXP4) & -(EXP5))` 設成 x
觀察題目,有兩種狀況:
1. **a >= b**
此時應該會 return a ,則算式應為 `a ^ 0 = a` -> `x = 0`
2. **a < b**
這個狀況則需要 return b ,算式應為 `a ^ a ^ b = b` -> `x = a ^ b`
依上述狀況可以判斷
* `EXP4` 可以填入 `a ^ b`
* `EXP5` 作為判斷 0 或 1
* `-(EXP5)` 是 0 ( $00000000000000000000000000000000_b$ ) -> `a > b`
* `-(EXP5)` 是 -(1) ( $11111111111111111111111111111111_b$ ) -> `a < b`
* 因此 `EXP5` 填入 `a < b`
```c=
#include <stdint.h>
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((a ^ b) & -(a < b));
}
```
:::success
延伸問題:
- [x] 1. 解釋上述程式碼運作的原理
- [ ] 2. 針對 32 位元無號/有號整數,撰寫同樣 [branchless](https://en.wikipedia.org/wiki/Branch_(computer_science)) 的實作
- [ ] 3. 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 檢索。
:::
## [測驗3](https://hackmd.io/@sysprog/linux2022-quiz2#%E6%B8%AC%E9%A9%97-3)
原本程式
```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;
}
```
改寫成
```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;
}
```
註: GCD 演算法
1. If both x and y are 0, gcd is zero $gcd(0,0) = 0$.
2. $gcd(x,0) = x$ and $gcd(0,y) = y$ because everything divides 0.
```c
if (!u || !v)
return u | v;
```
3. If x and y are both even, $gcd(x,y) = 2*gcd(\frac{x}{2}, \frac{y}{2})$ because $2$ is a common divisor. Multiplication with $2$ can be done with bitwise shift operator.
```c
for (shift = 0; !((u | v) & 1); shift++) {
u /= 2, v /= 2;
}
```
`(u | v) & 1` 判斷是否同為偶數,`shift` 紀錄總共做了幾次 right shift 的運算 ( <<1 )。
4. If x is even and y is odd, $gcd(x,y) = gcd(\frac{x}{2},y)$.
5. On similar lines, if x is odd and y is even, then $gcd(x,y) = gcd(x,\frac{y}{2})$. It is because $2$ is not a common divisor.
確保 `u` 為奇數:
```c
while (!(u & 1))
u /= 2;
```
輾轉相除法:
```c
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
```
利用 `while` 去遞迴的找餘數,使用連續減法來找出相除的餘數。
而根據輾轉相除法的定理,中止條件則是除到餘數為0。
在上述程式中
`u = v` 是商數的部份,而 `v = t` 是餘數的部份,所以 `COND` 指的就是餘數的部份也就是當 `v` 等於0的時候變中止迴圈; 因此 `COND = v` 。
而 `u` 則是最後的最大公因數,但記得要乘回一開始 shift 的部份,真正的最大公司數應為 $2^{shift}*gcd(u,v)$ 。
因此 `RET = u << shift` 。
* [輾轉相除法 wiki](https://zh.wikipedia.org/wiki/%E8%BC%BE%E8%BD%89%E7%9B%B8%E9%99%A4%E6%B3%95)
* [__ffs](https://blog.csdn.net/tiantao2012/article/details/59107960)
:::success
延伸問題:
- [x] 解釋上述程式運作原理;
- [ ] 在 x86_64 上透過 `__builtin_ctz` 改寫 GCD,分析對效能的提升;
- [ ] Linux 核心中也內建 GCD (而且還不只一種實作),例如 [lib/math/gcd.c](https://github.com/torvalds/linux/blob/master/lib/math/gcd.c),請解釋其實作手法和探討在核心內的應用場景。
:::
## [測驗4](https://hackmd.io/@sysprog/linux2022-quiz2#%E6%B8%AC%E9%A9%97-4)
* 題目:在影像處理中,[bit array](https://en.wikipedia.org/wiki/Bit_array) (也稱 bitset) 廣泛使用,考慮以下程式碼:
```c=
#include <stddef.h>
size_t naive(uint64_t *bitmap, size_t bitmapsize, uint32_t *out)
{
size_t pos = 0;
for (size_t k = 0; k < bitmapsize; ++k) {
uint64_t bitset = bitmap[k];
size_t p = k * 64;
for (int i = 0; i < 64; i++) {
if ((bitset >> i) & 0x1)
out[pos++] = p + i;
}
}
return pos;
}
```
考慮 GNU extenstion 的 [__builtin_ctzll](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)的行為是回傳由低位往高位遇上連續多少個 0 才碰到 1。
> 範例: 當 a = 16
16 這個十進位數值的二進位表示法為 00000000 00000000 00000000 00010000
從低位元 (即右側) 往高位元,我們可發現 0 → 0 → 0 → 0 → 1 ,於是 ctz 就為 4,表示最低位元往高位元有 4 個 0
用以改寫的程式碼如下:
```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;
}
```
其中第 9 行的作用是找出目前最低位元的 `1`,並紀錄到 `t` 變數。舉例來說,若目前 `bitset` 為 000101~b~,那 `t` 就會變成 000001~b~,接著就可以透過 XOR 把該位的 `1` 清掉,其他保留 (此為 XOR 的特性)。
若 bitmap 越鬆散 (即 `1` 越少),於是 `improved` 的效益就更高。
請補完程式碼。書寫規範:
* bitwise 運算子和運算元之間用一個半形空白區隔,如: bitset | 0x1
* 考慮到 -bitwise 的特性
* 不要包含任何小括號,即 ( 和 )
* 以最精簡的形式撰寫程式碼
#### 解釋
可以從題目得知該行的目的是為了找出最低位元的1,可以透過二補數的原理來達成目的。即 `bitset & -bitset`。
舉例
```
bitset = 6 = 0110
-bitset = 1010
則
bitset & -bitset = 0010 //剩下的即是最低位元的1
```
關於其他程式碼的解釋:
`k` 代表的是64位 bitset 在整個 bitmap 中的編號
`r` 則是 bitmap 中末端為零的數量
:::success
延伸問題:
- [x] 解釋上述程式運作原理,並舉出這樣的程式碼用在哪些真實案例中;
- [ ] 設計實驗,檢驗 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 的案例;
:::