--- tags: linux kernel, linux2022 --- # 2022q1 Homework2 (quiz2) contributed by < [`linjohnss`](https://github.com/linjohnss) > > [測驗題目](https://hackmd.io/@sysprog/linux2022-quiz2) ## 測驗 1 `無號整數取平均值` ### 程式運作原理 兩個無號整數取平均,代表在實做過程中我們不需要考慮 sign bit 的問題,但仍需考慮 overflow 的問題。 #### 第一個等價實作 ```c #include <stdint.h> uint32_t average(uint32_t a, uint32_t b) { return (a >> 1) + (b >> 1) + (EXP1); } ``` 從上述程式法可以看到它先將 `a` 與 `b` 右移一個位元,代表分別將 `a` 與 `b` 除 2。接著將兩者加在一起。 ```c (a >> 1) + (b >> 1) ``` 再來,我們需要考慮 `a`、`b` 是否為奇數相加,若果都是奇數則需考慮進位。 因此,`EXP1` 可以寫成 `a & b & 1` 來處理奇數的進位。 ```c (a >> 1) + (b >> 1) + (a & b & 1) ``` 我們可以透過帶入數字來演示 ``` a = 1001 (9) b = 1111 (15) a >> 1 = 0100 (4) b >> 1 = 0111 (7) 0100 + 0111 = 1011 (11) 缺少 last bit 的進位 a & b & 1 = 0001 都為 1 (奇數)故需要進位 1011 + 0001 = 1100 (12) ``` #### 第二個等價實做 ```c uint32_t average(uint32_t a, uint32_t b) { return (EXP2) + ((EXP3) >> 1); } ``` 題目這邊要求只能使用 `AND`、`OR`、`XOR` 來實作 若[用 c 來實做半加器](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/) `SUM = (a ^ b)` `CIN = (a & b)` (CIN 需要 *2 達到進位) `a + b = (a ^ b) + ((a & b) << 1)` ```graphviz digraph "Half-Adder" { rankdir=LR; AND[label="AND" shape=box]; XOR[label="XOR" shape=box]; a -> XOR b -> XOR a -> AND b -> AND XOR -> SUM AND -> CIN } ``` 接著將 `a + b` 除以 2,可以推得 `(a + b) >> 1= (a ^ b) >> 1 + (((a & b) << 1) >> 1) = (a ^ b) >> 1 + (a & b)` 因此,得知 `EXP2 = a & b` 以及 `EXP3 = a ^ b`。 ```c (a & b) + ((a ^ b) >> 1) ``` ### 開啟編譯器最佳化 編譯器版本:`gcc version 9.3.0` 使用 `-O3` 最高級別優化 `gcc -S -O3 interger_average.c` #### 第一個等價實作 優化前: ```c average1: .LFB0: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, -4(%rbp) movl %esi, -8(%rbp) movl -4(%rbp), %eax shrl %eax movl %eax, %edx movl -8(%rbp), %eax shrl %eax addl %eax, %edx movl -4(%rbp), %eax andl -8(%rbp), %eax andl $1, %eax addl %edx, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc ``` 優化後: ```c average1: .LFB0: .cfi_startproc endbr64 movl %edi, %eax movl %esi, %edx andl %esi, %edi shrl %eax shrl %edx andl $1, %edi addl %edx, %eax addl %edi, %eax ret .cfi_endproc ``` #### 第二個等價實作 優化前: ```c average2: .LFB1: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, -4(%rbp) movl %esi, -8(%rbp) movl -4(%rbp), %eax andl -8(%rbp), %eax movl %eax, %edx movl -4(%rbp), %eax xorl -8(%rbp), %eax shrl %eax addl %edx, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc ``` 優化後: ```c average2: .LFB1: .cfi_startproc endbr64 movl %edi, %eax andl %esi, %edi xorl %esi, %eax shrl %eax addl %edi, %eax ret .cfi_endproc ``` ### Linux 核心原始程式碼 > 指數移動平均(英語:exponential moving average,EMA或EWMA)是以==指數式==遞減加權的移動平均。 各數值的加權影響力隨時間而指數式遞減,越近期的數據加權影響力越重,但較舊的數據也給予一定的加權值。 -維基百科 在 [include/linux/average.h](https://github.com/torvalds/linux/blob/master/include/linux/average.h) 定義了一個 macro,其參數包含: - `name`(用於定義 struct 與 helper functions 名稱) - `_precision`(用於定義固定精度值的小數位數) - `_weight_rcp`(權重,用於確定新資料與舊資料的加權方式) 首先,定義一個開頭為 `ewma_` 的結構,內部包含一個 `unsigned long internal`。 ```c struct ewma_##name { unsigned long internal; }; ``` 使用前的初始化,檢查各項參數,並令 `e->internal = 0`。這能使其在 compile 時期就作檢查。 ```c 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; } ``` `e->internal >> (_precision)` 也就是讀取 `internal` 的整數值。 ```c 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); } ``` 此函數為輸入 `unsigned long val` 並計算 `e->internal` 的值。 其中 `weight_rcp` 為 $log_2$(_weight_rcp),因為 EWMA 的權值是利用指只函數計算。 ```c 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)); } ``` 根據公式 internal = 1/weight_rcp * val + (1 - 1/weight_rcp) * internal 用位移的方式去實做。 ```c internal = (((internal << weight_rcp) - internal) + (val << precision)) >> weight_rcp ``` ## 測驗 2 `不需要分支的 Max` ### 程式運作原理 #### 分析[不需要分支的設計的 Min](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) 找到兩個**有號整數**的最校值 ```c int32_t min(int32_t a, int32_t b) { int32_t diff = (a - b); return b + (diff & (diff >> 31)); } ``` 1. 計算差值 `diff` ```c nt32_t diff = (a - b); ``` 2. 找到差值得 sign bit ```c (diff >> 31) ``` 3. 判斷 sign bit 正負 - 正(`a > b`):`b` 即為最小值。 - 負(`b > a`):`b` 要加上 `diff`,才會變成 `a`。 ```c b + (diff & (diff >> 31)) ``` :::warning :warning: 算術位移 (Arithmetic shift) : 補上號數 (sign bit) 也就是最高有效位元的值在左側 ::: #### 兩個無號整數的最大值 找到兩個**無號整數**的最大值,達到 branchless 的實做。 ```c #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((EXP4) & -(EXP5)); } ``` 首先,我們可以根據 `a ^ ((EXP4) & -(EXP5))` 得知當 `a > b` 時 `((EXP4) & -(EXP5))` 應該要是 0。 反之,當 `a < b` 時,`((EXP4) & -(EXP5))` 應該要是 `a ^ b`。 - a >= b &rArr; a ^ 0 - a < b &rArr; a ^ (a ^ b) 題目提示 `EXP4` 為 bitwise 操作,所以 `EXP4` 應該是 `a ^ b`。 ```c a ^ ((a ^ b) & -(EXP5)) // EXP4 = a ^ b ``` 題目提示 `EXP5` 為 logical operator 操作,代表結果只可能為 0 或 1。 然後我們可以看到他是先作負號運算 - -0 = 0x00000000 - -1 = 0xFFFFFFFF 接著與 `a ^ b` 作 AND 運算,因此當 `a >= b` 時,`EXP5` 要等於 `0x00000000`,反之要等於 `0xFFFFFFFF`。將此寫法在精簡,故 `EXP5 = a < b`。 ```c a ^ ((a ^ b) & -(a > b)) // EXP5 = a > b ``` ### 32 位元有號整數的最大值 ```c int32_t max(int32_t a, int32_t b) { return a ^ ((a ^ b) & -(a < b)); } ``` 實做後發現,因為 `-(a < b)` 不論 sign 或 unsign 其結果都是 `0x00000000` 或 `0xFFFFFFFF` 其中之一,故可以用相同方法實做。 ### 找出Linux 核心原始程式碼中更多類似的實作手法 ## 測驗 3 `64位元 GCD` ### 程式運作原理 #### GCD 演算法運作原理 總是拿比較大的數值取餘數(相減)比較小的數值,直到其中一個數值為 0 時,另一個數值就是最大公因數。 1. 如果 `u, v` 都是 0 的話,其最大公因數必為 0。 - $gcd(0, 0) = 0$ 2. 如果 `u, v` 其中一個為 0 的話,其最大公因數必為不為 0 的那個數。 - $gcd(u, 0) = u$ - $gcd(0, v) = v$ 3. 接著將 `v` 當作除數; `u` 當作被除數,作 `u % v`。 4. 令餘數(`u % v`)為除數`v`,令原本的除數 `t` 為 `u`。 5. 重複 2, 3 直到 `v = 0`,`u` 即為最大公因數。 ```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; } ``` #### 改寫等價實做 這裡將原本的 Modulo Operator,改為用其他 Operator 實作。 ```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; } ``` 先計算兩個數字的 2 指數最大公因數。 - if u, v are both even, $gcd(u, v) = 2 * gcd(\frac{u}2, \frac{v}2)$ ```c for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; } ``` 因為我們已經找到公因數 $2^{shift}$ ,後續不會在遇到公因數 2,因此可以先將 2 除掉。 `!(u & 1)` 是用於判斷是否為偶數。 ```c while (!(u & 1)) u /= 2; ``` 進入迴圈後,同樣先將 `v` 變為奇數。 ```c while (!(v & 1)) v /= 2; ``` 根據輾轉相除法的演算法,這裡將取餘數改成用連續相減來達成。 - `u, v` 相減直到 `u > v` 時交換(此時 v 為餘數)。 - 迴圈條件應該要是餘數為 0 時候跳出,故 `COND = v`。 ```c do { while (!(v & 1)) v /= 2; if (u < v) { v -= u; } else { uint64_t t = u - v; u = v; v = t; } } while (COND); ``` 而最後回傳的值要在乘上先前除掉的 $2^{shift}$,所以 `RET = u << shift` ```c return RET; ``` ### 透過 __builtin_ctz 改寫 GCD #### int __builtin_ctz (unsigned int x) > Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined. 回傳 interger x 最末端 0 的個數。 因此,我們可以從上述程式法發現`__builtin_ctz` 可以用於偶數除法的部份。 ```c for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; } ``` ```c while (!(u & 1)) u /= 2; ``` ```c while (!(v & 1)) v /= 2; ``` #### 等價實作 ```c uint64_t gcd64(uint64_t u, uint64_t v) { if (!u || !v) return u | v; int ctz_u = __builtin_ctz(u); int ctz_v = __builtin_ctz(v); int shift = ctz_u > ctz_v ? ctz_v : ctz_u; u >> shift; v >> shift; while (!(u & 1)) u /= 2; do { v >> __builtin_ctz(v); if (u < v) { v -= u; } else { uint64_t t = u - v; u = v; v = t; } } while (v); return u << shift; } ``` #### 分析效能差異 ### Linux 核心的 GCD 實作 ## 測驗 4 `bit array` ### 程式運作原理 此函式的功能是將一個長度為 `bitmapsize` 的 64 位元無號數陣列的所有 1 記錄下來,並用陣列 `out` 將這些 1 依序記錄下來,最後回傳 1 的數量。 ```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; } ``` 為了找到陣列中的每一個 1,它利用 for 迴圈一個一個慢慢找,然而我們可以用 `__builtin_ctzll` 來找到最低位元 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; } ``` 首先,我們看到 `t` 會在後面與 `bitset`作 XOR 運算,代表其作用可能是清掉 `bitset` 的 1。因此,我們可以得知 `EXP6` 應該是取得最低位元 1 的位置,故 `EXP6 = bitset & -bitset`。 ## 測驗 5 `Fraction to Recurring Decimal` > leetcode [166. Fraction to Recurring Decimal](https://leetcode.com/problems/fraction-to-recurring-decimal/) ### 程式運作原理 #### 程式規則 計算一個分數的十進制成果。 * 若相除後有循環小數,則需 enclose 重複的部份 ``` Input: numerator = 4, denominator = 333 Output: "0.(012)" ``` #### 用 linux list.h 實作 使用 linux kernel 的 list API 來實做 hash table。 ```c struct rem_node { int key; int index; struct list_head link; }; ``` `find` 這個 function 會根據 `key` 找到 hash table 中對應的 `key` 是否存在,若不存在則回傳 `-1`。 ```c 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; } ``` 若分子或分母任意一個為 0 則回傳 0。 ```c if (denominator == 0) { result[0] = '\0'; return result; } if (numerator == 0) { result[0] = '0'; result[1] = '\0'; return result; } ``` 改用 long long 型態來處理,避免 overflow。 ```c /* using long long type make sure there has no integer overflow */ long long n = numerator; long long d = denominator; ``` 先處理負號的情況,若其中一個為負,則在開頭加上 ' - ',後續都以正數處理。 ```c /* deal with negtive cases */ if (n < 0) n = -n; if (d < 0) d = -d; bool sign = (float) numerator / denominator >= 0; if (!sign) *p++ = '-'; ``` 這裡處理除法以及取餘數的部份,若不是整除 (`remainder !=0`),則代表要處理小數部份,反之可以直接回傳相除的成果。 ```c 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++ = '.'; ``` 接來就是確認餘數(`remainder`)是否為循環小數 - 先從 hash table 找尋 `remainder` 是否存在 - 若不存在(pos = -1),則將 `remainder` 加到 hash table 中,並紀錄 `i`(位數)。 - MMM = `list_add` - EEE = `&heads[remainder % size]` - 若存在(pos >= 0),代表有循環小數,往回尋找重複的位數,加入括號 - PPP = `pos--` - 計算 `(remainder * 10) / d`,並將商加到 `q` 中 - 令 `remainder` 為餘數,重複上述動作直到 `remainder` 為 0 ```c 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; } ``` ## 測驗 6 `__alignof__` ### 程式運作原理 #### alignof 功能 `__alignof__` 的功能是能夠指定物件在記憶體中的對齊,將資料儲存在資料大小倍數的位址時,可以改善 cache 的效能。 #### 等價實做 我們可以利用 `struct` 的對齊性質來實作 `__alignof__`。首先,可以看到 `struct` 內部包含一個 `char c` 以及一個 `t _h`,目的是找到型別 `t` 對齊時所需的位元大小,因此`M = _h, X = 0` 計算頭尾相減即為答案。 ```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) ``` ## 測驗 7 `FizzBuzz`