# 2020q3 Homework3 (quiz3) contributed by < `justapig9020` > ###### tags: `sysprog2020` ## Outline [TOC] ## Link [作業區](https://hackmd.io/@sysprog/2020-homework3) [題目](https://hackmd.io/@sysprog/2020-quiz3) [github](https://github.com/justapig9020/sysprog2020-quiz3) ## 測驗 `1` ```cpp int asr_i(signed int m, unsigned int n) { const int logical = (((int) -1) >> 1) > 0; unsigned int fixu = -(logical & (m < 0)); int fix = *(int *) &fixu; return (m >> n) | (fix ^ (fix >> n)); } ``` - [ ] 解釋上述程式運作原理,應提供數學證明和 Graphviz 製作的圖解; - [ ] 練習實作其他資料寬度的 ASR,可參照 C 語言:前置處理器應用篇 撰寫通用的巨集以強化程式碼的共用程度; ### 議題 `1` :::info 解釋上述程式運作原理,應提供數學證明和 Graphviz 製作的圖解; ::: 在 x86 組語中右移運算分為兩種,邏輯右移 (Shift Logical Right, SHR) 與算術右移 (Shift Arithmetic Right, SAR)。其中邏輯位移即為常規意義上的右移運算,將位元右移並在高位元處補上對應數量的 `0` ;而算數位移在位移後則會根據原先 `MSB` 數值,補上對應數量的相同數值。 舉例來說,一八位元數 `0x80` 在經由 `SHR 1` 與 `SAR 1` 後分別為 `0x40` 與 `0xc0` 。 而題目中所述的 ==implementation-defined== 在 `gcc` 中被實作成使用 `SAR` 。簡而言之,在 `gcc` 中對有號數進行右移時會使用 `SAR` 而無號數則為 `SHR` 。 透過簡單實驗驗證。 ```cpp #include <stdint.h> #include <stdio.h> #define MAX 0xFFFFFFFF int main(void) { int32_t sar = MAX; uint32_t shr = MAX; printf("SAR: %x -> %x\n", sar, sar >> 3); printf("SHR: %x -> %x\n", shr, shr >> 3); sar &= ~(0x80000000); shr &= ~(0x80000000); printf("SAR: %x -> %x\n", sar, sar >> 3); printf("SHR: %x -> %x\n", shr, shr >> 3); return 0; } ``` 經過 gcc 編譯我們可得 ```asm _main: ## @main ... mov esi, dword ptr [rbp - 8] mov eax, dword ptr [rbp - 8] sar eax, 3 ... call _printf mov esi, dword ptr [rbp - 12] mov ecx, dword ptr [rbp - 12] shr ecx, 3 ... call _printf ... mov esi, dword ptr [rbp - 8] mov ecx, dword ptr [rbp - 8] sar ecx, 3 ... call _printf mov esi, dword ptr [rbp - 12] mov ecx, dword ptr [rbp - 12] shr ecx, 3 ... call _printf ``` 可以發現 `int32_t sar` 在位移運算上使用 `SAR` ,而 `uint32_t slr` 則是使用 `SLR` 。 由於無法肯定當前編譯器對於向右位移在有號負數上會使用 `SAR` 或是 `SLR` ,首先透過位移有號負數來檢測這件事。對應程式碼: ```cpp const int logical = (((int) -1) >> 1) > 0; ``` 考慮編譯器使用 `SHR` 的狀況。分為兩種情形 1. m 為正數,高位需補 `0` 2. m 為負數,高位需補 `1` 對應程式碼: ```cpp unsigned int fixu = -(logical & (m < 0)); ``` 這段程式碼有個需要注意的地方。當原本編譯器就使用 `SAR` 的情況直接進行位移運算即可,便不再另行處理補位位元,程式碼 `logical &` 便是在處理這種情況。 由於 `-1` 為 `0xFFFFFFFF` ,因此當 `m` 為負數時 `fixu` 為 `-1` 否則為 `0` 。 透過將其轉換為 依據 [ISO/IEC 9899:TC2](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf) 標準的 6.5.7 章第 5 節 (第 85 頁): > The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a nonnegative value, the value of the result is the integral E2 part of the quotient of E1 / 2 . ## 測驗 `2` ```cpp bool isPowerOfFour(int num) { return num > 0 && (num & (num - 1))==0 && !(__builtin_ctz(num) & 0x1); } ``` - [ ] 解釋上述程式運作原理; - [ ] 改寫上述程式碼,提供等價功能的不同實作,儘量降低 branch 的數量; - [ ] 練習 LeetCode 1009. Complement of Base 10 Integer 和 41. First Missing Positive,應善用 clz; - [ ] 研讀 2017 年修課學生報告,理解 clz 的實作方式,並舉出 Exponential Golomb coding 的案例說明,需要有對應的 C 原始程式碼和測試報告; > x-compressor 是個以 Golomb-Rice coding 為基礎的資料壓縮器,實作中也用到 clz/ctz 指令,可參見 Selecting the Golomb Parameter in Rice Coding。 ### 議題 `1` :::info 解釋上述程式運作原理; ::: 分析三組判斷式,前兩組判斷式首先判斷 `num` 需為 2 的冪次,原理如下。 $2^n$ 由 $2^0 = 1$ 起,每當 n + 1 等同於 $2^{n-1} * 2$ 。由於二進制中乘二等同於左移一位元,可知 $2^n$ 在二進制表達中永遠只會有一位元為 `1` 。判斷式 `(num & (num - 1))==0` 便是再做此一判斷,當任意數 `-1` 時,在二進制上由於借位,會呈獻成原數由右數來第一個 1 變成 0 ,而其右的所有 0 變成 1 。下表透過 8 為例展示此一特性。 | 8 | 8-1 | |-|-| |0b1000|0b0111| 因此透過 n & (n-1) 可以快速得知 n 是否只含有最多一位元為 1 ,進而得知其是否為 2 的冪次。然而其中有一例外 `0` ,由於 `0` 滿足最多一位元為 1 的條件,而又不屬於 2 的冪次方,需額外透過 `num > 0` 來處理。 最後一組判斷式則是透過判斷唯一的那一位元 1 的位置進一步確認 `num` 為 4 的冪次。如同 2 的冪次, 4 的冪次再二進制上也可以透過位移來實現, `n * 4 == n << 2` 。由於一次一次位移 2 位,因此可以推知 1 所在位元的位置的奇偶性會保持一致。若將最低位元視為 $b_1$ 此後第 n 為元為 $b_n$ 的話,可以斷言若 `num` 為 4 的冪次,且 `num` 的 $b_n$ 為 1 ,`n` 必為奇數。 題目中的 `num` 藉由 `__builtin_ctz` 計算 $b_n$ 右側之位元數,可以轉換為 `n - 1` 。根據剛才推論 `n` 必為奇數,則 `__builtin_ctz` 之運算結果在 `num` 為 4 的冪次時必為奇數,因此我們可以透過 `&0x1` 的運算進行奇偶性的判定。 透過以上三組判斷式,可以組合出以下邏輯並判斷是否為 4 的冪次。 1. 需為 2 的冪次 2. 額外處理特例 `0` 3. 2 的冪次 `n` 不為奇數 ## 測驗 `3` ```cpp int numberOfSteps (int num) { return num ? __builtin_popcount(num) + 31 - __builtin_clz(num) : 0; } ``` - [ ] 解釋上述程式運作原理; - [ ] 改寫上述程式碼,提供等價功能的不同實作並解說; >提示: [Bit Twiddling Hacks](http://graphics.stanford.edu/~seander/bithacks.html) 中提及許多 bitwise operation 的使用,如 [bit inverse](https://hackmd.io/@sysprog/ByzoiggIb)、abs 等等,是極好的參考材料。 - [ ] 避免用到編譯器的擴充功能,只用 C99 特徵及 bitwise 運算改寫 [LeetCode 1342. Number of Steps to Reduce a Number to Zero](https://leetcode.com/problems/number-of-steps-to-reduce-a-number-to-zero/),實作出 branchless 解法,儘量縮減程式碼行數和分支數量; ## 測驗 `4` ```cpp #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; } ``` - [ ] 解釋上述程式運作原理; - [ ] 在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升; ## 測驗 `5` ```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; int r = __builtin_ctzll(bitset); out[pos++] = k * 64 + r; bitset ^= t; } } return pos; } ``` - [ ] 解釋上述程式運作原理,並舉出這樣的程式碼用在哪些真實案例中; - [ ] 設計實驗,檢驗 clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 [bitmap density](http://www.vki.com/2013/Support/docs/vgltools-3.html); - [ ] 思考進一步的改進空間;