# 2020q3 Homework3 (quiz3) contributed by < `jeremy3951` > ###### tags: `(2020q3)進階電腦系統理論與實作` ## 測驗 `1` 填補下面的程式碼 : ```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)); } ``` ### 解答分析 :  失敗的算術右移 : ```graphviz digraph struct{ node[shape="record"] 1[label="1|1|1|0|1"] l2[label="0|1|1|1|0"] 1->l2 } ``` -3 位移完變 +14 . **為了避免這種情況發生 , 我們要讓負數右移後新的 bits 變成 1 .** 運作原理 : 根據 m 的正負來決定右移後要補 0 還是 1 . 根據運作原理來觀察這行程式碼 `(m >> n) | (fix ^ (fix >> n))` 可以得知在 m 位移完後 , 要用 fix 來修改位移後的新 bits . 首先觀察 `logical` 這個變數會發現它不是 0 就是 1 , 再看到 `unsigned int fixu = -(logical & (OP2))` , 如果此時把 `logical=0` 帶進去後 : `fixu=0` -> `fix=0` -> `return (m>>n)|0` ( 沒有動到 m>>n ) 這樣代表當 `logical=0` 的時候是正常狀況不用做修改 這時我們知當 `logical=1` 的時候就是我們要做修改的時候了 . 此時回來看第一行 `(((int) -1) OP1) > 0;` 若 `logical=1` 則 -1 經過 op1 後要 >0 , 選項 ( c )~( g ) 都會隨著 input 而有不同的正負值 , 所以可以去掉 , 選項( a ) 會讓 `logical=0` , 所以 op1 的答案是 ( b ) >>1 . 根據 op1 的答案我們會發現 -1 經過算術右移後變成了正數 , 想必是用 0 來當作左邊的新 bits , 所以這時候才有修改的必要 . 接下來再看回 `unsigned int fixu = -(logical & (OP2))` , 推測 fixu 的值不是 -1 * 0 就是 -1 * 1 , 這時我們看 -1 * 1 這條支線可以推得 `logical & op2=1` 也就是 `1 & op2 =1` , 所以 op2 就是 1 根據這個答案從選項中只剩 (m>0) 和 (m<0) 可以選 , 這時回頭看我們的假設是建立在需要修改的時候做更動 , 也就是 "負數做右移的時候補0當作新的 bits" , 從這個假設可得知我們的 m 要小於 0 才符合我們的假設 , 故 op2 選 ( c ) ## 測驗 `2` 題目 : 利用 __builtin_ctz 來實作 LeetCode [342. Power of Four](https://leetcode.com/problems/power-of-four/),假設 int 為 32 位元寬度。實作程式碼如下: ```cpp bool isPowerOfFour(int num) { return num > 0 && (num & (num - 1))==0 && !(__builtin_ctz(num) OPQ); } ``` 題目要求的是 4 的次方數 . ### 解題分析 + 運作原理 觀察 `(num & (num-1))==0` , 帶幾個數字進去 trace 後發現這個條件式是在篩選 num 是不是只有 1 個 bit ? 觀察 4 的次方數 : 4 -> 16 > 64 -> 256 -> 1024 用二進位來表示 : 2^2^ -> 2^4^ -> 2^6^ -> 2^8^ -> 2^10^ 由上述觀察可以發現的特性 : 1. 在二進位表示法中只有 1 個 bit 2. 經由 `__builtin_ctz` return 的值都是偶數(2,4,6,8...) 根據上面的特性寫的特性 , 對應的條件式如下 : 1. `(num & (num-1))==0` 2. `!(__builtin_ctz(num) OPQ)` 由此可知 OPQ 就是偵測 ctz return 的結果是否為偶數(搭配前面的 !) , OPQ 要填 ( f ) 0&1 ## 測驗 `3` 題目 : 針對 LeetCode [1342. Number of Steps to Reduce a Number to Zero](https://leetcode.com/problems/number-of-steps-to-reduce-a-number-to-zero/),我們可運用 gcc 內建函式實作,假設 int 寬度為 32 位元,程式碼列表如下: ```cpp int numberOfSteps (int num) { return num ? AAA + 31 - BBB : 0; } ``` 請補完程式碼 ### 解答分析 : 要填補上述的程式其實很簡單 , 帶幾次數字後就可以得知答案為 AAA=(a) BBB=(b) , 不過運作原理需要再想想 ### 運作原理 :  題目的步驟 : ```cpp while(num) (num 為偶數 ? )除 2 : 減 1 ; ``` 除 2 相當於往右 shift 1 個 bit ; 減 1 相當於把 LSB 設為 0. 我最初的想法 : 每個 1 都會被設為 0 並且位移 (除了最後一個 bit) 所以把 1 的個數 * 2 減 1 再加上這些 1 中間有幾個 0 再加上右邊有幾個 0 就是答案 照我上面的想法 , 這樣會需要 clz , ctz , popcount 來做運算 , 而且還不知道怎麼算中間的 0 , 跟老師的作法比起來既多一個 function 要呼叫 , 又要算出這些 1 之間的 0 .... 後來我換個角度來分析 , 把 num 分 3 區 : $\underline{0000000000} \ \underline{111001101011} \ \underline{0000000000}$ 第一區 : clz 第二區 : popcount * 2 - 1 + 中間的 0 個數 第三區 : ctz 答案 : 第二區 + 第三區 最後發現第二、三區的 0 可以算在一起 , 這些數字再加上 popcount 等價於 32 - clz 這時候第三區都算進去了 , 第二區剩下 popcount - 1 , 把這項加進去 : 32 - clz + popcount -1 = popcount + 31 - clz 上述的式子就是此題的答案了! ## 測驗 `4` 填補下面的程式碼 : ```cpp 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 (XXX); return YYY; } ``` ### 運作原理 : 先把兩數共同的 2 次方因數提出來 , 做完輾轉相除法後 , 把公因數乘上該 2 次方因數(藉由 shift 的方式) **程式分析 :** 先看 for 迴圈裡的判斷式 : `!((u | v) & 1)` &1 代表只跟最右邊的 bit 做 & 運算 , 再觀察回圈內的行為(u 和 v 都除 2) , 所以推斷得出此行程式碼在看是否 u 和 v 均為偶數 . 如果 u 或 v 其中一個為奇數就會跳出迴圈. 此時的狀態 : 2 已經不再是當下的 u 和 v 的公因數了(因為其中一者是奇數) 所以這時先把 u 用成奇數再進 while 迴圈 . 在迴圈裡的行為就是把 v 用成奇數 , 然後作輾轉相除法後就結束了 . ### 解題分析 : XXX : 由於 do while 迴圈內還有一個迴圈就是看 v 的最後一個 bit 決定它是不是偶數 , 但是如果 v 當下為 0 的話 , 這個迴圈就跑不停了 , 所以 XXX 一定要做到偵測 v 為 0 的時候就停止運作 , 故這題選擇 ( b ) v YYY : 根據前面的運作原理 , 求完公因數後還要再 shift 原本的 2 次方公因數 , 所以選 ( e ) u << shift ## 測驗 `5` 題目 : 在影像處理中,bit array (也稱 bitset) 廣泛使用,考慮以下程式碼: ```cpp 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; } ``` 可用 clz 改寫為效率更高且等價的程式碼: ```cpp 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 = KKK; int r = __builtin_ctzll(bitset); out[pos++] = k * 64 + r; bitset ^= t; } } return pos; } ``` 請補完程式碼 ### 解題分析 : 其實這題的解答在第三題的題目講解就劇透了( ~~不知道是巧合還是?~~ ) > 總是要讓學員偶爾占到好處,學員才會想繼續看下去,人性操作 > :notes: jserv 比對修改前和修改後的程式後我發現只有以下的程式碼不太一樣 :  ```cpp for (int i = 0; i < 64; i++) { if ((bitset >> i) & 0x1) out[pos++] = p + i; } ``` 這個迴圈針對 `bitset` 的 LSB 做判斷後再決定要不要寫入 out[] 裡面 , 改版後的程式碼 : ```cpp while (bitset != 0) { uint64_t t = KKK; int r = __builtin_ctzll(bitset); out[pos++] = k * 64 + r; bitset ^= t; } ``` 先看第二行得知 r 可以知道 bitset 的右邊還有幾個連續 0 , 再跳到第四行看到 bitset 被 t 改變了後進行下一輪 , 這時一樣是看當下的 bitset 的右邊有幾個 0 看到這裡可以推測 , t 可以改變 bitset 的樣子 , 甚至讓 ctz return 的值都不一樣 , 再看一下迴圈的條件 , 就可以推得 : **bitset 最右邊 1 的每次都會被設成 0 !** 然後我又想起第三題的某段補充 : > 類似的操作還有 `x & -x`,將 `x` 的 LSB 取出 (**isolate LSB**) 由這段得知這個方法 所以這題選 ( e ) `bitset & -bitset`