# 2020q3 Homework3 (quiz3) contributed by < `tsengsam` > [第三週測驗題](https://hackmd.io/@sysprog/2020-quiz3) --- ## 測驗 `1`: Arithmetic Right Shift #### 因為負數的算數右移屬 `unspecified behavior`,表示負數在不同平台做算數右移後的最高位元不一定相同 ```cpp= int asr_i(signed int m, unsigned int n) { const int logical = (((int) -1) OP1) > 0; unsigned int fixu = -(logical & (OP2)); int fix = *(int *) &fixu; return (m >> n) | (fix ^ (fix >> n)); } ``` 1. 首先從第 6 行判斷,負數右移最高位元若補 `1` 時 `(fix ^ (fix >> n))` 為 0,即無調整;反之若補 `0` 時 `(fix ^ (fix >> n))` 的最高位元為 1 其他位元 0。。 2. 再來選擇一種補法來回推:若補 1, fix 為 `0`。 3. 表示第 6 行的 fixu 亦為 `0` 。 4. 第 4 行的 `-(logical & (OP2))` 亦為 `0`, 不過 `logical` 與 `OP2` 的排列組合多達三種可能,故回到上述第 2 步驟改成補 0, fix 變為 (-1)~10~。 5. 表示第 6 行的 fixu 為所有位元皆 1,也就是最大值。 6. `-(logical & (OP2))` 即可知道小括號內為最大值的二補數,即 `1`。 7. `(logical & (OP2))` 為 1 表示 & 兩邊皆為 1。 8. `OP2` 可知只有 `(c) m < 0` 一定為 1 ,其他如 (f) 當位移為 0 時會錯。 9. `logical` 為 1 時表示`((int) -1) OP1` 應為正數,故只有 `(b) >> 1` 符合。 --- ## 測驗 `2`: Power Of Four #### 計算給定的有號整數是否為 4 的次方 ```cpp bool isPowerOfFour(int num) { return num > 0 && (num & (num - 1))==0 && !(__builtin_ctz(num) OPQ); } ``` * 解本題之前我先以慢但直覺的方式實作,思路是用迴圈計算當前數值是否為 4 的倍數,若是便將其除以 4 直到數值小於等於 1。 ```cpp bool isPowerOfFour_loop(int num) { if (num <= 0) return false; for (int r = 0; num > 1; num /= 4) { if (num % 4) return false; } return true; } ``` * 從迴圈版本可以了解邊界條件為 `num > 0`,且四的次方之數值僅一個位元為 1,如此可理解本題的前兩項判斷式的用意。 * 最後一個判斷式 `!(__builtin_ctz(num) OPQ)` 用作計算 `num` 從 LSB 算起有幾個連續的 `0`並進行 `OPQ` 運算,若希望回傳 `true` 則 `(__builtin_ctz(num) OPQ)` 應為 `false` 需發現 4 的次方之數字的 trailing zero 皆為偶數個,如此可知 `OPQ` 為 `& 0x1` 時可判斷 trailing zero 是否為偶數個。 --- ## 測驗 `3`: Population Count #### 用 gcc 內建函式__builtin_popcount 與 __builtin_clz 計算 [LeetCode 1342. Number of Steps to Reduce a Number to Zero](https://leetcode.com/problems/number-of-steps-to-reduce-a-number-to-zero/) * 首先在 LeetCode 用慢但直覺得方式作答,若當前數值為奇數時要減一;反之則除二: ```cpp int numberOfSteps_loop(int num) { int cnt=0; while (num) { cnt++; if (num % 2) { num -= 1; } else { num = num >> 1; } } return cnt; } ``` 如此可用以理解 bitwise 作法: 換算成二進制的數值來看可知當 LSB 為 1 時會減一 (奇數),若為 0 則除二 (偶數),直至數值為零 1. 所有位元皆會被檢查一次,而檢查的方式是透過右移運算並檢查 LSB 為 1 或 0 2. 只要該位元是 1 表示當其位移至 LSB 時必定會做減一 3. 可透過事先計算 leading zero 個數來避免檢查所有位元 4. 故上述步驟加總起來便是 (`計算 set bit 個數` + `最多位移次數` - `leading zero 數量`) = `__builtin_popcount(num)` + `31` - `__builtin_clz(num)` ```cpp= int numOfSteps(int num) { return num ? __builtin_popcount(num) + 31 - __builtin_clz(num) : 0; } ``` --- ## 測驗 `4`: Greast Common divisor (GCD) #### 求兩數之最大公因數 * 最直接的寫法是使用[輾轉相除法](https://zh.wikipedia.org/wiki/%E8%BC%BE%E8%BD%89%E7%9B%B8%E9%99%A4%E6%B3%95),將兩數相減所得的數會保留公因數,並將所得數與較小的數值重複相減動作直到較小數字變為 0。 ```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; } ``` * 本題則是先計算公因數是 2 的多少倍,算完後把仍有 2 因數的數字除以 2 直到變成奇數,最後再如同輾轉相除法相減直到有一數為 0。回傳時再將首先計算的 2 的倍數乘回即為答案。 ```cpp= #include <stdint.h> uint64_t gcd64(uint64_t u, uint64_t v) { if (!u || !v) return u | v; int shift; /* for-loop will calculate 2's common divisor * After this, at least one value is odd */ for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; } /*Let u be odd*/ while (!(u & 1)) u /= 2; do { /*Let v be odd*/ while (!(v & 1)) v /= 2; /*Reduce value but common divisor will stay*/ if (u < v) { v -= u; } else { uint64_t t = u - v; u = v; v = t; } } while (v); /*Multiply back common divisor related to 2*/ return u << shift; } ``` --- ## 測驗 `5`: Bit Array #### 從給定的 bitmap array 中找所有 set bit 並把其所在 index 放到 out array * naive 寫法是**逐一**確認 bitmap 每個元素中的每個位元是否為 set bit ```cpp= #include <stddef.h> size_t naive(uint64_t *bitmap, size_t bitmapsize, uint32_t *out) { /* bitmap is a array of uint64_t*/ size_t pos = 0; /* 0 <= k < bitmapsize * k means the kth word of bitmap */ for (size_t k = 0; k < bitmapsize; ++k) { /* In each loop, assign bitmap[k] to bitset */ uint64_t bitset = bitmap[k]; /* the index of first bit in each word*/ size_t p = k * 64; /*To check whether each bit is set bit or not*/ for (int i = 0; i < 64; i++) { /* If the ith bit is set * then pos ++, recording number of set bits * out[pos] stores the position of set bit in the whole array */ if ((bitset >> i) & 0x1) out[pos++] = p + i; } } return pos; } ``` 1. Bitmap 中的元素存放 64 bits 長度的無號整數 2. Line 6: `pos` 是 set bit 數量 3. Line 10: `k` 是 `bitmap` 的 index,也代表者存放第 `k` 個 words 的索引 4. Line 12: 將 `bitmap` 第 `k` 個元素指派給 `bitset`,以便做接下來找 set bit 的運算 5. Line 14: `p` 用來記錄 `bitmap` 第 `k` 個元素在整個 array 中屬於第幾個 bit 6. Line 16: 對第 `k` 個元素中的 64 個 bits 逐一確認是否為 set bit 7. Line 21: 透過 `>>` 運算把欲檢查的 bit 移到 LSB,與 `0x1` 做 `AND` 運算可確認是否為 set bit 8. Line 22: 將 set bit 在整個 array 中的位置指派給 `out` 9. Line 25: 最後將 set bit 的數量回傳 * 改良版透過 `__builtin_ctzll` 函式求出從 LSB 數起有幾個連續 0 bit 並可得出最低 set bit 的位置指派給 `out` ```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 = bitset & -bitset; /*KKK*/ int r = __builtin_ctzll(bitset); out[pos++] = k * 64 + r; bitset ^= t; } } return pos; } ``` 1. Line 8: 有別於 naive 寫法的第二層 for 迴圈,因為透過第 12 行運算可避免逐一 bit 檢查 2. Line 10: 計算最低位元的 set bit 之前有幾個 0 bit 3. Line 12: `bitset ^= t` 的 `t` 可得僅最低位元的 set bit 為 1,其餘皆 0 的數值,再與原本 `bitset` 做 `XOR` 會使得最低位元 set bit 變成 0 bit,其餘位元不變。 ###### tags: `linux2020`