---
tags: linux2022
---
# 2022q1 Homework2 (quiz2)
contributed by < `ppodds` >
## 2022q1 第 2 週測驗題
### 測驗 1
> 考慮以下對二個無號整數取平均值的程式碼:
> ```cpp
> #include <stdint.h>
> uint32_t average(uint32_t a, uint32_t b)
> {
> return (a + b) / 2;
> }
> ```
> 改寫為
> ```cpp
> #include <stdint.h>
> uint32_t average(uint32_t a, uint32_t b)
> {
> return (a >> 1) + (b >> 1) + (EXP1);
> }
> ```
#### EXP1
第二種寫法相當於
$$\lfloor \dfrac a2 \rfloor + \lfloor \dfrac b2 \rfloor + x$$
很顯然我們要補的就是被floor捨去掉的值。
舉個簡單的例子
$$\dfrac{1+1}2 = 1$$
$$\lfloor \dfrac 12 \rfloor + \lfloor \dfrac 12 \rfloor = 0$$
我們必須在 a 和 b 同時是奇數時補 1 給最終結果。寫法如下:
`(a & 1) & (b & 1)` $\rightarrow$ `a & b & 1`
故 EXP1 為 `a & b & 1`
#### EXP2 & EXP3
> 改寫為
> ```cpp
> uint32_t average(uint32_t a, uint32_t b)
> {
> return (EXP2) + ((EXP3) >> 1);
> }
> ```
>
> 限定使用 OR AND XOR
直覺想到用加法器的概念做
> 加法 $\rightarrow$ XOR
> 進位 $\rightarrow$ AND
先把 `a + b` 拆成用加法器的 bitwise 形式
`a + b = a ^ b + (a & b) << 1`
補上除2答案就出來了
`(a + b) / 2 = (a & b) + (a ^ b) >> 1`
故 EXP2 為 `a & b` , EXP3 為 `a ^ b`
:::success
延伸問題:
1. 解釋上述程式碼運作的原理
2. 比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 CS:APP 第 3 章)
3. 研讀 Linux 核心原始程式碼 include/linux/average.h,探討其 Exponentially weighted moving average (EWMA) 實作,以及在 Linux 核心的應用
> 移動平均(Moving average),又稱滾動平均值、滑動平均,在統計學中是種藉由建立整個資料集合中不同子集的一系列平均數,來分析資料點的計算方法。
移動平均通常與時間序列資料一起使用,以消除短期波動,突出長期趨勢或周期。短期和長期之間的閾值取決於應用,移動平均的參數將相應地設置。例如,它通常用於對財務數據進行技術分析,如股票價格、收益率或交易量。它也用於經濟學中研究國內生產總值、就業或其他宏觀經濟時間序列。
:::
#### 延伸2
> 比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 CS:APP 第 3 章)
`avg.c`
```c
#include <stdint.h>
#include <stdio.h>
uint32_t average_1(uint32_t a, uint32_t b)
{
return (a + b) / 2;
}
uint32_t average_2(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (a & b & 1);
}
uint32_t average_3(uint32_t a, uint32_t b)
{
return (a & b) + ((a ^ b) >> 1);
}
int main()
{
int a = 1, b = 1;
printf("%d\n", average_1(a, b));
printf("%d\n", average_2(a, b));
printf("%d\n", average_3(a, b));
return 0;
}
```
編譯指令 `gcc avg.c -O2 -o avg`
`objdump -D -M intel avg > avg.dump`
> 先備知識
> rdi 和 rsi 是 gcc x64 calling convention 的前兩個參數
```
00000000000011c0 <average_1>:
11c0: f3 0f 1e fa endbr64
11c4: 8d 04 37 lea eax,[rdi+rsi*1]
11c7: d1 e8 shr eax,1
11c9: c3 ret
11ca: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0]
```
> lea 將後者 address 存的值放到前者之中,且允許後者可以進行暫存器運算
根據上面的解釋,可以看出第一行其實在執行 `a + b` 並把結果存入 `eax` 中。下一行的 `shr eax,1` 即是 `eax / 2` 最終做出 `(a + b) / 2`
```
00000000000011d0 <average_2>:
11d0: f3 0f 1e fa endbr64
11d4: 89 f8 mov eax,edi
11d6: 89 f2 mov edx,esi
11d8: 21 f7 and edi,esi
11da: d1 e8 shr eax,1
11dc: d1 ea shr edx,1
11de: 83 e7 01 and edi,0x1
11e1: 01 d0 add eax,edx
11e3: 01 f8 add eax,edi
11e5: c3 ret
11e6: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
11ed: 00 00 00
```
`mov eax,edi` 、 `mov edx,esi` 先各做了一份 a 和 b 變數的備份,待會算 `a & b & 1` 會用到。接下來直接做 `a & b` 存到 `edi` 中,並執行 `a >> 1` 與 `b >> 1`。此時才會再把 `edi & 1` (在此可以看出 compiler 在優化時有重排我們的程式碼),最後把前面計算出來的東西用 `add` 加起來。
```
00000000000011f0 <average_3>:
11f0: f3 0f 1e fa endbr64
11f4: 89 f8 mov eax,edi
11f6: 21 f7 and edi,esi
11f8: 31 f0 xor eax,esi
11fa: d1 e8 shr eax,1
11fc: 01 f8 add eax,edi
11fe: c3 ret
11ff: 90 nop
```
先備份 a 到 `eax` 後把 `edi` 設為 `a & b` ,再把 `eax` 設為 `(a ^ b) / 2`,最終把兩個結果加在一起。
#### 延伸3
> 研讀 Linux 核心原始程式碼 include/linux/average.h,探討其 Exponentially weighted moving average (EWMA) 實作,以及在 Linux 核心的應用
[EWMA介紹](https://corporatefinanceinstitute.com/resources/knowledge/trading-investing/exponentially-weighted-moving-average-ewma/)
大致上可以理解為一個簡單的動態平均系統,一開始先設置一個權重,之後每次計算時根據權重和新進來的數值做加權平均。
`include/linux/average.h` 定義了一個大micro `DECLARE_EWMA(name, _precision, _weight_rcp)` 。可讓 user 根據傳入的參數產生 EWMA 的 struct 和操作函式。分別為 `ewma_##name` (struct) 、`ewma_##name##_init` 、 `ewma_##name##_read` 、 `ewma_##name##_add` 。
```c
#define DECLARE_EWMA(name, _precision, _weight_rcp) \
struct ewma_##name { \
unsigned long internal; \
}; \
static inline void ewma_##name##_init(struct ewma_##name *e) \
{ \
BUILD_BUG_ON(!__builtin_constant_p(_precision)); \
BUILD_BUG_ON(!__builtin_constant_p(_weight_rcp)); \
/* \
* Even if you want to feed it just 0/1 you should have \
* some bits for the non-fractional part... \
*/ \
BUILD_BUG_ON((_precision) > 30); \
BUILD_BUG_ON_NOT_POWER_OF_2(_weight_rcp); \
e->internal = 0; \
} \
static inline unsigned long \
ewma_##name##_read(struct ewma_##name *e) \
{ \
BUILD_BUG_ON(!__builtin_constant_p(_precision)); \
BUILD_BUG_ON(!__builtin_constant_p(_weight_rcp)); \
BUILD_BUG_ON((_precision) > 30); \
BUILD_BUG_ON_NOT_POWER_OF_2(_weight_rcp); \
return e->internal >> (_precision); \
} \
static inline void ewma_##name##_add(struct ewma_##name *e, \
unsigned long val) \
{ \
unsigned long internal = READ_ONCE(e->internal); \
unsigned long weight_rcp = ilog2(_weight_rcp); \
unsigned long precision = _precision; \
\
BUILD_BUG_ON(!__builtin_constant_p(_precision)); \
BUILD_BUG_ON(!__builtin_constant_p(_weight_rcp)); \
BUILD_BUG_ON((_precision) > 30); \
BUILD_BUG_ON_NOT_POWER_OF_2(_weight_rcp); \
\
WRITE_ONCE(e->internal, internal ? \
(((internal << weight_rcp) - internal) + \
(val << precision)) >> weight_rcp : \
(val << precision)); \
}
```
從上面的 code 可觀察出 linux 在 compile time 時就有對巨集傳入的參數做檢查,來避免 runtime 時的錯誤。
> ##name##會被代換成巨集的 name 參數
`ewma_##name##_init`
初始化 struct 的內容(把 internal 設成 0)
`ewma_##name##_read`
讀取 EWMA 當前的數值
`ewma_##name##_add`
加入新的一筆資料進到 EWMA 中
> `read` 和 `add` 剛好對應到 EWMA 的基本操作
---
### 測驗2
> 改寫〈解讀計算機編碼〉一文的「不需要分支的設計」一節提供的程式碼 min,我們得到以下實作 (max):
> ```cpp
> #include <stdint.h>
> uint32_t max(uint32_t a, uint32_t b)
> {
> return a ^ ((EXP4) & -(EXP5));
> }
> ```
由於題目有一點難度,需要慢慢把思考過程寫下來。
首先從EXP5開始猜測, EXP5 是一個 a 和 b 的比較,意思是只會傳回 `true` 或 `false`,即 `1` 或 `0` 。經過負號轉換後 `1` 變為 `-1` ,即 `0xFFFFFFFF` 。到此為止可以稍微有點頭緒, `(EXP4) & -(EXP5)` 在 EXP5 的條件滿足時,會把 EXP4 的數值完整留下來,否則結果會是 `0` 。當此敘述為 `0` 時,經過和 `a` 的 XOR 運算,結果會是 `a`。固可先猜測 EXP5 在 `a < b` 時會滿足。
接下來考慮 EXP5 滿足的情形。當 EXP5 滿足時,我們需要把 b 作為最終結果回傳回去。簡化後可以看成 `a ^ (EXP4)` 要等於 `b` 。回想 XOR 的特性,某數對同樣數字 XOR 兩次以後會得到自己,因此可猜測 EXP4 為 `a ^ b` 。
`EXP4` = `a ^ b`
`EXP5` = `a < b`
:::success
延伸問題:
1. 解釋上述程式碼運作的原理
2. 針對 32 位元無號/有號整數,撰寫同樣 branchless 的實作
3. Linux 核心也有若干 branchless / branch-free 的實作,例如 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 檢索。
:::
#### 延伸2
> 針對 32 位元無號/有號整數,撰寫同樣 branchless 的實作
```cpp
#include <stdint.h>
int32_t max(int32_t a, int32_t b)
{
return a ^ ((a ^ b) & -(a < b));
}
```
在這裡只要 `a` 和 `b` 同時是有號數或無號數結果就會正確(不一樣的話會造成 `a < b` 的比較與預期不同)。
---
### 測驗3
>考慮以下 64 位元 GCD (greatest common divisor, 最大公因數) 求值函式:
>
> ```cpp
> #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;
> }
> ```
> 改寫為以下等價實作:
> ```cpp
> #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. if both x, y are 0, $gcd(0,0) = 0$
> 2. $gcd(x,0)=x$ and $gcd(0,y)=y$
剛好對應上
```c
if (!u || !v) return u | v;
```
> 3. if x, y are both even, $gcd(x,y)=2*gcd(\frac{x}{2},\frac{y}{2})$
```c
for (shift = 0; !((u | v) & 1); shift++) {
u /= 2, v /= 2;
}
```
只要 `u` 和 `v` 都是偶數,就除二並把 `shift` 加 `1` (此處用 last bit 是否等於 `1` 來判斷奇偶)。當迴圈結束時,會剛好達成 $gcd(u,v)=2^{shift}*gcd(\dfrac u {2^{shift}},\dfrac v{2^{shift}})$ (此處的 `u` 和 `v` 指的是函式剛傳進來時的值)。
> 4. If x is even and y is odd, then $gcd(x,y)=gcd(\frac{x}{2},y)$
> 5. If x is odd and y is even, then $gcd(x,y)=gcd(x,\frac{y}{2})$
此時已經不能再提出 `2` 的公因數了,因此可以把 `u` 和 `v` 都除 `2` 到變成奇數為止。
```c
while (!(u & 1))
u /= 2;
```
```c
while (!(v & 1))
v /= 2;
```
對到最後的剩下的兩條。
最後的 do while 迴圈中止條件和一般的 gcd 一樣是 `v=0`,故 `COND = v`,而回傳值如上面所述, `RET = u << shift`
`COND = v`
`RET = u << shift`
#### 延伸2
> 在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升
#### 延伸3
> Linux 核心中也內建 GCD (而且還不只一種實作),例如 lib/math/gcd.c,請解釋其實作手法和探討在核心內的應用場景。
---
### 測驗4
> 考慮以下程式碼:
> ```cpp
> #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;
> }
> ```
> 改寫的程式碼如下:
> ```cpp=
> #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 為 000101b,那 t 就會變成 000001b,接著就可以透過 XOR 把該位的 1 清掉,其他保留 (此為 XOR 的特性)。
> 請補完 EXP6
`-bitset` 會將 `bitset` 最右邊的 `1` 前方的所有 bits 都翻轉(二補數),和 `bitset` 做 AND 後就會只剩下最右邊的 1 的 bit。故 `EXP6=bitset & -bitset` 。
`EXP6=bitset & -bitset`
#### 延伸1
> 解釋上述程式運作原理,並舉出這樣的程式碼用在哪些真實案例中
#### 延伸2
> 設計實驗,檢驗 ctz/clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 bitmap density
#### 延伸3
> 思考進一步的改進空間
#### 延伸4
> 閱讀 Data Structures in the Linux Kernel 並舉出 Linux 核心使用 bitmap 的案例
---
### 測驗5
> 以下是 LeetCode 166. [Fraction to Recurring Decimal](https://leetcode.com/problems/fraction-to-recurring-decimal/) 的可能實作:
> ```cpp
> #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;
>}
>```
> 請補完
> PPP = ? (表示式)
> MMM = ? (巨集)
> EEE = ? (表示式)
`PPP=`
`MMM=`
`EEE=`
---
### 測驗6
> __alignof__ 是 GNU extension,以下是其可能的實作方式:
> ```cpp
> /*
> * 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)
> ```
> 請補完上述程式碼,使得功能與 __alignof__ 等價。
> 這題的答案我不確定
`M=_h`
`X=0`
將 `0` 轉型成 `struct { char c; t _h; }` 的 pointer,並取得 `-h` 的 address,會拿到 `-h` 在結構中的 offset,再扣掉一開始 struct base address 的位置。
> 不解的是為什麼要扣掉 base address ,如果 base address 已經是 0 了應該不需要再扣一次。
#### 延伸2
> 在 Linux 核心原始程式碼中找出 __alignof__ 的使用案例 2 則,並針對其場景進行解說
#### 延伸3
> 在 Linux 核心源使程式碼找出 ALIGN, ALIGN_DOWN, ALIGN_UP 等巨集,探討其實作機制和用途,並舉例探討 (可和上述第二點的案例重複)。思索能否對 Linux 核心提交貢獻,儘量共用相同的巨集
---
### 測驗7
> 考慮貌似簡單卻蘊含實作深度的 FizzBuzz 題目:
>
> - 從 1 數到 n,如果是 3的倍數,印出 “Fizz”
> - 如果是 5 的倍數,印出 “Buzz”
> - 如果是 15 的倍數,印出 “FizzBuzz”
> - 如果都不是,就印出數字本身
>
> 以下是利用 bitwise 和上述技巧實作的 FizzBuzz 程式碼: (bitwise.c)
> ```cpp
> static inline bool is_divisible(uint32_t n, uint64_t M)
> {
> return n * M <= M - 1;
> }
>
> static uint64_t M3 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 3 + 1;
> static uint64_t M5 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 5 + 1;
>
> int main(int argc, char **argv)
> {
> for (size_t i = 1; i <= 100; i++) {
> uint8_t div3 = is_divisible(i, M3);
> uint8_t div5 = is_divisible(i, M5);
> unsigned int length = (2 << KK1) << KK2;
>
> char fmt[9];
> strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (KK3)], length);
> fmt[length] = '\0';
>
> printf(fmt, i);
> printf("\n");
> }
> return 0;
> }
> ```
> 作答區
> KK1 = ? (變數名稱)
> KK2 = ? (變數名稱)
> KK3 = ? (表示式,不包含小括號)
`KK1=div3`
`KK2=div5`
`KK3=div3 << 2`
原理是透過位元運算湊出 `fmt` (要輸出的字串)數值
`fmt` 的決定方法是由 `&"FizzBuzz%u"[(9 >> div5) >> (KK3)]` 和 `length` 共同控制。第一項是字串的開頭,後面是持續長度,列出表以後可以求出上面三條式子。
`9` 怪怪的,要改成 `8`,不然吃不到 `%u` ,最後印出來的是 `u`
#### 延伸1
> 解釋上述程式運作原理並評估 naive.c 和 bitwise.c 效能落差
> - 避免 stream I/O 帶來的影響,可將 printf 更換為 sprintf
#### 延伸2
> 分析 Faster remainders when the divisor is a constant: beating compilers and libdivide 一文的想法 (可參照同一篇網誌下方的評論),並設計另一種 bitmask,如「可被 3 整除則末位設為 1」「可被 5 整除則倒數第二位設定為 1」,然後再改寫 bitwise.c 程式碼,試圖運用更少的指令來實作出 branchless;
> - 參照 fastmod: A header file for fast 32-bit division remainders on 64-bit hardware
#### 延伸3
> 研讀 The Fastest FizzBuzz Implementation 及其 Hacker News 討論,提出 throughput (吞吐量) 更高的 Fizzbuzz 實作
#### 延伸4
> 解析 Linux 核心原始程式碼 kernel/time/timekeeping.c 裡頭涉及到除法運算的機制,探討其快速除法的實作 (注意: 你可能要對照研讀 kernel/time/ 目錄的標頭檔和程式碼)
過程中,你可能會發現可貢獻到 Linux 核心的空間,請充分討論