--- tags: linux2022 --- # 2022q1 Homework2 (quiz2) contributed by < [`ShawnLu31`](https://github.com/ShawnLu31) > ## 測驗 1 ### 解釋程式碼 ```c uint32_t average(uint32_t a, uint32_t b) { return (a + b) / 2; } ``` 簡單地將 `a` 和 `b` 相加後除以 2 。但此作法可能導致 overflow,例如: uint32 的最大值相加。 ```c uint32_t average(uint32_t low, uint32_t high) { return low + (high - low) / 2; } ``` 避免直接相加導致 overflow ,其中一個數與兩數的差的平均相加,兩者的差不會超過該兩個數的數值,所以不會導致 overflow 。 $$high = (high - low) + low$$ $$ \frac{high + low}{2} = \frac{(high - low) + 2low}{2} = \frac{high - low}{2} + low $$ ```c uint32_t average(uint32_t a, uint32_t b) { return (a >> 1) + (b >> 1) + (a & b & 1); } ``` `a` 和 `b` 各自除以 2 並相加。 因右移運算子 `>>` 移出的位元會被忽略,若 `a` `b` 皆為奇數,右移除 2 的結果會差 1 ,必須加回 1 ,故使用 `a & b & 1` 判斷兩者是否皆為奇數。 ```c uint32_t average(uint32_t a, uint32_t b) { return (a & b) + ((a ^ b) >> 1); } ``` 參考加法器原理 `a + b` 等價於 `((a & b) << 1) + (a ^ b)` , `(a & b) << 1` 為進位, `(a ^ b)` 為和。 所以 `(a + b) / 2` 等價於 `(((a & b) << 1) + (a ^ b)) >> 1` ,再化簡 -> `(((a & b) << 1) >> 1) + ((a ^ b) >> 1)` -> `(a & b) + ((a ^ b) >> 1)` ## 測驗 2 ### 解釋程式碼 ```c uint32_t max(uint32_t a, uint32_t b) { return a ^ ((a ^ b) & -(a < b)); } ``` 若 `a` 小於 `b`,則 `-(a < b) = -1` ,否則等於 `0` 。 回傳值會有兩種情況: `a ^ ((a ^ b) & -1)` 或 `a ^ ((a ^ b) & 0)` 在 `a` 小於 `b` 時,`-(a < b) = -1` , `-1` 在二補數表示時為 `0xffffffff` ,所以 AND `-1` 不會有改變,可以去掉化簡為 `a ^ a ^ b` 。兩個同樣的數做 XOR 會為 `0` ,將 `a ^ a` 先運算為 `0` ,再運算 `0 ^ b` 等於 `b` 。 ``` // a < b a ^ ((a ^ b) & -(a < b)) = a ^ ((a ^ b) & -1) = a ^ ((a ^ b) & 0xffffffff) = a ^ (a ^ b) = 0 ^ b = b ``` 在 `a` 大於等於 `b` 時, `-(a < b) = 0` , AND `0` 結果都是 `0` ,括號內 `((a ^ b) & 0)` 等於 `0` ,最後`a ^ 0` 等於 `a` 。 ``` // a >= b a ^ ((a ^ b) & -(a < b)) = a ^ ((a ^ b) & 0) = a ^ 0 ``` 變數用途: * `(a < b)` 做比較。 * 第一個 `a` 回傳 `a` 的值。 * 第二個 `a` 抵銷第一個 `a` 在需要回傳 `b` 時。 * 第一個 `b` 回傳 `b` 的值。 ## 測驗 3 ### 解釋程式碼 ```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 (v); return u << shift; } ``` 可分成三個區域: 1. `if` 區域對應 GCD 演算法規則第 1、2 條。因至少一個數為 0 ,使用 OR 運算子回傳另一個數的值。 2. `for` 區域對應 GCD 演算法規則第 3 條。兩數 LSB 皆為 0 (偶數), 2 為公因數, shift 紀錄兩者最大公因數的 2 的冪。 3. `while` 和 `do while` 區域對應 GCD 演算法規則第 4、5 條。 * 第一個 `while` 與 `do while` 內部的 `while` 迴圈,使 `u` `v` 移除 2 的因數,因為經過前述的 `for` 迴圈,已不再有 2 的因數。 * `do while` 迴圈為輾轉相除法:被除數(大數)除以除數(小數),再使餘數與除數成為下一輪的被除數與除數,重述此動作直到餘數為 0 ,除數便是最大公因數。 在此實作,因 `v` 為迴圈的條件,所以使 `v` 一直放餘數, `u` 一直放除數,當 `v` 為 0 時, `u` 便為最大公因數。 所以 `v` 大於 `u` 時,可直接 `v -= u` ;`v` 小於 `u` 時,此時 `v` 為除數 `u` 被除數,須將除數換成 `u` ,並把差值給 `v` ,才能維持 `v` 餘數 `u` 除數。 最後回傳 `u << shift` ,公因數(不含因數 2)乘回因數 2 的冪,得到最大公因數。 ## 測驗 4 ### 解釋程式碼 ```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; } ``` 外部迴圈走訪 bitmap 每個元素。 bitset 取出 bitmap 的一個元素,內部迴圈低位往高位尋找為 `1` 的位元,若該位原為 `1` ,此時 `i` 為第 `i` 個位元。 :::warning * out 的輸出涵義? pos 為 out 的 index,每找到一個 `1` pos 會加一。 bitmap 位元若都是 `1` pos 有最大值 ` bitmapsize * 64` 。 `p + i = k * 64 + i` 的意義? ::: ```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 = bitset & -bitset; int r = __builtin_ctzll(bitset); out[pos++] = k * 64 + r; bitset ^= t; } } return pos; } ``` 使用 `__builtin_ctzll` 改寫原本的迴圈,由低位往高位尋找第一個碰到的 `1` , `r` 等價於原本程式的 `i` 。 且使用 `XOR t` 清除碰到的 `1` (最低位元的 `1` ) ,使下一次的 `__builtin_ctzll` 不會成重複找到同個位元的 `1` 。 `-bitset` 為 `bitmap` 的二補數,兩者做 AND 後的 `t` 唯一的 `1` 會是 `bitmap` 最低位元的 `1` 。 此方法會直接找到需要的位元位置,不需像原本程式用迴圈一一檢查每個位元。 ## 測驗 5 ### 解釋程式碼 ## 測驗 6 ### 解釋程式碼 ## 測驗 7 ### 解釋程式碼