# 2020q3 Homework3 (quiz3) contributed by < `jonec76` > ###### tags: `sysprog-2020` > [作業說明](https://hackmd.io/@sysprog/B1zAbkAEP) > [題目](https://hackmd.io/@sysprog/2020-quiz3) ## 開發環境 ```shell $ uname -a Linux jonec76-Aspire-M7720 5.4.0-47-generic #51-Ubuntu SMP GNU/Linux ``` ## 參考解答 OP1 = >> 1 OP2 = m < 0 OPQ = & 0x1 AAA = __builtin_popcount(num) BBB = __builtin_clz(num) XXX = v YYY = u << shift KKK = bitset & -bitset ## 作業 - 重新回答 [第 3 周測驗題](https://hackmd.io/@sysprog/2020-quiz3),連同附帶的「延伸問題」。 - 將你的共筆加到 [2020q3 Homework3 (作業區)](https://hackmd.io/@sysprog/2020-homework3) ### Q1 ```c=1 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)); } ``` - 解題想法 在 [undefined behavior](https://en.wikipedia.org/wiki/Undefined_behavior) 中提到 > In C/C++ bitwise shifting a value by a number of bits which is either a negative number or is greater than or equal to the total number of bits in this value results in undefined behavior. 可以知道對一個負數做 shift 時,是 undefined behavior。,因為第 3 行後面是個判斷式,程式碼中變數 `logical` 應只為 0 或者 1。此判斷式希望知道 `-1>>1` 後,編譯器會執行邏輯右移,還是算數右移。若右移後值 >0,表示最左邊的 sign bit 是補 0,藉此判斷出是邏輯右移。 OP2 的選項在課堂答題時想不出來,有了提示後才知道,此行是用在 1. 負數右移,但是最左邊卻補 0 2. m 為負數 (所以 OP2=m<0) 負數右移後卻變成正的,這樣並不合理。而這行在做的事情,就是幫忙在右移後補 0 的位置,改成 1,使得負數右移後仍是負數。 倒著來想的話 ```c=1 | (fix ^ (fix >> n) ``` 因為右移的特性在上述的 case 是補 0,再搭配 XOR 之後,此行應該要產生類似 0x11110000 的結果,其中 1 的位置跟 `m>>n` 做 or 後剛好能夠把 0 的位置改成 1。 第 5 行參考了 [RinHizakura 筆記](https://hackmd.io/@RinHizakura/SkjPS8vBP) 中老師的評語,是為了避免編譯器進行過度 (aggressive) 最佳化,但這部分還在研究。 ### Q2 ```c=1 bool isPowerOfFour(int num) { return num > 0 && (num & (num - 1))==0 && !(__builtin_ctz(num) OPQ); } ``` - 解題想法 此題需要找出 num 是否是 4 的冪次方,首先先探討 ```c=1 (num & (num - 1))==0 ``` 當此條件要成立,就表示 num 是 2 的冪次方($num=2^n, where\ n=1,2,...$),因為若是 2 的冪次方,則會是只有一個 1 跟後續全部都是 0,例如 `0x100000`,該數減 1 後會變成原本 0 的地方變成 1,1 變成 0,剛好能滿足此式。 接著,觀察一下 $num=4^n, where\ n=1,2,...$,這樣的數字執行 `__builtin_ctz` 後,得到的都是偶數,`!__builtin_ctz(num) & 0x1` 即成立。 ### Q3 ```c=1 int numberOfSteps (int num) { return num ? __builtin_popcount(num) + 31 - __builtin_clz(num) : 0; } ``` - 解題想法 此題希望能夠將一個數字,用特定的步驟方法使之變為 0。步驟如下 1. divide it by 2 2. subtract 1 from it 一個數字只要知道它在二進制時候的最高位,就可以知道要除以多少次 2 後會變成 0,因為可以當作是右移直到為 0。那何時需要減 1 呢?就是當該數目前是奇數時,或者除以 2 後是奇數時。 那這題可以以二進制的方法來想,如果最低位是 1(表示是奇數),那要算一步減 1,如果是 0 且該數還不為 0,那就也要算一步(右移)。而這題的選項 `__builtin_popcount(num)`,為的是要找出該數有多少個 1 需要被減去,而 `31 - __builtin_clz(num)` 為的是要找出最高位是第幾位。如此一來將兩者相加,所得到的就是這題題目要的總步驟數了。 ### Q4 ```c=1 #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); } ``` - 分析提示程式碼 ```c=1 while (v) { uint64_t t = v; v = u % v; u = t; } ``` 可以先看到主要做輾轉相除法的部分,在這段程式碼中以例子來看,u=10 v=8 ,那執行一輪之後希望得到 u=8 v=2,也就依據輾轉相除法的方式做。 - 解題方向 下面程式碼在做的事情,是檢查 u, v 是否都是偶數,若是則同時除 2,並且讓 shift +1。 ```c=1 for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; } ``` 而下面的程式碼,如果執行了,表示 u 是偶數且 v 是奇數。可以知道偶數跟奇數並不會有 2 冪次方的公因數,所以下方就是要將 u 再做調整,將 2 的因數都拿掉 ```c=1 while (!(u & 1)) u /= 2; ``` 上方所做的為的都是希望使 u, v 的值越小越好,這樣做輾轉相除法的次數就會越少。進入輾轉相除法的回圈中能看到,因為 u 一定是奇數,但 v 有可能在減去 u 的過程變成偶數,所以對 v 做調整(原理同上)。 ```c=1 while (!(v & 1)) v /= 2; ``` 第一個選項 XXX 跟提示程式碼的中止條件是相同的,而回傳的 YYY=(u << shift),是因為在一開始若兩數 u, v 都是偶數,會被提出公因數 2 直到沒辦法再提,此步驟就是將這公因數乘回去最後的答案。 ### Q5 ```c=1 #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; } ``` - 分析提示程式碼 ```c=1 for (int i = 0; i < 64; i++) { if ((bitset >> i) & 0x1) out[pos++] = p + i; ``` 在看到此段程式碼時,其實並不知道其背後的物理意義為何。但是單就閱讀程式碼時可以發現,在上面的這行是要找出 `bitset` 中,bit=1 的 index ,並且將這些 index 都加起來。 - 解題方向 老師在上課時提到,這段程式碼是用來降噪用的,此部分還沒深入理解。但是還是可以依據上面的分析,將這題的答案找出來。 倒著分析來看此題,知道這函式的目的是要找出 1 的 index 並且加起來後,又看到了 `__builtin_ctzll`。表示可以透過計算後面有多少個 0,得到不同 1 的 index。但是要如何讓最低位的 1 在算完這次的 `__builtin_ctzll` 後,變成 0 使得能計算出第二低的 1 的 index 呢?看到 ```c=1 bitset ^= t; ``` 既然要透過 XOR,那表示最低位的 1 要對應到 t 相對應位置也是 1,且 t 的其他位置應為 0。此時可以發現 ```c=1 bitset & -bitset ``` 可以透過二補數的特性,剛好在做完了 `&` 之後,只會剩下最低位 1 的位置是 1,其餘都是 0。