# 2020q3 Homework3 (quiz3) contributed by < `quantabase13` > ## 程式運作原理 ### 測驗 1 ```c= int asr_i(signed int m, unsigned int n) { const int logical = (((int) -1) >>1/*OP1*/) > 0; unsigned int fixu = -(logical & ( m < 0/*OP2*/)); int fix = *(int *) &fixu; return (m >> n) | (fix ^ (fix >> n)); } ``` 這題的目標是要實作跨平台算術右移(arithmetic right shift, 簡稱asr)。由於在 C 語言中對有號型態的物件(且值為負) asr 屬於 `unspecified behavior` (程式行為仰賴編譯器實作和平台特性而定),因此實作過程中必須要對可能出現的情況做出補償。 此題思路如下: * 對有號型態的負數算術右移後, most significat bit (MSB) 可能為 0 或 1 。 * 根據原本的有號型態物件值是否大於0,決定是否該對算術右移後的結果做出補償。 列出表格來討論: |logical|Is signed object < 0 ?||MSB(the output of our implementation)| |-|-|-| |1|yes|1| |0|yes|1| |1|no|0| |0|no|0| 發現不論 logical 值為何,只要 signed object < 0 的話,就必須讓 MSB 等於 1 。 把 logical 去掉後,程式碼如下: ```c= int asr_i(signed int m, unsigned int n) { unsigned int fixu = -( m < 0/*OP2*/); int fix = *(int *) &fixu; return (m >> n) | (fix ^ (fix >> n)); } ``` 只要 m < 0 ,就把 (m >> n) 最左邊的 n 個 bits 設為1。 <!-- 我們可以列出真值表進行分析: |MSB(after asr in C)|Is signed object < 0 ?|MSB(the output of our implementation)| |-|-|-| |1|1|1| |0|1|1| |1|0|0| |0|0|0| --> ### 測驗 2 ```c= bool isPowerOfFour(int num) { return num > 0 && (num & (num - 1))==0 && !(__builtin_ctz(num) & 0x1 /*OPQ*/); } ``` 分為兩個方向討論: * `(num & (num -1)) == 0`: 四的冪次可以寫成 $4^n$, $$ \begin{aligned} {4^n} &= 2^{2n}\\ &= {1\overbrace{000...000}^{共 2n 個 0}} \end{aligned} $$ $$ \begin{aligned} {4^{n}-1}&=2^{2n}-1\\ &= {0\overbrace{111...111}^{共 2n 個 1}} \end{aligned} $$ 故只要 num 為4的冪次,(num & (num -1))為0。 * `!(__builtin_ctz(num) & 0x1 /*OPQ*/)`: $$ \begin{aligned} {4^n} &= 2^{2n}\\ &= {1\overbrace{000...000}^{共 2n 個 0}} \end{aligned} $$ 我們只要檢查`__builtin_ctz(num)` 是不是偶數即可,方法就是檢查其 less significant bit 是否為 1。 ### 測驗 3 ```c= int numberOfSteps (int num) { return num ? __builtin_popcount(num)/*AAA*/ + 31 - __builtin_clz(num)/*BBB*/ : 0; } ``` 閱讀 `LeetCode` 上關於 `numberOfSteps` 的說明後,可以歸納出程式碼的運作流程: ```graphviz digraph G { node [fontname = "Handlee"]; edge [fontname = "Handlee"]; draw [ label = "numberOfSteps(num)"; shape = rect; ]; check [ label = "Is num odd?"; shape = diamond; ]; point [ label = "num = num >> 1;\nstep++\ngoto numberOfSetps(num)"; shape = rect; ]; point2 [ label = "num = num - 1;\nstep++\ngoto numberOfSetps(num)"; shape = rect; ]; draw:s -> check:n; check -> point [ label = "No" ]; { rank = same check; point; } check -> point2 [label = "Yes"]; { rank=same; draw; }; } ``` 由此,我們其實可以發現: * 2 進位表示的 num 中 1 的個數就是`num = num -1` 的執行次數。 * 32 - (leading zero bit 的數目) -1 就是 `num = num >> 1` 的執行次數。 ### 測驗 4 ```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 (v/*XXX*/); return u << shift/*YYY*/; } ``` 這題的實作利用了 Binary GCD algorithm。參考了[維基百科](https://en.wikipedia.org/wiki/Binary_GCD_algorithm)上的資料後,得知可以重複利用以下四條恆等式將問題化簡: `1. gcd(0, v) = v` `2. gcd(2u, 2v) = 2·gcd(u, v)` `3. gcd(2u, v) = gcd(u, v)` `4. gcd(u, v) = gcd(|u − v|, min(u, v))` 程式碼的第一部份: ```c= for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; } ``` 對應到恆等式(1),將兩數共有的 2 的冪次提出。 程式碼的第二部份中的: ```c= while (!(u & 1)) u /= 2; ``` 和 ```c= while (!(v & 1)) v /= 2; ``` 對應到恆等式(3)。 另外 ```c= if (u < v) { v -= u; } else { uint64_t t = u - v; u = v; v = t; } ``` 對應到恆等式(4)。 過程中,`v` 存的是 `| u - v |` 的值,也就是說透過 `v` 可以確定何時離開 `while` 迴圈。 另外, `u` 存的就是還沒 shift 的 gcd。 ### 測驗 5 這題原始的實作如下: ```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; } ``` `bitmap` array 裡存了 `bitmapsize` 個 類型為`uint64_t` 的 object, ```graphviz digraph g { graph[rankdir=LR, center=true, margin=0.2, nodesep=1, ranksep=1] edge[arrowsize=1.0, arrowhead=vee] node[shape = record, fontsize=14, width=1.5, height=0.5, fixedsize=false]; subgraph cluster_0 { node1[label = "<f0> 0 |<f1> 1 |<f2>2|<f3>.\n.\n.\n|<f4>bitmapsize-1"]; label = "bitmap"; color=none; } subgraph cluster_1 { node2[label = "<f0>unit64_t"]; label = "bitset"; color=none; } subgraph cluster_2 { node3[label = "<f0>unit64_t"]; label = "bitset"; color=none; } node1:f1:e -> node2:w; node1:f4:e -> node3:w; } ``` `bitmap` 中的第 i 個 bitset ,其第 j 個 bit 對應到數字 $64 \cdot i + j$。往 `output[pos]` 存入數字時,如果此 bit 為 0,代表 $64 \cdot i + j$不會被存入入。存入 `output[pos]` 的將是下一個非0 bit 所對應的數。 以此作為基礎,可以試著分析使用 `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 = bitset & -bitset/*KKK*/; int r = __builtin_ctzll(bitset); out[pos++] = k * 64 + r; bitset ^= t; } } return pos; } ``` 改寫後的程式碼主要目的是為了去掉`判斷 bit 是否為 1` 的 for loop。 原先的程式碼必須1個 bit 1 個 bit 的檢查,直到有非 0 bit 才能將對應的數字存入 `out` ; 改良後的程式碼使用 `ctz` 一次取得非 0 bit 在 64 bits 表示中的相對位置,並將對應的數字存入 `out` 。 由於 `ctz` 是算從 least sigificant bit 向左遇到第一個 1 之前的 `0` 數量,因此若要得知下一個 1 在 64 bits 表示中的相對位置,必須利用額外的技巧才能達到目的,這就是 ```c= uint64_t t = bitset & -bitset ``` 這段程式碼的作用。 分析bitset,可分為兩種 case : 1. |bitset(1~63)|bitset(0)| |-|-| |xxx...xxx|1| 2. |bitset(1~63)|bitset(0)| |-|-| |xxx...xxx|0| * 若 bitset 屬於 case 1 ,則 -bitset 可以表示成如下: |-bitset(1~63)|-bitset(0)| |-|-| |~bitset(1~63)|1| 這樣 `bitset & -bitset` 就可表示成: |bitset & -bitset(1~63)|bitset & -bitset(0)| |-|-| |$\overbrace{000..000}^{63個 0}$|1| 如此 `bitset^=(bitset & -bitset)` 後就能利用 `ctz` 找出下一個 bit 1 的相對位置。 * 若 bitset 屬於 case 2 , 則 -bitset 可以表示如下: |-bitset(n+1~63)|-bitset(n)|-bitset(0~n-1)| |-|-|-| |~bitset(n+1~63)|1|$\overbrace{000...000}^{n個0}$| 由右數來第 n 個 bit 的位置就是原本的 bitset 第一個非 0 bit 所在位置。 `bitset^=(bitset & -bitset)` 一樣能利用 `ctz` 找出下一個 bit 1 的相對位置。