--- tags: linux2022 --- # 2022q1 Homework (quiz2) contributed by < `hankluo6` > > [第 2 週測驗題](https://hackmd.io/@sysprog/linux2022-quiz2) ## 測驗 `1` ### 運作原理 將兩數 $x, y$ 分成,整數部分及小數部分: $$ \begin{equation} \frac{x}{2}=\lfloor{\frac{x}{2}}\rfloor+frac(\frac{x}{2}) \\ \frac{y}{2}=\lfloor{\frac{y}{2}}\rfloor+frac(\frac{y}{2}) \\ \end{equation} $$ 就能列出算式: $$ \begin{equation} \begin{aligned} avg(x, y) &= \lfloor \frac{x+y}{2}\rfloor \\ &= \lfloor \lfloor{\frac{x}{2}}\rfloor + \lfloor{\frac{y}{2}}\rfloor + frac(\frac{x}{2}) + frac(\frac{y}{2}) \rfloor \\ &= \lfloor{\frac{x}{2}}\rfloor + \lfloor{\frac{y}{2}}\rfloor + \lfloor frac(\frac{x}{2}) + frac(\frac{y}{2}) \rfloor \end{aligned} \end{equation} $$ $$ \lfloor frac(\frac{x}{2})+frac(\frac{y}{2}) \rfloor= \begin{cases} 1, & \text{if $x$ and $y$ are odd} \\ 0, & \text{else} \end{cases} $$ 求出 $x$ 與 $y$ 皆為奇數時需要加一,`EXP1 = a & b & 1`。 根據加法器原理可得:$a + b=a \oplus b +(a \land b)<<1$ 所以:$\begin{equation} (a+b)>>1 = (a \oplus b)>>1 + a \land b \end{equation}$ 得出 `EXP2 = a & b`,`EXP3 = a ^ b`。 ### 對應組合語言輸出 * Target: x86_64-w64-mingw32 * Compiler: gcc ```c uint32_t averagev1(uint32_t low, uint32_t high) movl %edx, %eax // $eax = high subl %ecx, %eax // $eax = high - low shrl %eax // $eax = (high - low) / 2 addl %ecx, %eax // $eax = low + (high - low) / 2 ret uint32_t averagev2(uint32_t low, uint32_t high) movl %ecx, %eax // $eax = high movl %edx, %r8d // $r8d = low andl %edx, %ecx // $ecx = low & high shrl %eax // $eax = high >> 1 shrl %r8d // $r8d = low >> 1 andl $1, %ecx // $ecx = low & high ^ 1 addl %r8d, %eax // $eax = low >> 1 + high >> 1 addl %ecx, %eax // $eax = low >> 1 + high >> 1 + (a & b & 1) ret uint32_t averagev3(uint32_t low, uint32_t high) movl %ecx, %eax // $eax = high andl %edx, %ecx // $ecx = low & high xorl %edx, %eax // $eax = low ^ high shrl %eax // $eax = (low ^ high) >> 1 addl %ecx, %eax // $eax = low & high + (low ^ high) >> 1 ret ``` TODO ### EWMA ```c #define DECLARE_EWMA(name, _precision, _weight_rcp) \ struct ewma_##name { \ unsigned long internal; \ }; ... ``` * name: 決定宣告的 structure 和 function 名稱 * \_precision: 決定需要用幾個 bit 來表示[定點數小數](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) * \_weight_rcp: EWMA 中的 $\frac{1}{\alpha}$ ```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; \ } ``` 檢查輸入的數值是否合法,`_precision` 及 `_weight_rcp` 必須為編譯時期能決定的常數、`_precision` 不能大於 30、_weight_rcp` 須為 2 的倍數,因為 `add` 是用 shift 實作,最後初始化 `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); \ } ``` `read` 數值時沒有將小數部分印出。 ```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` 的數值分成兩部分: * `internal > 0`: $$ \begin{equation} \begin{aligned} internal &= \frac{(internal \times \frac{1}{\alpha} - internal) + (val \times 2^{precision})}{\frac{1}{\alpha}} \\ &= \frac{(internal \times (\frac{1}{\alpha} - 1)) + (val \times 2^{precision})}{\frac{1}{\alpha}} \\ &= (internal \times (1 - \alpha)) + (val \times 2^{precision})\times \alpha \end{aligned} \end{equation} $$ * `internal <= 0` $$ \begin{equation} \begin{aligned} internal = val \times 2^{precision} \\ \end{aligned} \end{equation} $$ --- ## 測驗 `2` ### 運作原理 ```c #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((EXP4) & -(EXP5)); } ``` `EXP5` 為 boolean 運算,可知結果只可能為 `0x0000` 及 `0xFFFF`,當為 `0x0000` 時,回傳 `a ^ 0 = a`,當為 `0xFFFF` 時,回傳 `a ^ EXP4`。 所以當 `a > b` 時,`EXP5 = 1` 才能滿足條件,得出 `EXP5 = a > b`。 而當 `b >= a` 時,`a ^ EXP4 = b`,可得 `EXP4 = a ^ b` (根據 XOR 的特性 `a ^ a = 0`)。 ### 32 位元有號數 branchless 實作 改寫〈[解讀計算機編碼](https://hackmd.io/@sysprog/binary-representation)〉一文的「不需要分支的設計」 ```c int32_t max(int32_t a, int32_t b) { int32_t diff = (a - b); return b + (diff & ~(diff >> 31)); } ``` 只需在判斷大小的 `diff >> 31` 加上 `NOT` 運算,相反大小判斷便能改成回傳 `max`。 這實作僅在 `INT32_MIN` <= (a - b) <= `INT32_MAX` 成立時,才會發揮作用。 --- ## 測驗 `3` ### 運作原理 根據[輾轉相除法](https://en.wikipedia.org/wiki/Euclidean_algorithm) 的演算法可求出答案: * `COND = v` * `RET = u << shift` 在每次回圈中,持續保持 `v` 為最小值,當 `v` 為 0 時,表示無法再提出公因數,便跳出迴圈。而在函式剛開始時,透過迴圈將最大公因數的 2 的次方提出,記錄在 `shift` 當中,故最後回傳時需再將次方乘回去。 ### 透過 `__builtin_ctz` 改寫 GCD * 在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升; ```c #include <stdint.h> uint64_t gcd64(uint64_t u, uint64_t v) { if (!u || !v) return u | v; int shift; int a = __builtin_ctzl(u), b = __builtin_ctzl(v); shift = b ^ ((a ^ b) & -(a < b)); u >>= shift, v >>= shift; int bit; if (bit = __builtin_ctzl(u)) u >>= bit;; do { if (bit = __builtin_ctzl(v)) v >>= bit; if (u < v) { v -= u; } else { uint64_t t = u - v; u = v; v = t; } } while (v); return u << shift; } ``` 效能分析: 隨機產生 64 bit 的資料,並測量 1000000 次的運行時間 | gcd64 | __builtin_ctz gcd64 | |:----------:|:-------------------:| | 0.667711 s | 0.425873 s | 可發現效能有有效的提昇,約提昇 $\frac{0.667711-0.425873}{1000000} = 2.4 * 10^{-7}$ 秒。 - [2020q3 quiz3](https://hackmd.io/@hankluo6/B17jPGv8D#%E5%BB%B6%E4%BC%B8%E5%95%8F%E9%A1%8C3) --- ## 測驗 `4` ### 運作原理 根據提示找出目前最低位元的 1 即為 `bitset & (-bitset)`,因為 `-bitset = ~bitset + 1`,`~bitset` 將所有位元反轉,`+1` 會將位元為 1 的部分 (原本為 0) 往 MSB 進位,直到位元為 0 (原本為 1) 的位元,AND 運算後該位元為唯一與 `bitset` 相同的位元,故能找到最低位元的 1。 ### `ctz/clz` 效能比較 ![](https://i.imgur.com/XPEr7E8.png) 其中 bit ratio 為 bitmap 中為 bit 為 `true` 的比率,可以發現原本的方法不管 bit 數多寡,其運行效率大致相同;而使用 `ctz` 的方法會受到 bit 數量,當 bit 數量越多,就執行越多次 `__builtin_ctzll`,在 bit 比率大約 50% 時為分水嶺。故可以得出結論:bit 比率小於 50% 時 `__builtin_ctzll` 效率較好,反之則為原本的方法較好。 --- ## 測驗 `5` ### 運作原理 根據[長除法](https://en.wikipedia.org/wiki/Long_division)演算法,可利用當前餘數 $\times 10$ 求出下個位數的數字及對應的餘數。程式內的 `char *p` 即指向一個 1024 bytes 空間的指標,在迴圈內會紀錄對應的商。 當出現重複的小數數值時,表示構成循環小數,便能直接從 hash table 中取出剩下的數字,並加上 `(` 以及 `)` 後回傳。 所以 `MMM` 應為將有存放小數數字的節點加入到 hash table 中的操作,即為 `list_add`,`EEE` 決定該節點插入的是 hash table 中的第幾個 bucket,從 `find` 中可以知道 hash function 為 `number % size`,所以 `EEE` 為 `&heads[remainder % size]`,當找到重複的數字時,`pos >= 0` 進到條件式中,`while` 迴圈將非循環的小數部分存到 `p` 中,而 `node->index` 紀錄數字為小數點後第幾個數,所以 `PPP` 應為 `pos--`。 --- ## 測驗 `6` ```c #define ALIGNOF(t) \ ((char *)(&((struct { char c; t _h; } *)0)->_h) - (char *)0) ``` ```graphviz digraph G { rankdir=LR tee [shape=none margin=0 label= <<table border="0" cellspacing="0" cellborder="1"> <tr> <td port="f1" width="55" height="22" fixedsize="true">char c</td> </tr> <tr> <td port="f2" width="55" height="66" fixedsize="true">padding</td> </tr> <tr> <td port="f3" width="55" height="88" fixedsize="true">t _h</td> </tr> </table>>] start [label="0", shape=plaintext]; start->tee:f1:nw alignment [label="alignment", shape=plaintext]; alignment->tee:f3:nw } ``` 根據[你所不知道的 C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory?type=view#data-alignment)在成員位址不能整除時,會自動加入 padding 做 alignment。`&((struct { char c; t _h; } *)0)->_h` 取出 `_h` 成員相對於 0 的位址,相減便能求出 alignment 的位址。 ---