# 2022q1 Homework2 (quiz2)
contributed by < [Build-A-Moat](https://github.com/Build-A-Moat/lab0-c) >
###### tags: `linux2022`
### 測驗 `1`
考慮以下對二個無號整數取平均值的程式碼:
```c
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a + b) / 2;
}
```
我們可改寫為以下等價的實作:
```c
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (EXP1);
}
```
| a | b | (a + b) / 2 | (a + b) >> 1 | (a >> 1) + (b >> 1) |
|:---:|:---:|:-----------:|:------------:|:-------------------:|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 1 | **0** |
* 從真值表中得知 `(a + b) / 2` 等價於 `(a + b) >> 1` ,而與 `(a >> 1) + (b >> 1)` 不等價,所以 `>>` 沒有分配律。
* 數值被 `>>` 作用時最低位元會被捨去,所以若 `a` 和 `b` 最低位元相加的結果,
只影響到最低位元,則`(a + b) >> 1` 與 `(a >> 1) + (b >> 1)` 等價,而其中當 `a` 和 `b` 的最低位元皆為 `1` 時,會產生進位
* `(EXP1)` 就是進位的部份,即 `(a & b & 1)` , `& 1`代表取 `a` 與 `b` 的最低位元。
再次改寫為以下等價的實作:
```c
uint32_t average(uint32_t a, uint32_t b)
{
return (EXP2) + ((EXP3) >> 1);
}
```
* 從<[你所不知道的 C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics)> ,我們可以知道
`(x + y) / 2`
= `(x + y) >> 1`
= `(x ^ y + (x & y) << 1) >> 1`
= `(x & y) + ((x ^ y) >> 1)`
* `EXP2` 即為 `a & b` , `EXP3` 即為 `a ^ b`
* 但是剛剛上面才說 `>>` 沒有分配律,為何在此處卻成立,
| x | y | (x ^ y + (x & y) << 1) >> 1 | (x & y) + ((x ^ y) >> 1) |
|:---:|:---:|:---------------------------:|:------------------------:|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 0 |
| 1 | 0 | 0 | 0 |
| 1 | 1 | 1 | 1 |
* 從真值表中的確可以看出結果相等,
* 由上面兩個例子可以推斷,當 `(a + b) >> 1` 中的 `a + b` 若會產生進位,則不可以將 `>>` 分別運算於 `a` 和 `b`
* 以 `(a ^ b + a ^ b) >> 1` 驗證猜測是否正確
| a | b | (a ^ b + a ^ b) >> 1 | (a ^ b) >> 1 + (a ^ b) >> 1 |
|:---:|:---:|:--------------------:|:---------------------------:|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | **0** |
| 1 | 0 | 1 | **0** |
| 1 | 1 | 0 | 0 |
* 猜測正確
先用 `gcc -O3 -c average.c` 編譯,再用 `objdump -d average.o` 來看。
```c
0000000000000000 <average>:
0: f3 0f 1e fa endbr64
4: 8d 04 37 lea (%rdi,%rsi,1),%eax
7: d1 e8 shr %eax
9: c3 retq
```
```c
0000000000000000 <average>:
0: f3 0f 1e fa endbr64
4: 89 f8 mov %edi,%eax
6: 89 f2 mov %esi,%edx
8: 21 f7 and %esi,%edi
a: d1 e8 shr %eax
c: d1 ea shr %edx
e: 83 e7 01 and $0x1,%edi
11: 01 d0 add %edx,%eax
13: 01 f8 add %edi,%eax
15: c3 retq
```
```c
0000000000000000 <average>:
0: f3 0f 1e fa endbr64
4: 89 f8 mov %edi,%eax
6: 21 f0 and %esi,%eax
8: 31 f7 xor %esi,%edi
a: d1 ef shr %edi
c: 01 f8 add %edi,%eax
e: c3 retq
```
- [ ] 比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 CS:APP 第 3 章)
- [ ] 研讀 Linux 核心原始程式碼 include/linux/average.h,探討其 Exponentially weighted moving average (EWMA) 實作,以及在 Linux 核心的應用
---
### 測驗 `2`
改寫[〈解讀計算機編碼〉](https://hackmd.io/@sysprog/binary-representation) 一文的「不需要分支的設計」一節提供的程式碼 `min`,我們得到以下實作 (`max`):
```c
#include <stdint.h>
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((EXP4) & -(EXP5));
}
```
* 考慮以下兩種狀況
* *Case 1* : a > b, `return a ^ 0`,`EXP5` 需要為 `0`,則 `EXP5` 為 `a < b` 或 `a <= b`,當 `a == b` 時,return `a` 即可,所以選擇 `a < b`
* *Case 2* : a < b, `return a ^ (a ^ b)`,`EXP4` 為 `a ^ b`
- [ ] 對 32 位元無號/有號整數,撰寫同樣 branchless 的實作
- [ ] Linux 核心也有若干 branchless / branch-free 的實作,例如 lib/sort.c: 請在 Linux 核心原始程式碼中,找出更多類似的實作手法。請善用 git log 檢索。
---
### 測驗 `3`
考慮以下 64 位元 GCD ([greatest common divisor](https://en.wikipedia.org/wiki/Greatest_common_divisor), 最大公因數) 求值函式:
```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.
`if (!u || !v) return u | v;` 可以表達1與2
> 3. If x and y are both even, gcd(x,y)=2∗gcd(x2,y2) 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` or `v` 可以同時被 `2` 整除,那就表達成 gcd(u,v)=$2^{shift}$∗gcd($\dfrac{u}{2^{shift}}$,$\dfrac{v}{2^{shift}}$)
> 4. If x is even and y is odd, gcd(x,y)=gcd(x2,y).
> 5. On similar lines, if x is odd and y is even, then gcd(x,y)=gcd(x,y2). It is because 2 is not a common divisor.
```c
while (!(u & 1))
u /= 2;
while (!(v & 1))
v /= 2;
```
當 `u` 或 `v` 為一基數與一偶數時,`2` 不會再是公因數,所以可以直接除`2`
```c
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
```
最後的部份就是在做輾轉相除法,可以看出當 `v` 為 `0` 時,就是結束,因此 `COND` 為 `v`,而 `RET` 就是 `u << shift`
- [ ] 在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升;
- [ ] Linux 核心中也內建 GCD (而且還不只一種實作),例如 lib/math/gcd.c,請解釋其實作手法和探討在核心內的應用場景。
---
### 測驗 `4`
在影像處理中,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 extension 的 `__builtin_ctzll` 的行為是回傳由低位往高位遇上連續多少個 `0` 才碰到 `1`。
用以改寫的程式碼如下:
```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;
}
```
* 第一反應是 `0x1 << r` 這樣就能做出最低位 `1` 的mask,但 `r` 的宣告在 `t` 之後,所以這不是答案。
* 想到 `__builtin_ctzll` 也是需要找到同樣的mask才能去算出return的值,於是去看了 `__builtin_ctzl` 的實做,如下:
```c
int __builtin_ctzl(unsigned long x) {
int r = 63;
x &= ~x + 1;
if (x & 0x00000000FFFFFFFF) r -= 32;
if (x & 0x0000FFFF0000FFFF) r -= 16;
if (x & 0x00FF00FF00FF00FF) r -= 8;
if (x & 0x0F0F0F0F0F0F0F0F) r -= 4;
if (x & 0x3333333333333333) r -= 2;
if (x & 0x5555555555555555) r -= 1;
return r;
}
```
`x &= ~x + 1` 可以得到mask,而 `~x + 1` 即是 `-x`,因此 `EXP6` 是 `bitwise & -bitwise`
- [ ] 解釋上述程式運作原理,並舉出這樣的程式碼用在哪些真實案例中;
- [ ] 設計實驗,檢驗 ctz/clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 bitmap density;
- [ ] 思考進一步的改進空間;
- [ ] 閱讀 Data Structures in the Linux Kernel 並舉出 Linux 核心使用 bitmap 的案例;
---
### 測驗 `5`
以下是 [LeetCode 166. Fraction to Recurring](https://leetcode.com/problems/fraction-to-recurring-decimal/) Decimal 的可能實作:
```c=
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "list.h"
struct rem_node {
int key;
int index;
struct list_head link;
};
static int find(struct list_head *heads, int size, int key)
{
struct rem_node *node;
int hash = key % size;
list_for_each_entry (node, &heads[hash], link) {
if (key == node->key)
return node->index;
}
return -1;
}
char *fractionToDecimal(int numerator, int denominator)
{
int size = 1024;
char *result = malloc(size);
char *p = result;
if (denominator == 0) {
result[0] = '\0';
return result;
}
if (numerator == 0) {
result[0] = '0';
result[1] = '\0';
return result;
}
/* using long long type make sure there has no integer overflow */
long long n = numerator;
long long d = denominator;
/* deal with negtive cases */
if (n < 0)
n = -n;
if (d < 0)
d = -d;
bool sign = (float) numerator / denominator >= 0;
if (!sign)
*p++ = '-';
long long remainder = n % d;
long long division = n / d;
sprintf(p, "%ld", division > 0 ? (long) division : (long) -division);
if (remainder == 0)
return result;
p = result + strlen(result);
*p++ = '.';
/* Using a map to record all of reminders and their position.
* if the reminder appeared before, which means the repeated loop begin,
*/
char *decimal = malloc(size);
memset(decimal, 0, size);
char *q = decimal;
size = 1333;
struct list_head *heads = malloc(size * sizeof(*heads));
for (int i = 0; i < size; i++)
INIT_LIST_HEAD(&heads[i]);
for (int i = 0; remainder; i++) {
int pos = find(heads, size, remainder);
if (pos >= 0) {
while (PPP > 0)
*p++ = *decimal++;
*p++ = '(';
while (*decimal != '\0')
*p++ = *decimal++;
*p++ = ')';
*p = '\0';
return result;
}
struct rem_node *node = malloc(sizeof(*node));
node->key = remainder;
node->index = i;
MMM(&node->link, EEE);
*q++ = (remainder * 10) / d + '0';
remainder = (remainder * 10) % d;
}
strcpy(p, decimal);
return result;
}
```
* 第13-22行,將 remainder 經過 hash function 後,找尋是否已經存在 `head[hash]`,若有則是代表小數部份已經開始循環,回傳值 pos 是開始循環小數的位置,若無則回傳-1。
* 第51-60行,將結果的正負號串到 `p`,並將結果分為 `remainder` 與 `division`,但最後一行的 `division > 0 ? (long) division : (long) -division` 有點問題,因為 `n` 與 `d` 都是正數,不會出現 `division` 為負的情況,所以要改為 `sprintf(p, "%ld", (long) division)`。
* 第73-75行,將要儲存 hash table 的 array 用 `INIT_LIST_HEAD` 初始化。
* 第79-88行,若 `pos >= 0` 代表已經找到開始重複小數的位置,要先印出沒有重複的部份,`pos` 以前的數字都代表不重複的,`PPP` 是 `pos--`。
* 第89-93行,要將還沒出現在 `heads` 內的點串上去對應的 heads[hash],所以 `MMM` 是 `list_add`,`EEE` 是 `&heads[remainder % size]`。
為了測試 `edge case`,我採用`numerator = 2147483646, remainder = 2147483647`,將參數輸入後執行程式,發現會觸發 `Segmentation fault`,使用GDB debug時發現,有node的位址無法存取,但如果是 `malloc` 失敗,`node->key` 時就會觸發 `Segmentation fault`,有串上list代表執行當下是成功的,但要存取時卻無法存取,還需要再研究。
```c=
Breakpoint 1, find (heads=0x555555559ac0, size=1333, key=1171491056)
at fractionToDecimal.c:16
16 int hash = key % size;
(gdb) next
17 list_for_each_entry (node, &heads[hash], link) {
(gdb) p hash
$7 = 2
(gdb) heads[2]
Undefined command: "heads". Try "help".
(gdb) p heads[2]
$8 = {prev = 0x3937383439363536, next = 0x3234303032343638}
(gdb) p heads[2]->next
$9 = (struct list_head *) 0x3234303032343638
(gdb) p heads[2]->next->next
Cannot access memory at address 0x3234303032343640
(gdb) p heads[2]->prev
$10 = (struct list_head *) 0x3937383439363536
(gdb) p heads[2]->prev->prev
Cannot access memory at address 0x3937383439363536
```
- [ ] 解釋上述程式碼運作原理,指出其中不足,並予以改進
- [ ] 在 Linux 核心原始程式碼的 mm/ 目錄 (即記憶體管理) 中,找到特定整數比值 (即 fraction) 和 bitwise 操作的程式碼案例,予以探討和解說其應用場景
---
### 測驗 `6`
[__alignof__](https://gcc.gnu.org/onlinedocs/gcc/Alignment.html) 是 GNU extension,以下是其可能的實作方式:
```c
/*
* ALIGNOF - get the alignment of a type
* @t: the type to test
*
* This returns a safe alignment for the given type.
*/
#define ALIGNOF(t) \
((char *)(&((struct { char c; t _h; } *)0)->M) - (char *)X)
```
- [ ]在 Linux 核心原始程式碼中找出 __alignof__ 的使用案例 2 則,並針對其場景進行解說
- [ ]在 Linux 核心源使程式碼找出 ALIGN, ALIGN_DOWN, ALIGN_UP 等巨集,探討其實作機制和用途,並舉例探討 (可和上述第二點的案例重複)。思索能否對 Linux 核心提交貢獻,儘量共用相同的巨集
### 測驗 `7`