# 2020q3 Homework3 (quiz3) contributed by < `nelsonlai1` > ## 測驗 1 ```c= 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)); } ``` 這題要實作在任何環境下可運作的 ars(arithmetic right shift),ars 會在右移後,依照原本首位的 bit 補回值,也就是正數補 0,負數補 1。 一開始的 logical 判斷環境是不是 logical right shift (一律補 0),所以對 -1 右移 1(`OP1 = >> 1`),如果是 lrs,會變成 0b011...111 > 0 = true = 1,反之則是 0b111...111 < 0 = false = 0。 fixu 用來判斷被除數 m 是不是負數(`OP2 = m < 0`),當 logical = 1 以及 m < 0 時,fixu = -(1 & 1) = 0b111...111,其他時候fixu = 0b000...000。fix 則是將 fixu(unsigned int) 的值存成 int。 最後 return 的結果,如果在 logical = 1 以及 m < 0 都成立時,fix ^ (fix >> n) 會是前 n 個 bits 為 1,其餘為 0。所以 | (fix ^ (fix >> n)) 就可以把 lrs 吃掉的 1 補回來。 example : m = 0b10110101, n = 4, system = logical ```graphviz digraph { m [shape = plaintext] intm [label = "1|0|1|1|0|1|0|1" shape = record] } ``` ```graphviz digraph { m_shift [label = "m >> n" shape = plaintext] intm_shift [label = "0|0|0|0|1|0|1|1" shape = record] } ``` ```graphviz digraph { fix [shape = plaintext] intfix [label = "1|1|1|1|1|1|1|1" shape = record] } ``` ```graphviz digraph { fix_shift [label = "fix ^ (fix >> n)" shape = plaintext] intfix_shift [label = "1|1|1|1|0|0|0|0" shape = record] } ``` ```graphviz digraph { return [shape = plaintext] intreturn [label = "1|1|1|1|1|0|1|1" shape = record] } ``` ## 測驗 2 ```c= bool isPowerOfFour(int num) { return num > 0 && (num & (num - 1))==0 && !(__builtin_ctz(num) OPQ); } ``` 這題要輸入的數字是否為 4 的次方,4 的二進位表示是 100,也就是說 4 的次方一定是 1 後面接著偶數個 0 (每次乘四都是 << 2)。 一開始 `num > 0` 判斷是否為正整數,`(num & (num - 1)) == 0` 判斷是否為 2 的次方 ($1{\overbrace{000...000}^{n個}}$ & $0{\overbrace{111...111}^{n個}} = 0$) ,所以最後這項 `!(__builtin_ctz(num) OPQ` 可以知道是在判斷後面的 0 的個數是否為偶數了。而任一偶數 & 1 = 0 (偶數最後一項 bit 為 0),所以 `OPQ = & 0x1`。 ### 延伸問題 1. 改寫上述程式碼,提供等價功能的不同實作,儘量降低 branch 的數量 ```c= bool isPowerOfFour(int num){ return num & 0x55555555 && (num & (num - 1)) == 0; } ``` ![](https://i.imgur.com/B2VqwmN.png) & 0x55555555 可以一次判斷是否為正數以及 trailing 0-bits 是否為偶數(在 num 是 2 的次方條件下) 2. 練習 LeetCode 1009. Complement of Base 10 Integer 和 41. First Missing Positive,應善用 clz Complement of Base 10 Integer : ```c= int bitwiseComplement(int N){ if (N == 0) return 1; int shift = __builtin_clz(N); return ~((uint32_t)N << shift) >> shift; } ``` 利用右移會無條件捨去的特性,先將輸入值左移到最左邊後做 not,再右移一樣的次數(`__builtin_clz(N)`),就能得到結果。 ![](https://i.imgur.com/ve3zk2L.png) First Missing Positive : ```c= int firstMissingPositive(int* nums, int numsSize) { for (int i = 0; i < numsSize; i++) { if (nums[i] > 0 && nums[i] <= numsSize) { int swap = nums[i] - 1; if (nums[i] != nums[swap]) { int tmp = nums[i]; nums[i] = nums[swap]; nums[swap] = tmp; i--; } } } for (int i = 0; i < numsSize; i++) { if (nums[i] != i + 1) return i + 1; } return numsSize + 1; } ``` ![](https://i.imgur.com/QJ9EqOZ.png) 用 bucket sort 的概念,將 1, 2, 3... 換到 nums[0], nums[1], nums[2]...。最後再從 nums[0] 開始找起,第一個不符合的數就是答案,如果都符合的話答案為 numsSize + 1 (代表從 1 到 numsSize 都各出現一次)。 值得一提的是,如果直接寫 nums[nums[i] - 1],會出現 `ERROR: AddressSanitizer: heap-buffer-overflow` 錯誤,推測是因為數據中有小於 1 的數,即使不會執行到這行,系統也會判斷操作到錯誤的 array 位置 (< 0)。 ## 測驗 3 ```c= int numberOfSteps (int num) { return num ? AAA + 31 - BBB : 0; } ``` 這題計算一個數經過多少次計算可以變為零,而計算只有分兩種 : >> 1 (當前是偶數)以及 - 1 (當前是奇數)。 int 寬度為 32 bits,因此將最左邊的 bit 移到最右邊要 31 次 >> 1,每有一個 1 都需要做一次 - 1,而起始 1 的位置每往右一個 (leading 0-bits 多一)就能少做一次 >> 1。 所以 `AAA = __builtin_popcount(num)`, `BBB = __builtin_clz(num)` ### 延伸問題 1. 改寫上述程式碼,提供等價功能的不同實作並解說 ```c= int numberOfSteps (int num){ return __builtin_popcount(num) + 31 - __builtin_clz(num | 1); } ``` 因為 `__builtin_clz(0)` 不被允許,所以加上 `| 1` 讓即使在 `num == 0` 時也能返回正確值,同時也不會影響其他情況的結果(因為是最後一位)。 2. 避免用到編譯器的擴充功能,只用 C99 特徵及 bitwise 運算改寫 LeetCode 1342. Number of Steps to Reduce a Number to Zero,實作出 branchless 解法,儘量縮減程式碼行數和分支數量 ```c= int popcount(int x) { uint32_t v = (uint32_t) x; uint32_t n; n = (v >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; v = (v + (v >> 4)) & 0x0F0F0F0F; v *= 0x01010101; return v >> 24; } int clz(int x) { x |= x >> 1; x |= x >> 2; x |= x >> 4; x |= x >> 8; x |= x >> 16; return 32 - popcount(x); } int numberOfSteps (int num){ return popcount(num) + 31 - clz(num | 1); } ``` popcount 的寫法參照測驗 3 題目敘述的 popcnt_branchless,而 clz 寫法前面五行可以保證 MSB 後的 bits 皆為 1。最後再用 32(total bits) 減去 popcount 就是 leading 0-bits 數量。 (做法參考 [The Aggregate Magic Algorithms](http://aggregate.org/MAGIC/#Leading%20Zero%20Count)) ## 測驗 4 64-bit GCD (greatest common divisor, 最大公因數) 求值函式 ```c= #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; } ``` 改寫為以下等價實作 ```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 (XXX); return YYY; } ``` 上面的 function 在實作輾轉相除法,一開始先判斷如果 u, v 有任一數不存在,就回傳存在的那個數。之後 while 迴圈裡在做的是將較大的一方 u 對較小的一方 v 取餘數(即使一開始 u 比較小,過完一輪後就會跟 v 互換值),將 v 改為餘數值(小),將 u 改為原本的 v (大),算是比較直觀的寫法。 下面的做法可以搭配 [Binary GCD](https://hackmd.io/@sysprog/gcd-impl#Binary-GCD) 來思考。第一個 for 迴圈的內容就是這句的應用 >binary gcd 的精神就是當兩數為偶數時,必有一 2 因子。 而 shift 用來紀錄有幾個 2 因子,所以最後 return 的結果應該要 << shift ($\times 2^{shift}$) 補回來。 之後分別檢查 u, v 是否偶數則是對應這句 >且一數為奇數另一數為偶數,則無 2 因子。 所以透過不斷除以二,直到兩者都是奇數為止。 最後就是做一般的輾轉相除法,只是這次不使用 % 運算子,所以就是比較兩數大小,然後大減小,變得比較像是 [Wikipedia 的展示動畫](https://zh.wikipedia.org/zh-tw/%E8%BC%BE%E8%BD%89%E7%9B%B8%E9%99%A4%E6%B3%95#/media/File:Euclidean_algorithm_252_105_animation_flipped.gif),而我們也可以發現 v 都是被減數,所以`XXX = v`。 而 return 結果則是依照 $\text{gcd}(x, y) = \text{even_factor}·\text{gcd}((x\gg \text{even_factor}), (y\gg \text{even_factor}))$,把 $\text {even_factor}(2^{shift})$ 乘回來,所以 `YYY = u << shift`。 ### 延伸問題 1. 在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升 ```c= uint64_t gcd64(uint64_t u, uint64_t v) { if (!u || !v) return u | v; int ctzu = __builtin_ctz(u); int ctzv = __builtin_ctz(v); int shift = ctzv ^ ((ctzu ^ ctzv) & -(ctzu < ctzv)); u = u >> ctzu; v = v >> ctzv; 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; } ``` 第五行使用 [Bit Twiddling Hacks](http://graphics.stanford.edu/~seander/bithacks.html#IntegerMinOrMax) 的方法得到兩數中較小的數。 利用 perf 檢查兩者效能差距 : 由於單一次呼叫 function 不足以有效看出差距,所以使用 for 迴圈來多次呼叫,且因為主要改善在 u, v 皆為偶數時,故只測 u, v 皆為偶數的情況。 ```c= for (uint64_t i = 0; i <= 0x100000;i += 2) gcd64(i, 0x100000); ``` 原始 : ``` Performance counter stats for './quiz3_4_control': 57.54 msec task-clock # 0.995 CPUs utilized 1 context-switches # 0.017 K/sec 0 cpu-migrations # 0.000 K/sec 46 page-faults # 0.799 K/sec 2,3809,7810 cycles # 4.138 GHz 2,2032,2845 instructions # 0.93 insn per cycle 4311,6621 branches # 749.310 M/sec 332,9927 branch-misses # 7.72% of all branches 0.057811581 seconds time elapsed 0.057830000 seconds user 0.000000000 seconds sys ``` 使用 __builtin_ctz 改寫 : ``` Performance counter stats for './quiz3_4_test': 40.82 msec task-clock # 0.996 CPUs utilized 0 context-switches # 0.000 K/sec 0 cpu-migrations # 0.000 K/sec 45 page-faults # 0.001 M/sec 1,6873,4941 cycles # 4.134 GHz 1,4741,2793 instructions # 0.87 insn per cycle 3052,7407 branches # 747.908 M/sec 261,0331 branch-misses # 8.55% of all branches 0.040974330 seconds time elapsed 0.041005000 seconds user 0.000000000 seconds sys ``` 可以發現在速度上以及 branches 有大約 30% 的改善。若是 u, v 有很大的 2^N 公因數時,效能差距可以更為顯著。 ## 測驗 5 在影像處理中,bit array (也稱 bitset) 廣泛使用,考慮以下程式碼: ```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; } ``` 可用 clz 改寫為效率更高且等價的程式碼: ```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 = KKK; int r = __builtin_ctzll(bitset); out[pos++] = k * 64 + r; bitset ^= t; } } return pos; } ``` 一個 bitmap 長得像這樣,共有 64 * bitmapsize 個 bits。 for 迴圈裡做的是找出那些 bits 為 1 (`if ((bitset >> i) & 0x1)`),pos 紀錄 1 的個數,out 紀錄 1 的位置(`out[pos++] = p + i`)。 | | 0 | 1 | 2 | ... | bitmapsize - 1 | | --- | --- | --- | --- | --- | -------------- | | 0 | 0 | 1 | 0 | ... | 1 | | 1 | 0 | 0 | 1 | ... | 0 | | 2 | 1 | 1 | 1 | ... | 1 | | ... | ... | ... | ... | ... | ... | | 63 | 0 | 1 | 0 | ... | 1 | 下方的 code 改寫了後面尋找 1 的部分,從原本逐個 bit 測試,改成直接計算有幾個 trailing 0-bits,再把最後的 1 改成 0,進入下一輪。 而可以做到的 t 是 `bitset & -bitset`,因為 -bitset = ~bitset + 1 (2's complement),所以 t 會是 bitset 最後一個 1 的位置為 1,而其餘皆是 0 (獨立 LSB)。而 `bitset ^= t` 就能把最後一個 1 改成 0 了。 ### 延伸問題 1. 舉出這樣的程式碼用在哪些真實案例中 2. 設計實驗,檢驗 clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 bitmap density 3. 思考進一步的改進空間 在[這篇文章](https://lemire.me/blog/2018/03/08/iterating-over-set-bits-quickly-simd-edition/)中有提到使用 SIMD 指令改寫的方法,在 bitmap 密集時,有明顯的效能增長。