# 2020q3 Homework3 (quiz3) contributed by < `ptzling310` > > [2020q3 第 3 週測驗題](https://hackmd.io/@sysprog/2020-quiz3) ## 測驗 `1` : 有號整數的跨平台 ASR 實作 依據 ISO/IEC 9899:TC2 (即 C99) 標準的 6.5.7 章節 (第 84 到第 85 頁): >“The result of `E1 >> E2` is E1 right-shifted E2 bit positions. … If E1 has a signed type and a negative value, the resulting value is implementation-defined.” ```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)); } ``` - [x] OP1: `>> 1` - [x] OP2: `m < 0` ### 程式解析 ```c=3 const int logical = (((int) -1) OP1) > 0; ``` 首先這邊先判目前所在的平台是採算數右移或是邏輯右移,藉由 `-1` 來測試。 所以 `OP1= >>1`。 1. 算術右移: (-1) >> 1 = (1111)~2~ , logical = 0 2. 邏輯右移: (-1) >> 1 = (0111)~2~ , logical = 1 ```c=4 unsigned int fixu = -(logical & (OP2)); ``` 接著判斷輸入的 `m` 是否需要進行 ASR 的修正, 如果 `m > 0` ,表示 `m` 是正數,不論平台做邏輯或算數右移,其結果都是對的。 若`m < 0`,表示 `m` 是負數,若 `logical == 1`,就要對該數右移的結果進行修正。所以`OP2 = ( m < 0 )`。 故 `logical & (m < 0)` 之結果就可能為 (0000 0000)~2~ 或者 (0000 0001)~2~,為了要讓最後產生 1 在 MSB 的位置, 所以在 `logical & (m < 0)` 前面加上 `-`。 最後 `fixu` 會有兩種可能,分別為 (0000 0000)~2~ = 0 或者 (1111 1111)~2~ = (-1)。 ```c=5 int fix = *(int *) &fixu; ``` 就結果來看, `fix` 與 `fixu` 所存的值應該是一樣的,那為何要不要直接寫 `int fix = = -(logical & (m < 0));` ? 參照 `jserv` 在 `RinHizakura` 中的[回覆](https://hackmd.io/@RinHizakura/SkjPS8vBP#測驗-1):避免**編譯器進行過度 (aggressive) 最佳化** :::warning TODO:過度最佳化會帶來的影響 ::: ```c=6 return (m >> n) | (fix ^ (fix >> n)); ``` 在 return 這階段來做右移修正, 當`m >> n` 後, m 的二進制的最左 n 個 bit 會為 0 , 若 m 為負數時,這些最左 n 個 bits 應為 1 ,故藉由`(fix ^ (fix >> n))` 來產生所需要的 n 個 1 補在 `m >> n` 的最左 n 個 bits。 ### 數學證明和 Graphviz 製作的圖解 :::warning Question: 會有哪一類型的圖產生? > 可參照 [Swift: Advanced Operators](https://docs.swift.org/swift-book/LanguageGuide/AdvancedOperators.html) 精美的圖示,用來說明算術位移,你可搭配數學證明解釋通用 (generic) 的狀況 > :notes: jserv ::: #### logical 變化 - 考慮平台為**邏輯右移** ```graphviz digraph structs { node[shape=record] int[label="(int) -1",shape=plaintext] struct_1 [label="<f0> 1|<f1> 1|<f2> ... | <f3>1 | <f4> 1"]; {rank=same; int struct_1} int2[label=">>1",shape=plaintext] struct_12 [label="<f0> 0|<f1> 1|<f2> ... | <f3>1 | <f4> 1"]; {rank=same; struct_12 int2} struct_1:<f0>->struct_12:<f1> struct_1:<f1>->struct_12:<f2> struct_1:<f3>->struct_12:<f4> } ``` 所以右移後的結果 `>0` ,故 `logical = 1` ```graphviz digraph structs { node[shape=record] int[label="logical",shape=plaintext] struct_1 [label="<f0> 0|<f1> 0|<f2> ... | <f3>0 | <f4> 1"]; } ``` #### fixu 變化: 假設 m = -5 因為 `logical = 1` 且 `m = -5 < 0` ,`logical & m < 0 ` 為 `1`。 故 `fixu =` (1...1)~2~。 ```graphviz digraph structs { node[shape=record] int[label="input1: logical = 1",shape=plaintext] struct_1 [label="<f0> 0|<f1> 0|<f2> ... | <f3>0 | <f4> 1"]; {rank=source; int struct_1} int2[label="input2: m < 0 = 1",shape=plaintext]; struct_2 [label="<f0> 0|<f1> 0|<f2> ... | <f3>0 | <f4> 1"]; {rank=max ; int2; struct_2} result [label="<f0> 0|<f1> 0|<f2> ... | <f3>0 | <f4> 1"]; res[label="logical & (m<0)",shape=plaintext]; {rank=same ; result res} struct_1:f4 -> result:f4 struct_2:f4 -> result:f4 } ``` 最後 `fixu` 是上圖結果再取 `-`。 ```graphviz digraph structs { node[shape=record] int2[label="fixu",shape=plaintext]; struct_2 [label="<f0> 1|<f1> 1|<f2> ... | <f3>1 | <f4> 1"]; {rank=max ; int2; struct_2} } ``` #### fix 變化: 其實就是 `fixu` 的值,但 `fix` 為 signed。 ```graphviz digraph structs { node[shape=record] int2[label="fix",shape=plaintext]; struct_2 [label="<f0> 1|<f1> 1|<f2> ... | <f3> 1| <f4> 1"]; {rank=max ; int2; struct_2} } ``` #### 求最後的結果 假設 m = -5 , n = 1 ```graphviz digraph structs { label="m >> n" node[shape=record] int[label="(int) -5",shape=plaintext] struct_1 [label="<f0> 1| <f1> ... |<f2> 1|<f3> 0| <f4> 1 | 1"]; {rank=same; int struct_1} int2[label="-5 >> 1",shape=plaintext] struct_12 [label="0|<f0> 1| <f1> ...|<f2>1|<f3> 0|<f4>1"]; {rank=same; struct_12 int2} struct_1:f0->struct_12:f0 struct_1:f1->struct_12:f1 struct_1:f2->struct_12:f2 struct_1:f3->struct_12:f3 struct_1:f4->struct_12:f4 } ``` ```graphviz digraph structs { label="fix >> n" node[shape=record] int[label="fix",shape=plaintext] struct_1 [label="<f0> 1| <f1> ... |<f2> 1|<f3> 1| <f4> 1 | 1"]; {rank=same; int struct_1} int2[label="fix >> 1",shape=plaintext] struct_12 [label="0|<f0> 1| <f1> ...|<f2>1|<f3> 1|<f4>1"]; {rank=same; struct_12 int2} struct_1:f0->struct_12:f0 struct_1:f1->struct_12:f1 struct_1:f2->struct_12:f2 struct_1:f3->struct_12:f3 struct_1:f4->struct_12:f4 } ``` ```graphviz digraph structs { label="fix ^ (fix >> n)" node[shape=record] int[label="input1: fix",shape=plaintext] struct_1 [label="<f0> 1|<f1> 1|<f2> ... | <f3>1 | <f4> 1"]; {rank=source; int struct_1} int2[label="input2: fix>>1",shape=plaintext]; struct_2 [label="<f0> 0|<f1> 1|<f2> ... | <f3>1 | <f4> 1"]; {rank=max ; int2; struct_2} result [label="<f0> 1|<f1> 0|<f2> ... | <f3>0 | <f4> 0"]; res[label="fix ^ (fix >> 1)",shape=plaintext]; {rank=same ; result res} struct_1:f0 -> result:f0 struct_2:f0 -> result:f0 } ``` ```graphviz digraph structs { label="(m>>n) | fix ^ (fix >> n)" node[shape=record] int[label="input1: -5 >> 1",shape=plaintext] struct_1 [label="<f0>0| <f1>1| ...|<f2>1| 0|<f3>1"]; {rank=source; int struct_1} int2[label="input2: fix ^ (fix >> 1)",shape=plaintext]; struct_2 [label=" <f0>1| 0|... | 0 | 0|0"]; {rank=max ; int2; struct_2} result [label=" <f0>1| <f1>1| ... | <f2>1 | 0|<f3>1"]; res[label="(-5>>1) | fix ^ (fix >> 1)",shape=plaintext]; {rank=same ; result res} struct_2:f0 -> result:f0 struct_1:f1 -> result:f1 struct_1:f2 -> result:f2 struct_1:f3 -> result:f3 } ``` ### 練習實作其他資料寬度的 ASR,可參照 [C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor) 撰寫通用的巨集以==強化程式碼的共用程度== ::: warning Question:其他寬度的意思? > 指 int16, int32, int64, 等等。width 就是「資料寬度」,不要因為讀了一堆對岸文章,就忘了繁體中文怎麼書寫 > :notes: jserv ::: - 巨集 Macro [巨集簡介](https://openhome.cc/Gossip/CGossip/Macro.html) - 常見的前置處理器指令 1. `#include` 2. `#define` - `#define`: `#define 巨集名稱 算式` - 被定義的內容就是巨集( macro ) - 常用來定義一個模板,取代常用的程式片段 - `#define` 若為多行,需在每一行最後加上`\` - 會定義在 `#include` 以及 `main()` 中間 - `\`後不可有空格,否則會出現 `warning: backslash and newline separated by space` - 改寫 :::spoiler 此版本程式雖然使用了 Macro ,卻沒程式碼共用!(因為我把不同 data width 都分開打) ```c= #include <stdio.h> #include <stdlib.h> #include <stdint.h> int16_t asr_16(int16_t m, unsigned int n) { const int16_t logical = (((int16_t) -1) >>1 ) > 0; uint16_t fixu = -(logical & (m<0)); int16_t fix = *(int *) &fixu; return (m >> n) | (fix ^ (fix >> n)); } int32_t asr_32(int32_t m, unsigned int n) { const int32_t logical = (((int32_t) -1) >>1 ) > 0; int32_t fix = *(int *) &fixu; return (m >> n) | (fix ^ (fix >> n)); } int64_t asr_64(int64_t m, unsigned int n) { const int64_t logical = (((int64_t) -1) >>1 ) > 0; uint64_t fixu = -(logical & (m<0)); int16_t fix = *(int *) &fixu; return (m >> n) | (fix ^ (fix >> n)); } #define asr_i(m,n) \ _Generic((m), \ int16_t: asr_16, \ int32_t: asr_32, \ int64_t: asr_64 \ )(m,n) int main() { int16_t a = 8; int32_t b = 64; int64_t c = 128; printf("asr_i(8,1)= %d\n",asr_i(a,1)); printf("asr_i(64,2)= %d\n",asr_i(b,2)); printf("asr_i(128,3)= %ld\n",asr_i(c,3)); return 0; } ``` ::: ```c= #include <stdio.h> #include <stdlib.h> #include <stdint.h> #define asr_i(m,n) \ _Generic((m), \ int16_t: asr_16, \ int32_t: asr_32, \ int64_t: asr_64 \ )(m,n) #define asr(type) \ const type logical = (((type) -1) >>1 ) > 0; \ u##type fixu = -(logical & (m<0)); \ type fix = *(type *) &fixu; \ return (m >> n) | (fix ^ (fix >> n)) int16_t asr_16(int16_t m, unsigned int n) { asr(int16_t); } int32_t asr_32(int32_t m, unsigned int n) { asr(int32_t); } int64_t asr_64(int64_t m, unsigned int n) { asr(int64_t); } int main() { int16_t a = 8; int32_t b = 64; int64_t c = 128; printf("asr_i(8,1)= %d\n",asr_i(a,1)); printf("asr_i(64,2)= %d\n",asr_i(b,2)); printf("asr_i(128,3)= %ld\n",asr_i(c,3)); return 0; } ``` 結果 ``` asr_i(8,1)= 4 asr_i(64,2)= 16 asr_i(128,3)= 16 ``` --------- ## 測驗 `2` :LeetCode [342. Power of Four](https://leetcode.com/problems/power-of-four/description/) ```c bool isPowerOfFour(int num) { return num > 0 && (num & (num - 1))==0 && !(__builtin_ctz(num) OPQ); } ``` - [x] OPQ: `& 0x1` 分別來看 3 個判斷條件: 1. `num > 0`:因為 $4^n$ > 0 2. `(num & (num - 1))==0`:因為 $4^n = (2^2)^n = 2^{2n}$,所以若 `num` 是 4 的冪,則必為 2 的冪,其二進位必定長得像 (1xx...x)~2~,`(num - 1)` 長得會像 (011...1)~2~, 兩者在做`&`則必為 0。 4. `!(__builtin_ctz(num) OPQ)`:`__builtin_ctz(num)` 是回傳 `num` 的二進制的尾巴有幾個 `0`。 觀察下面幾個$4^n$: | $4^n$ | 二進制 | | -------- | -------- | | $4^1$ | (100)~2~ | | $4^2$ | (10000)~2~ | | $4^3$ | (1000000)~2~ | | $4^4$ | (100000000)~2~ | 觀察到尾數 `0` 的個數的變化是 2 → 4 → 6 → 8 … ,也就是說 0 的尾數應該要是**偶數個**。 故 `(__builtin_ctz(num) OPQ)` 判斷 `__builtin_ctz(num)` 的 0 的個數為**奇數個**。而奇數的特徵就是,其 b~0~(LSB) 為 `1`,利用 `&`(`0..01`)~2~ 來讀取 b~0~看是否為 1 。 得OPQ = `& 0x1`。 ### 改寫上述程式碼,提供等價功能的不同實作,儘量降低 branch 的數量 - 觀察 $4^n$ 1. 只有一個 bit 1,可利用`__builtin_popcount`來確認。而且也可以排除`0`的情況,就可以不用做`num > 0`。 2. 且 bit 1 會位在 b~偶數~ 的位置(這樣後面一定跟著偶數個 0 ),所以會在 b~2~、b~4~、b~6~......等位子。利用 `0x55555555` = (`0101 0101 ... 0101`)~2~ 與 `num` 做 `AND` 來確認 bit 1 是否位在 b~偶數~。 ```c bool isPowerOfFour(int num){ return __builtin_popcount(num) == 1 && (num & 0x55555555); } ``` ### LeetCode [1009. Complement of Base 10 Integer](https://leetcode.com/problems/complement-of-base-10-integer/) - 求補數,即是把 input 二進制中的0↔1。 - 把某一個數與(`1...1`)~2~ 做`XOR`即可得 0 、 1 互換。 > input = 5 = (0101)~2~ > input ^ (1111)~2~ = (1010)~2~ = -6 > expect output = 2 = (010)~2~ 所以應該是要找到最左的 bit 1 後才做`XOR`! - 所以我們先找出由左至右有幾個 0 (利用`__builtin_clz`),再針對這些 0 以後的 bits 做`XOR`。 ```c int bitwiseComplement(int N){ if (N==0) return 1; unsigned int mask = 0xffffffff ; mask = mask >> __builtin_clz(N); return mask ^ N; } ``` ![](https://i.imgur.com/DkxLkN2.png) ### LeetCode [41. First Missing Positive](https://leetcode.com/problems/first-missing-positive/) - 若第 `i` 個元素不等於 `i+1`, `i+1` 就是要找的。 - 若第 `i` 個元素皆等於 `i+1`,則回傳 `numsSize+1`。 ```c int firstMissingPositive(int* nums, int numsSize){ if(nums==NULL || numsSize==0) return 1; //欲查看 nums 中的每個數字 for(int i = 0; i<numsSize; i++){ //值為正數,且未超過範圍(1~numsSize) if(nums[i] > 0 && nums[i]<numsSize ){ //當 nums[i] != nums[nums[i] - 1 if(nums[i] != nums[nums[i] - 1]){ //交換nums[i]跟nums[nums[i]-1] int j = nums[i]-1; int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; i--; } } } //值不對 for(int i = 0 ; i<numsSize; i++) { if(nums[i] != i+1) return i+1; } //皆正確 return numsSize+1; } ``` ![](https://i.imgur.com/cGeeBt2.jpg) ### 研讀 [2017 年修課學生報告](https://hackmd.io/@3xOSPTI6QMGdj6jgMMe08w/Bk-uxCYxz),理解 clz 的實作方式,並舉出 Exponential Golomb coding 的案例說明,需要有對應的 C 原始程式碼和測試報告 :::warning TODO ::: ------------- ## 測驗 `3`: LeetCode 1342. Number of Steps to Reduce a Number to Zero 計算出將 number 藉由 `/2` 以及 `-1` 計算至 0 的步驟數。 假設 `int` 寬度為 32 位元 ```c int numberOfSteps (int num) { return num ? AAA + 31 - BBB : 0; } ``` - [x] AAA = `__builtin_popcount(num)` - [x] BBB = `__builtin_clz(num)` 假設 `num` 為 32 bits ,假設每個 bit 都會做右移(`/2`),則至少做 32-1 次 (因為最後一個留下的 bit 並不用在右移,頂多只要 -1),但如果在 `num` 中 b~n~ = 1 時,必須多花一個步驟做 `-1` ,所以 AAA 應為 `__builtin_popcount(num)`。 如: (00010010)~2~),最左只會做到最左邊的那個 1 就會停止,則不用再做右移,我們就將不用右移的次數扣除,所以 BBB 為 `__builtin_clz(num)`。 --------- ## 測驗 `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 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; } ``` - [x] XXX = `v` - [x] YYY = `u << shift` ```c=5 for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; } ``` `for` 的終止條件是 `!((u | v) & 1` ,表 `u` 或 `v` 之 b~0~ 不為 1,才進入`for`迴圈,簡單來說就是否兩者皆為偶數。這邊是進行 $gcd(u,v) = 2 \times gcd(\dfrac{u} {2},\dfrac{v}{2}) = 2 \times gcd(u',v')$ 。利用 `shift` 來記錄有幾個 2 要乘在 $gcd$ 前 (可利用左移來完成!)。所以接下還的 $gcd(u',v')$ 頂多$u'$ **或** $v'$ 是偶數。 ```c=8 while (!(u & 1)) u /= 2; ``` 如果 $u'$ 是偶數,則一樣`/2`,進行$gcd(u',v')=gcd(\dfrac{u'}{2},v')$。 ```c=10 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; ``` Line 11 如果 $v'$ 是偶數,則`/2`,進行$gcd(u',v')=gcd(u',\dfrac{v'}{2})$。 所以 line 11 後,$gcd$ 內的數**都是奇數**了! 再來又分兩種情況,一個是 `u < v` ,此時我們就不斷將 `v - u`;另一個是 `u > v`,我們利用變數 `t` 的幫助,轉換為 `u < v` 的 case 去做。而 `whiel` 的結束點就是 `v`,所以 XXX = `v` 。而 `u` 會記錄在一連串輾轉相處法的過程中所得出的最大公因數。 但在 line 5 ~ 7 中,我們有利用 `shift` 來記錄我們要補回幾次 2 ,這樣才會是正確結果。故 YYY = `u << shift`。 ----------- ## 測驗 `5` : 找 bit array (aka bit map, bit set, bit string, or bit vector) 中 b~n~=1 的位址 ### 版本一 ```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; } ``` 參數: - `uint64_t` *bitmap : bitmap point to a bit array. - `size_t bitmapsize` : 有 bitmapsize 個 bitmap ,每一個 bitmap 有 64 bits 。 - `uint32_t *out` : 要將結果儲存在 out 中 程式: ``` for (int i = 0; i < 64; i++) { if ((bitset >> i) & 0x1) out[pos++] = p + i; ``` 從這段可以看出來,我們每次都是去比較 1 個 bit (每次把 bitset 往右移動,在取 b~0~ 看是否為 1 ),當遇到 1 時,我們就把這個位子記錄到 out 中。 不過這樣的處方法,可以再改進,最右的 bit 0 個數如果很多,就乾脆都不要看,最快的方法是我們直接找到最右邊的 bit 1 ,且紀錄位子。 此作法及可透過 `__builtin_ctzll` 來達成。 ### 版本二:透過 ctz 改寫 ```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; } ``` - [x] KKK = `bitset & -bitset` - 程式理解 ```c=4 size_t pos = 0; uint64_t bitset; for (size_t k = 0; k < bitmapsize; ++k) { bitset = bitmap[k]; ``` 藉由 `k` 來記錄我們應該確認的 bitmap (共 bitmapsize 個)。 `bitset` 就是一次取一個 bitmap 來看。 ```c=10 int r = __builtin_ctzll(bitset); out[pos++] = k * 64 + r; ``` `__builtin_ctzll` : Returns the number of ==trailing 0-bits== in x, starting at the ==least significant bit== position. If x is 0, the result is undefined. 所以透過 `builti_ctzll` 我可以推算出**最右的 bit 1** 在哪個位子。 ``` x = (1101 1000) builti_ctz(x) = 3 則最右的 bit 1 會位於第 3+1=4 個位子 ``` ```c=12 bitset ^= t; ``` 為了找到第二個 bit 1 ,那會希望可以將第一個 bit 1 先忽略掉,所以這裡會希望能夠將**目前最右的 bit 1** 設置為 `0`,而其他不變。 故 t 希望為 `(0...0 1 0..0`)~2~ 而 1 的位子正好是與目前最右 bit 1 的位子相同。 ```c=9 uint64_t t = KKK; ``` 所以 KKK = `bitset & -bitset` , 因為 `-bitset` 的結果是由右至左,遇到第一個 bit 1 保留,其餘的 0 ↔ 1 。 ``` bitset 1101 1000 &-bitset 0010 1000 -------------------- 0000 1000 ``` ----------- ##### 時間表 - [x] 2020/09/28 測驗 `1`、`2`、`3` 程式理解。 - [x] 2020/09/29 測驗 `4` 程式理解、[浮點數運算](https://hackmd.io/@sysprog/c-floating-point#Uneven-density-of-the-floating-point-numbers) - [x] 2020/09/30 測驗 `5` 程式理解、測驗 `1` 延伸問題_2 - [x] 2020/10/01 測驗 `1` 延伸問題_3、[C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor) - [x] 2020/10/02 測驗 `2` 延伸問題_1、2 ###### tags: `sysprog2020`