# 2020q3 Homework2 (quiz2) contributed by < `sammer1107` > ###### tags: `進階電腦系統理論與實作` > [測驗題](https://hackmd.io/@sysprog/2020-quiz2) # 測驗 1 ```cpp #include <stdbool.h> #include <stddef.h> #include <stdint.h> #include <string.h> bool is_ascii(const char str[], size_t size) { if (size == 0) return false; int i = 0; while ((i + 8) <= size) { uint64_t payload; memcpy(&payload, str + i, 8); if (payload & MMM) return false; i += 8; } while (i < size) { if (str[i] & 0x80) return false; i++; } return true; } ``` + 這題我們要檢查每個 byte 是否為 extended ASCII,對應的數字範圍為 128~255,也就是第 8 個 bit 一定要是 1 + 若我們要檢查一個 byte 中第 8 個 bit 是否為 1 ,可以把這個 byte 跟 二進位的 `10000000` 作 AND。對應到 16 進位的 `80` + 所以要一次檢查 8 個 byte ,就複製 8 次即可。只要有一個為 extended ascii,出來結果界不會為 0。 + 所以答案選 `(d) 0x8080808080808080` # 測驗 2 ```c= uint8_t hexchar2val(uint8_t in) { const uint8_t letter = in & MASK; const uint8_t shift = (letter >> AAA) | (letter >> BBB); return (in + shift) & 0xf; } ``` + `'0'`, `'1'`, `'2'`, …, `'9'` 對應到 `0x30`, `0x31`, `0x32`, … `0x39` + `'a'`, `'b'`, `'c'`, …, `'f'` (小寫) 對應到 `0x61`, `0x62`, `0x63`, …, `0x66` + `'A'`, `'B'`, `'C'`, …, `'F'` 對應到 `0x41`, `0x42`, `0x43`, …, `0x46` 1. 首先我根據名字 `letter` 推測,第 3 行應該是要判斷輸入是否屬於字母 a~f。我注意到數字的16進位表示法中開頭都為 3 ,而小寫字母則是 6 ,大寫為 4。 將 3、4、6 的二進位寫出來,如下: | 數字 | 二進位表示 | | -------- | -------- | | 3 | 0011 | | 4 | 0100 | | 6 | 0110 | 可以看出來第三個 bit 可用來區分是否為字母,故 **MASK** = `0b01000000` = `0x40` 2. 由於不清楚第 4 行的作用,我先看第五行。 + `& 0xf` 是只取最小的四個 bit 的意思,作用是確保回傳的值為 0~15。 + 如果 in 為數字的話,可以看出來單純取前四個 bit 就完成轉換了。 e.g. `'1' = 0x31`, `0x31 & f = 0x01 = 1` 故我推測只有當 letter 非 0 時,shift 才為非 0 3. 回到第 4 行,我現在的任務就是當 `letter` 不為 0,我要用他湊出能把字母轉成數字的方法。我繼續觀察目前有的資訊: + 當 in 為字母,`letter` 為 `0b01000000` + 不管小寫大寫,字母的編碼有此規律: | 字母 | lower 4 bit <br>(Decimal) | 對應數值 | | --- | --- | --- | | a | 1 | 10 | | b | 2 | 11 | | c | 3 | 12 | | d | 4 | 13 | | e | 5 | 14 | | f | 6 | 15 | a 對應到前四個 bit 為 1,b 對應到 2,後面的規律一樣。所以要讓字母對應到他們的數值,其實就只要 +9 就好了,所以 shift 應該要是 9。 + 9 的二進位為 `00001001`,故可以用 letter 向右位移 3 和位移 6 來合成,所以選擇 **AAA**=3, **BBB**=6 # 測驗 3 ## jemalloc 由於我根本看不懂題目說明,所以決定先追查到 jemalloc 的原始碼,看看有何線索。以下為 `jemalloc/src/div.c` 的註解所說(數學這裡用英文比較順): Suppose $n = qd$ for some integer $q$, we have \begin{align} \newcommand{\floor}[1]{\lfloor{#1}\rfloor} \newcommand{\ceil}[1]{\lceil{#1}\rceil} \newcommand{\naturals}[]{\mathbb{N}} &\floor{\ceil{\frac{2^k}{d}} \cdot n \cdot \frac{1}{2^k}} \\ =& \floor{\frac{2^k+r'}{d} \cdot n \cdot \frac{1}{2^k}} \\ =& \floor{\frac{n}{d}\ +\ \frac{r'}{d} \cdot \frac{n}{2^k} } \\ =& \frac{n}{d}\ +\ \floor{\frac{r'}{d} \cdot \frac{n}{2^k} } \\ \end{align} where $2^k+r' = q'd$ for some integer $r',q'$ and $r'<d$. Because $\frac{r'}{d} < 1$, if we select $k$ such that $\frac{n}{2^k} < 1$, we'll have $\frac{r'}{d} \cdot \frac{n}{2^k} < 1$ and \begin{align} \frac{n}{d}\ +\ \floor{\frac{r'}{d} \cdot \frac{n}{2^k} } = \frac{n}{d}\ . \end{align} 這告訴我們**只要今天 n 是被 d 整除且我們選擇的 k 夠大**,我們就可以用 $\ceil{\frac{2^k}{d}}$ 當作 magic number $M$。**要做除法時就做 $n \cdot M \cdot \frac{1}{2^k}$ 即可**,對應到 c 語言中,即是乘法與位移兩個動作。 但要注意的是,他假設 $n$ 是被 $d$ 整除。但我們的情況不同,因為我們要算餘數, $n$ 當然不一定會被整除,因此我們需要修改一下他的推導。 ## Our Case According to division rule, we can say that $n = qd + r$ for some $q,r \in\naturals$ and $r < d$. Then, we have \begin{align} &\floor{\ceil{\frac{2^k}{d}} \cdot n \cdot \frac{1}{2^k}} \\ =& \floor{\frac{2^k+r'}{d} \cdot n \cdot \frac{1}{2^k}} \\ =& \floor{\frac{n}{d}\ +\ \frac{r'}{d} \cdot \frac{n}{2^k} } \\ =& \floor{q + \frac{r}{d}\ +\ \frac{r'}{d} \cdot \frac{n}{2^k} } \\ =&\ q\ +\ \floor{\frac{r}{d}+\frac{r'}{d} \cdot \frac{n}{2^k} }. \end{align} The first and second equality comes from the same reasoning in the jemalloc case. The third equality comes from the fact that $\frac{n}{d} = q+\frac{r}{d}$. The final equality stands because $q$ is an integer. The final line equals to $q$ only if $\frac{r}{d}+\frac{r'}{d} \cdot \frac{n}{2^k} < 1$. ## 回到測驗題 ```c= const uint32_t D = 3; #define M ((uint64_t)(UINT64_C(XXX) / (D) + 1)) /* compute (n mod d) given precomputed M */ uint32_t quickmod(uint32_t n) { uint64_t quotient = ((__uint128_t) M * n) >> 64; return n - quotient * D; } ``` 我們可以從 shift 64 的動作看出來 $k=64$,再經過計算我們得到 $2^{64}\mod{3} = 1$ 和 $r'=2$。 而 $M$ 應該要等於 $\ceil{\frac{2^{k}}{d}} = \floor{\frac{2^{k}}{d}} + 1 = \frac{2^{k}-1}{d} + 1$,等式成立因為 $2^{64}\mod{3} = 1$。**因此答案選擇 `0xFFFFFFFFFFFFFFFF` $=2^{64}-1$**。 帶進上面的推導,我們的方法要成立必須要滿足 $\frac{r}{3} + \frac{2}{3}\cdot \frac{n}{2^{64}}$。由於 $r$ 在這裡最大是 2,而 $n$ 受型別的限制所以 $n < 2^{32}$,我們可以看出來 $\frac{r}{3} + \frac{2}{3}\cdot \frac{n}{2^{64}} < \frac{2}{3} + \frac{1}{3} = 1$,**所以演算法成立**:clap:。 ## 探討此方法的限制 **我們上面討論了演算法為何成立,那有沒有不成立的情況呢?** 我們令 $k$ 一樣是 $64$,則若我們選擇 $n \geq 2^{63}$,且這個 $n$ 對應到 $r=2$,則 \begin{align} &\frac{r}{3} + \frac{2}{3}\cdot \frac{n}{2^{64}}\\ = &\frac{2}{3} + \frac{2}{3}\cdot \frac{n}{2^{64}}\\ \geq &\frac{2}{3} + \frac{2}{3}\cdot \frac{1}{2}\\ \geq &\ 1 \end{align} 演算法就會崩潰。 為了驗證我的理論,我修改了題目程式: ```cpp #include <stdio.h> #include <stdint.h> const uint32_t D = 3; #define M ((uint64_t)(UINT64_C(0xFFFFFFFFFFFFFFFF) / (D) + 1)) /* compute (n mod d) given precomputed M */ uint64_t quickmod(uint64_t n) { uint64_t quotient = ((__uint128_t) M * n) >> 64; return n - quotient * D; } int main(void) { for (uint64_t i= ((uint64_t)1 << 63) - 10, ans=i%3; i < ((uint64_t)1 << 63)+10; ++i) { printf("i=%lu\n",i); uint64_t ret = quickmod(i); if (ans != ret) { printf("wrong ans for %lu, expect %lu but get %lu\n", i, ans, ret); } ans = (ans+1) % 3; } return 0; } ``` + 首先我改了 `quickmod` 使用的 type 為 `uint64_t`,來因應我要測試的 $n$(在 $2^{63}$上下) + 在 `main`,我從 $2^{63}-10$ 開始測試到 $2^{63}+10$ + `ans`為對應初始值的正解,可以算出之後的正解。 + 我期望在數值 $n$ 超過 $2^{63}$ 之後,會看到錯誤 **程式輸出** ``` i=9223372036854775798 i=9223372036854775799 i=9223372036854775800 i=9223372036854775801 i=9223372036854775802 i=9223372036854775803 i=9223372036854775804 i=9223372036854775805 i=9223372036854775806 i=9223372036854775807 i=9223372036854775808 wrong ans for 9223372036854775808, expect 2 but get 18446744073709551615 i=9223372036854775809 i=9223372036854775810 i=9223372036854775811 wrong ans for 9223372036854775811, expect 2 but get 18446744073709551615 i=9223372036854775812 i=9223372036854775813 i=9223372036854775814 wrong ans for 9223372036854775814, expect 2 but get 18446744073709551615 i=9223372036854775815 i=9223372036854775816 i=9223372036854775817 wrong ans for 9223372036854775817, expect 2 but get 18446744073709551615 ``` :::success :clap: Bingo! 我們看到在 $n=2^{63}$ 時,剛好 $r=2$,所以程式就出錯了。之後隨著 $n$ 愈來愈大,每次只要 $r=2$ 就會出錯,因此每 3 個 input 會有一個錯。 ::: + 其中錯誤的答案皆為 $18446744073709551615 = 2^{64}-1$,這是因為我們原本期望要得到 $q$,卻得到了 $q+1$,因此 `n - quotient * D` 原本的 $2$ 再多減了 $3$ 變成 $-1$,所以在無號數才會變成很大的數字。 **從這裡我們看到,隨著 $d$ 以及 $n$ 的範圍不同,選擇適當的 $k$ 是很重要的,否則演算法不一定會成立。** # 測驗 4 這題我嘗試沿用上面的推導但不得其門而入,所以我重新觀察解答為何成立: ```c= bool divisible(uint32_t n) { return n * M <= M-1; } ``` **什麼樣的整數乘以 $M$ 會小於 $M$ 呢?** 答案是不存在這樣的整數,要回答這題,必須把 overflow 考慮進來。由於 `M` 的 type 為 `uint64_t`,根據 **Integer Promotion** 的規則,所有的運算與比較會在 `uint64_t` 底下完成。這對應到數學中的 $\mod{2^{64}}$ 的操作。以下分成兩個情況討論: $$ \newcommand{\divs}{\mid} \newcommand{\ndivs}{\nmid} $$ 1. $d \divs n$.Let $n=3q$, then $$ \begin{align} n\cdot M &= 3q \cdot (\frac{2^{64}+2}{3})\\ &= 2^{64}q + 2q\\ &\equiv 2q \pmod{2^{64}}. \end{align} $$ Because $n<2^{32}$, we know $q<2^{32}$. Thus, $2q < 2^{33} < \frac{2^{64}}{4} < \frac{2^{64}+2}{4} < \frac{2^{64}+2}{3} = M$. 我們證明了當 $d \divs n$ , $n\cdot M < M \pmod{2^{64}}$。 3. $d \ndivs n$. Let $n=3q+r$ where $1 < r < 3$, then $$ \begin{align} n\cdot M &= (3q+r) \cdot M\\ &\equiv 2q + r\cdot M \pmod{2^{64}}. \end{align} $$ We can see that if $2q + r\cdot M < 2^{64}$,then $n \cdot M \equiv 2q + r\cdot M \geq M$. It's equivalent to prove that $r\cdot M < 2^{64}-2^{33} < 2^{64}-2q$ since $q < 2^{32}$. So, $$ \begin{align} r \cdot M &\leq 2\cdot M \\ &= 2\cdot \frac{2^{64}+2}{3} \\ &< \frac{2}{3}(2^{64}) \\ &= 2^{64}(1-\frac{1}{3}) \\ &< 2^{64}(1-\frac{1}{2^{31}}) \\ &= 2^{64} - 2^{33} \end{align} $$ 我們證明了當 $d\ndivs n$,$n\cdot M \equiv 2q+r\cdot M \geq M$。 我好像找不到方法直接推出解答,但根據上面證明 `(c) M - 1` 的確是正確答案。 這裡我也用了小測試程式測試我的推導,但沒有測試所有數字: :::spoiler ```c= #include <stdio.h> #include <stdint.h> #include <stdbool.h> const uint32_t D = 3; #define M ((uint64_t)(UINT64_C(0xFFFFFFFFFFFFFFFF) / (D) + 1)) bool divisible(uint32_t n) { return n * M <= M - 1; } int main(void) { for (uint32_t i=0; i<100; ++i) { uint32_t q = i / 3, r = i % 3; uint64_t result = i * M; if (r == 0){ if (result != (uint64_t)2*q) { printf("conjecture is wrong for i=%d=%dd+%d, result is %lu instead of %lu\n", i, q, r, result, (uint64_t)2*q); } else { printf("conjecture is correct for i=%d=%dd+%d, result is %lu\n", i, q, r, result); } } else { if (r != 0 && result != r*M+2*q) { printf("conjecture is wrong for i=%d=%dd+%d, result is %lu instead of %lu\n", i, q, r, result, r*M+2*q); } else { printf("conjecture is correct for i=%d=%dd+%d, result is %lu\n", i, q, r, result); } } } return 0; } ``` ::: # 測驗 5 ```cpp= #include <ctype.h> #include <stddef.h> #include <stdint.h> #define PACKED_BYTE(b) (((uint64_t)(b) & (0xff)) * 0x0101010101010101u) /* vectorized implementation for in-place strlower */ void strlower_vector(char *s, size_t n) { size_t k = n / 8; for (size_t i = 0; i < k; i++, s += 8) { uint64_t *chunk = (uint64_t *) s; if ((*chunk & PACKED_BYTE(VV1)) == 0) { /* is ASCII? */ uint64_t A = *chunk + PACKED_BYTE(128 - 'A' + VV2); uint64_t Z = *chunk + PACKED_BYTE(128 - 'Z' + VV3); uint64_t mask = ((A ^ Z) & PACKED_BYTE(VV4)) >> 2; *chunk ^= mask; } else strlower(s, 8); } k = n % 8; if (k) strlower(s, k); } ``` + **PACKED_BYTE**:要了解這段程式碼,最重要的是先理解 `PACKED_BYTE` 的作用。可以看到 b 先被 `&0xff`,也就是取最低的一個 byte。再來 `* 0x0101010101010101u` 的作用相當於是**將此 byte 複製 8 次**。 + 12行: 這裡我們一次抓了 8 個 byte 過來處理。 + 13行: 這裡根據提示我們是要確認這 8 個字元是否都為 ASCII。所以與**測驗1**一樣,**VV1** 應該用 `0x80` 來做 mask 。 + 由 16~17 行判斷,`mask` 的作用應該是在字元是 `'A'-'Z'` 的情況下,對應的 byte 的第6個 bit 應該要為 1,其他皆為0,如此來做到轉小寫。<br> 所以可以判斷 `(A ^ Z) & PACKED_BYTE(VV4)` 的作用是在屬於 `'A'-'Z'` 的 byte 的第 8 個位元產生一個 1 。合理判斷 **VV4** 為 `0x80`,前提是只要當字母屬於 `'A'-'Z'` 的情況下, `A` 與 `Z` 的第 8 個 bit 不同即可。 + 若我們選擇 **VV2=0** 和 **VV3=-1**,我們可以觀察到下面的規律(不用考慮 wrap around 因為我們已經去掉 extended ASCII ) | 原始區間 | 經 `+128-'A'`<br>轉換後 | 經 `+128-'Z'-1`<br>轉換後 | |--|--|--| | `<'A'` | `<128` | `<128` | | `>'Z'` | `>128` | `>128` | | `'A'-'Z'` | `>128` | `<128` | 只有 `'A'-'Z'` 區間的字元會在 bit 8 不一樣。如此就可以達成區間的判別了。 # 測驗 6 > 受到 [`RinHizakura`](https://hackmd.io/@RinHizakura/SJ5NuIANP#%E6%B8%AC%E9%A9%97-4) 的啟發,以下是我嘗試自己整理一次邏輯。 ## 觀念 + 首先每個 bit 的動作是獨立的,不會互相影響。所以我們其實是在分別計算每個 bit 的出現次數。 + 我們可以利用 $\mod{3}$ 算術來紀錄每個 bit 的出現次數。一開始為 0, 每出現一次就 $+1$, 加到3的時候又歸零。根據題目,第 $n$ 個 bit 最後累計出現的次數一定是 $i_n + 3k \equiv i_n \pmod{3}$,其中 $i$ 為只出現過 1 次的那個數字,$i_n$ 代表 $i$ 的第 $n$ 個 bit。而因為其他數字出現次數一定為 3,但他們不一定會出現在第 $n$ 個 bit,因此我用 $3k$ 來代表有 $k$ 個數字用到這個 bit。 + 若我們用 `0 => 00`,`1 => 01`,`2 => 10` 3個狀態來實作 $\mod{3}$ 算術。左邊叫 higher,右邊叫他 lower。根據題目,最後每個位置的狀態要嘛是 `0=>00`,要嘛是 `1=>01`。而取所有的 lower 反應出來的就是我們要的 $i$ 了。 ## 回到測驗題 ```c= int singleNumber(int *nums, int numsSize) { int lower = 0, higher = 0; for (int i = 0; i < numsSize; i++) { lower ^= nums[i]; lower &= KKK; higher ^= nums[i]; higher &= JJJ; } return lower; } ``` + 假設我想要透過上面的程式碼來完成下表的狀態轉換: | $H$ | $L$ | $i$ | $H_o$ | $L_o$ | | --- | --- | --- | --- | --- | | 0 | 0 | 1 | 0 | 1 | | 0 | 1 | 1 | 1 | 0 | | 1 | 0 | 1 | 0 | 0 | | 1 | 1 | 1 | X | X | | 0 | 0 | 0 | 0 | 0 | | 0 | 1 | 0 | 0 | 1 | | 1 | 0 | 0 | 1 | 0 | | 1 | 1 | 0 | X | X | $\newcommand{\xor}{\oplus}$ + 藉由把 $L_o$ 中的 X 設為 0,我們可得到 $L_o = H'L'i+H'Li' = (i\xor L)H'$,因此第 5 行做完 XOR 後,第6行做 `&= ~higher` 可以獲得我們要的結果。 + 到了第 7 行,目前 $H_o = (Hi'+H'i)f(H,L,i)$,我們需要知道 $f$ 是什麼。若 $H_o = (Hi'+H'i)$ ,真值表長這樣: | $H$ | $L$ | $i$ | $H_o$ | $L_o$ | | --- | --- | --- | --- | --- | | 0 | 0 | 1 | 1 | 1 | | 0 | 1 | 1 | 1 | 0 | | 1 | 0 | 1 | 0 | 0 | | 1 | 1 | 1 | 0 | 0 | | 0 | 0 | 0 | 0 | 0 | | 0 | 1 | 0 | 0 | 1 | | 1 | 0 | 0 | 1 | 0 | | 1 | 1 | 0 | 1 | 0 | + (a) higher => 不對,這樣根本沒變 + (b) lower => 在 第1列就不對了 + (c) ~higher => 會全零,不對 + (d) ~lower => 剛好是對的 + (e) 0xFFFFFFFF => 如表,第1列就錯了 ## 觀念推廣 + 今天如果要做 $n=4$,則我們就是要選擇一個 $m$ 並確保 $4k \mod{m} = 0$ 即可。我們可以選擇模二算術,因為 $4k \mod{2} = 0,\ \forall k\in\naturals$。事實上,當 $n$ 屬於偶數,我們都可以套用 $n=2$ 的算法。 + 如果 $n=5$,我們可以用模五算術,如此就需要用 3 個變數來實作。對於更長的奇數 $n$,我們可以取 $m$ 為 $n$ 的最小質因數。 e.g. $7$ 取 $m=7$,$9$ 取 $m=3$,$15$ 取 $m=3$。以 $n=15$ 再細部說明,我們可以看出來 $15k \mod{3} = 0$,因此我們可以使用本題的解法來解任何 $n$ 為 $3$ 的倍數的情況。 ## 延伸問題二 我使用更直覺的加法器設計,每個位置一樣使用兩個位 (lower, higher) 代表狀態: 1. 先把低位的進位加到高位 2. 再計算低位加法的結果 3. 最後把所有狀態為 11 的位置重新歸 0 這樣就完成 Mod 3 計數器了。 ```cpp int singleNumber(int* nums, int numsSize){ int lower = 0, higher = 0; for (int i = 0; i < numsSize; i++) { int null_mask; // add carry from lower higher ^= (lower & nums[i]); // add bit to lower lower ^= nums[i]; // set all 11 back to 00 null_mask = lower & higher; lower ^= null_mask; higher ^= null_mask; } return lower; } ```