# 2020q3 Homework2 (quiz2) contributed by < `chwchao` > > [2020q3 第 2 週測驗題](https://hackmd.io/@sysprog/2020-quiz2) ###### tags: `sysprog2020` ## 測驗一 ### 題目解析 ```cpp=1 #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; } ``` 標準 ASCII 編碼以一個 byte 儲存空間,並使用 7 個 bits 進行編碼,即 00000000 至 01111111 , 因此若需判斷一字元是否為標準 ASCII 編碼,可以僅確認 MSB 是否為 0 進行判斷。 如第 18 行,即利用一字元與二進位編碼 10000000 進行 AND 運算,若該字元的 MSB 同樣為 1,AND 運算結果則會同樣是 10000000,若否則是 00000000,並可以此進行 if 判斷。 ```cpp=18 if (str[i] & 0x80) return false; ``` 回到題目程式碼,可看到在第 12 行,使用 memcpy 將 8 bytes 儲存至 payload 以一次進行八個字元的比對,因此此處可輕易判斷使用的 mask 應選擇 **`(d)` `0x8080808080808080`** ,對八個字元進行 AND 運算,唯有全數字元的 MSB 皆為 0,即皆為 ASCII 編碼,才可避免判斷為否。 ```cpp=11 uint64_t payload; memcpy(&payload, str + i, 8); if (payload & MMM) return false; ``` ### Why `memcpy` ? 由於編譯器會根據變數型別,包含其內容長度,決定變數間的運算方式和內容。若直接以 `char` 型別進行 `&` 運算,則只能單次比對 8 個位元的資料。 程式碼中的 `uint_64_t payload` 用途則是以一個 64 位元的載體,一次儲存 8 個字元進行判斷,而此處 `memcpy` 便是用於實作出將字串中連續特定 8 個字元的記憶體內容複製至 `payload` 的儲存空間中。 ### 進階使用一 定義一函式,給予一個已知長度的字串,檢測裡頭是否包含有效的英文大小寫字母。 實作程式碼 ([is_alpha.c](https://github.com/chwchao/Course-SysProg2020/blob/master/quiz02/is_alpha.c)): ```cpp=1 bool is_alpha(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); uint64_t A = payload + PACKED_BYTE(128 - 'A'); uint64_t Z = payload + PACKED_BYTE(128 - 'Z' - 1); uint64_t a = payload + PACKED_BYTE(128 - 'a'); uint64_t z = payload + PACKED_BYTE(128 - 'z' - 1); uint64_t lower_mask = ((a ^ z) & PACKED_BYTE(0x80)); uint64_t upper_mask = ((A ^ Z) & PACKED_BYTE(0x80)); if ((lower_mask | upper_mask) ^ PACKED_BYTE(0x80)) return false; i += 8; } while (i < size) { if (str[i] < 'A' || (str[i] > 'Z' && str[i] < 'a') || str[i] > 'z') return false; i++; } return true; } ``` 此程式碼使用了測驗五中的技巧以判斷一字元是否大小寫字母,詳細解釋可直接參照該題說明。 ## 測驗二 ### 題目解析 定義一函式,可將輸入的十六進位表示字元 0~9, a~f or A~F,轉換為十進位數值 0~15。 ```cpp=1 uint8_t hexchar2val(uint8_t in) { const uint8_t letter = in & MASK; const uint8_t shift = (letter >> AAA) | (letter >> BBB); return (in + shift) & 0xf; } ``` 並根據題目中引述: > 以下摘自 ASCII 表格 >* '0', '1', '2', …, '9' 對應到 0x30, 0x31, 0x32, … 0x39 >* 'a', 'b', 'c', …, 'f' (小寫) 對應到 0x61, 0x62, 0x63, …, 0x66 >* 'A', 'B', 'C', …, 'F' 對應到 0x41, 0x42, 0x43, …, 0x46 ```cpp=3 const uint8_t letter = in & MASK; ``` 首先判斷是否為英文字母,並以一變數 `letter` 儲存。由 ASCII 編碼中可發現,數字的由左第二個位元為 0,而英文為 1。 > '0': 0b00110000 > '9': 0b00111001 > 'A': 0b01000001 > 'F': 0b01000110 > 'a': 0b01100001 > 'f': 0b01100110 因此此處 **MASK 應選擇 `(c)` `0x40`**, 將該位元分離出來,若為數字則為 0b00000000,英文字母則為 0b01000000。 ```cpp=4 const uint8_t shift = (letter >> AAA) | (letter >> BBB); ``` 同樣由 ASCII 編碼中可以發現,若僅看右四位元, '0' ~ '9' 可直接對應到數值 0 ~ 9, 'a' ~ 'f' 和 'A' ~ 'F' 則是直接對應到數值 1 ~ 6,而再加 9 即為目標值。 因此此處的 `shift` 變數則是使其若為數字時為 0,英文字母則為 9 作為偏移量,所以 **AAA 選擇 `(a)` `3`**,將字母特有左二的 1 移至右四變為 0b00001000 **BBB 選擇 `(b)` `6`**,將字母特有左二的 1 移至右一變為 0b00000001 再進行 OR 運算變成 0b00001001,即為位移量 9 (若為數字則會是 0b00000000)。 ```cpp=5 return (in + shift) & 0xf; ``` 最後將輸入字元加上該位移量,並與 `0xf` 做 AND 運算取右四位元,結果即會符合函式需求。 ### Hexspeak 將 hexchar2val 擴充為允許 "0xFF", "0xCAFEBABE", "0x8BADF00D" 等字串輸入,且輸出對應的十進位數值。 實作程式碼([hexspeak.c](https://github.com/chwchao/Course-SysProg2020/blob/master/quiz02/hexspeak.c)): ```cpp=1 uint8_t hexchar2val(uint8_t in) { const uint8_t letter = in & 0x40; const uint8_t shift = (letter >> 3) | (letter >> 6); return (in + shift) & 0xf; } uint64_t hexspeak(char str[]) { int len = strlen(str); assert(len <= 18 && len > 2); assert(str[0] == '0' && (str[1] == 'x' || str[1] == 'X')); uint64_t result = 0; for (int i = len - 1; i >= 2; --i) { result |= (uint64_t)hexchar2val(str[i]) << ((len - i - 1) << 2); } return result; } ``` 首先檢查字串格式,此處限制為 `"0x"` 或 `"0X"` 開頭,且字串長度須大於 2 且不超過 18 (16 個數值字元,上限 64 位元)。 接著將字串由後往前處理,每一字元先利用題目提供 `hexchar2val` 函式轉換為 0~15 的 `uint8_t` 數值,並轉型為 `uint_64` 避免 bitwise 操作超出位元限制。 `(len - i - 1) << 2` 等同 `(len - i - 1) * 4`,目的為將個字元數值移動至其所屬的位置,最右字元須放在最右四位元,右二字元須保留最右四位元為 0,....... 以此類推。 最後再藉由 `|=` 或 `+=` 運算子將其加總,並回傳正確結果。 ## 測驗三 ### 題目解析 快速除法原理為: $$ \dfrac{n}{d} = n \times (\dfrac{2^N / d}{2^N}) $$ 並以下方程式碼實作: ```cpp=1 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; } ``` 由第七行的回傳值 `n - quotient * D` 可看出 `quotient` 為此餘除的商,即為 $\dfrac{n}{d}$。 而第六行中對 `M * n` 向右偏移 64 位元,即為除以 $2^{64}$,因此可得知 $N$ 應為 64。因此此處 **XXX 應選擇 `(f)` `0xFFFFFFFFFFFFFFFF`**,即 $2^{64} - 1$ ## 測驗四 ### 題目解析 ```cpp=1 bool divisible(uint32_t n) { return n * M <= YYY; } ``` 由上題,`M * n` 128位元數字中,左 64 位元視為整數部分,右 64 位元則視為小數部分,且此題中並無擴展並向右偏移的動作,因此可知道此處的 `n * M` 為 $\dfrac{n}{d}$ 的小數部分。另外,`M - 1` 應為 $\dfrac{1}{d}$ 的小數部分,且若能整除,不可能會有比其更小小數,因此 **YYY 應選擇 `(c)` `M - 1`** ## 測驗五 ### 題目解析 ```cpp=1 #include <ctype.h> #include <stddef.h> #include <stdint.h> #define PACKED_BYTE(b) (((uint64_t)(b) & (0xff)) * 0x0101010101010101u) /* in-place implementation for converting all characters into lowercase. */ void strlower(char *s, size_t n) { for (size_t j = 0; j < n; j++) { if (s[j] >= 'A' && s[j] <= 'Z') s[j] ^= 1 << 5; else if ((unsigned) s[j] >= '\x7f') /* extended ASCII */ s[j] = tolower(s[j]); } } /* 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); } ``` #### `strlower` 根據 ASCII 編碼,可以發現大小寫同一英文字母的十進位差為 32,即二進位的 0010 0000。 > 'A' 的二進位編碼為 0100 0001,'a' 則為 0110 0001 > 'Z' 的二進位編碼為 0101 1010,'z' 則為 0111 1010 因此在 `strlower` 函式中,當確認一字元範圍在 A~Z 中,則直接利用 0010 0000 做 XOR 運算,將字元中由右第三位元改為 1,結果即為該英文字母的小寫。而若字元為擴展 ASCII 中的編碼,則使用 C 提供的 `tolower` 函式進行轉換。 #### `strlower_vector` ```cpp=23 uint64_t *chunk = (uint64_t *) s; ``` 此處使用類似測驗一中 `payload` 的手法進行實作,並以 `(uint_64_t *)` 的強制轉型取代 `memcpy`,目的上同樣為以 8 個字元的同時比對提升效率。 ```cpp=24 if ((*chunk & PACKED_BYTE(VV1)) == 0) { /* is ASCII? */ ``` 接著先以一判斷式辨別該字串是否皆為標準 ASCII 編碼,此處使用測驗一中使用的技巧,和 0x80 進行 AND 運算以辨認 MSB 是否為 0,因此 **`VV1` 應填入 `(e)` `0x80`** ```cpp=27 uint64_t mask = ((A ^ Z) & PACKED_BYTE(VV4)) >> 2; *chunk ^= mask; ``` 首先看到此判斷式中的最後一行,可看到最終須對 mask 做 XOR 運算,由 `strlower` 函式中的原理可以知道,此處若一字元為大寫英文字母,`mask` 須為 0b00100000,反之則為 0b00000000。 再由上方行可看到,`mask` 在計算後被右移了兩位,因此可重新調整條件:若一字元為大寫英文字母,`(A ^ Z) & PACKED_BYTE(VV4)` 須為 0b10000000,反之則為 0b00000000。 ```cpp=25 uint64_t A = *chunk + PACKED_BYTE(128 - 'A' + VV2); uint64_t Z = *chunk + PACKED_BYTE(128 - 'Z' + VV3); ``` 此兩變數宣告則是作為該字元是否為大寫英文字母的判斷範圍, `128 - 'A'` 為字元 'A' 到 0b10000000 的距離,若字元為大寫字母,即 >= 'A',則應 >= 128 `128 - 'Z'` 為字元 'Z' 到 0b10000000 的距離,若字元為大寫字母,及 <= 'Z',則應 <= 128 而此處若字元在 A~Z 中,應要使 `A` 和 `Z` 其一小於 128,另一大於等於 128,以在最大位元進行 XOR 運算後為 1。 因此 **VV3 應選擇 `(a)` `-1`**,避免字元為 'Z' 時`Z`等於 128,因為此時 `A` 必須大於 128。 而 **VV2 應選擇 `(b)` `0`**,若為 1,將多判斷 '@' 字元,若為 -1,則無法判斷 'A' 字元。 最後 **VV4 則選擇 `(e)` `0x80`**,用以分離出 MSB。 ### char str[] or char *str ? 在修改題目提供程式碼中 `char str[]` 為 `char *str` 後,執行時出現`bus error` 的錯誤。 :::danger 文字訊息不要用圖片去展現! :notes: jserv ::: 在 ISO/IEC 9899 的第 494 頁的 undefined behavior 列表中有提到 > — The program attempts to modify a string literal (6.4.5) 在 ISO/IEC 9899 的第 130 頁 (6.7.8) >EXAMPLE 8 The declaration > >**char s[] = "abc", t[3] = "abc";** > >defines ‘‘plain’’ char array objects s and t whose elements are >initialized with character string literals. > >This declaration is identical to >**char s[] = { 'a', 'b', 'c', '\0' },** > >**t[] = { 'a', 'b', 'c' };** > >The contents of the arrays are modifiable. On the other hand, the >declaration > >**char \*p = "abc";** > >defines p with type ‘‘pointer to char’’ and initializes it to point to an object with type ‘‘array of char’’ with length 4 whose elements are initialized with a character string literal. If an attempt is made to use p to modify the contents of the array, the behavior is undefined. 不難看出,在 `char *p = "abc";` 的操作上,`"abc"` 是一系統自動分配且無法修改的 string literal,而 `p` 則僅是儲存此區段的記憶體位置,而非如 `memcpy` 的操作是另外複製內部內容,因此若試圖今天要讓程式修改一 string literal 本身的內容,此一行為屬於 undifined behavior,因此此處才會出現錯誤。 ## 測驗六 ### 題目解析 >Given a non-empty array of integers, every element appears three times except for one, which appears exactly once. Find that single one. ```cpp=1 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; } ``` #### 討論出現兩次的情況 若提議只要求單一元素最多出現兩次,此時可僅以一變數儲存各 bit 的出現情況,即有 (1) 或無 (0),因此只需利用簡單 XOR 運算實作即可,範例如下([singleNumber_two.c](https://github.com/chwchao/Course-SysProg2020/blob/master/quiz02/singleNumber_two.c)): ```cpp=1 int singleNumber(int *nums, int numSize) { int result = 0; for (int i = 0; i < numSize; ++i) result ^= nums[i]; return result; } ``` 由以上範例可看出,對於每一位元的出現次數,藉由 XOR 0 會是其本身,並且其本身 XOR 其本身會回歸 0,以此方式去找出最後僅出現一次的位元組合。 #### 討論本題出現三次的情況 由上方討論,我們利用單一位元的 0、1 兩個狀態,來表示出現次數,因此延伸思考,是否當最高次數為三次時,可以用兩個位元來進行表示呢? 因此我們試圖實作以下方式: * 當尚未出現 (初始狀態):00 * 出現第一次:01 * 出現第二次:10 * 出現第三次:11,但這時因為已達上限,可將其進行處理回歸為 00 #### 回到題目原始碼 ```cpp=3 int lower = 0, higher = 0; ``` 在程式碼中可以看到兩個變數 `higher`、`lower`,用於儲存如上方所述兩位元表示的狀態,並且初始值均為 0。 ```cpp=5 lower ^= nums[i]; ``` 當一位元在特定位置出現第一次時,和判斷兩次的方式一樣,用 XOR 將此次數紀錄至 `lower` 低位元。 ```cpp=6 lower &= KKK; ``` 而回顧我們的目標, 當 `lower` 為 0,會發生在記錄到第二次出現的情況,但此 `&=` 運算子將不會對其造成影響,略過。 當 `lower` 為 1,會發生在第一次和第三次出現,而兩時間點 `higher` 的值會分別是 0 和 1,我們希望當此時 `higher` 已為 1 時,要將 `lower` 歸至 0,可視為以下判斷式: ```cpp if(higher) lower = 0; ``` 因此此處 **KKK 應選擇 `(c)` `~higher`**,當 `higher` 為 1 時,使 `lower` 為 0。 ```cpp=7 higher ^= nums[i]; higher &= JJJ; ``` 接著看到 `higher` 的處理,注意此處 `lower` 的狀態皆為已處理完畢,並由於是 XOR 運算,現在也只考慮當有 input (某數字出現特定次數) 的情況。 可條列出以下情況: * 出現第一次: `lower` 為 1,`higher` 原為 0,結果須為 0 * 出現第二次: `lower` 為 0,`higher` 原為 0,結果須為 1 * 出現第三次: `lower` 為 0 (已歸零),`higher` 原為 1,結果須為 0 在第七行運算完後,則是以下情況 (已假設 `nums[i]` 為 1): * 出現第一次: `lower` 為 1,`higher` 為 1,結果須為 0 * 出現第二次: `lower` 為 0,`higher` 為 1,結果須為 1 * 出現第三次: `lower` 為 0,`higher` 為 0,結果須為 0 因此此處 **JJJ 即可選出為 `(d)` `~lower`**,當 `lower` 為 1 時,使 `higher` 為 0。 ### 其他解題思路 由於每數最多出現 3 次,且會有一數僅出現 1 次,因此可以推測,若加總判斷特定位元位置上的所有 bit 值,並對 3 餘除,應可僅得到 1 或 0,且正好為該單一數的該位元值,因此可改寫 ([singleNumber_three.c](https://github.com/chwchao/Course-SysProg2020/blob/master/quiz02/singleNumber_three.c)): ```cpp=1 int singleNumber(int *nums, int numSize) { int result = 0; for (int i = 0; i < 32; ++i) { int sum = 0; for (int j = 0; j < numSize; ++j) sum += (nums[j] >> i) & 1; result += (unsigned)(sum % 3) << i; } return result; } ``` 即將一特定 bit 加總後餘除 3,並將其加總至最終結果的正確 bit 上。 以下為在 [LeetCode 137. Single Number II](https://leetcode.com/problems/single-number-ii/) 的執行結果。 ![](https://i.imgur.com/Y5H9S1b.png) ### 實作可擴展次數 實作程式碼 ([singleNumber_n.c](https://github.com/chwchao/Course-SysProg2020/blob/master/quiz02/singleNumber_n.c)): ```cpp=1 int singleNumber(int *nums, int numsSize, int time) { int result = 0; for (int i = 0; i < 32; ++i) { int sum = 0; for (int j = 0; j < numsSize; ++j) sum += (nums[j] >> i) & 1; result += (sum % time) << i; } return result; } ``` 若利用上方餘除的方式進行判斷,只需要改變除數即可將限制次數擴展至任意數,因此在函式中再額外新增了一參數作為除數。 :::warning TODO: 思考避免 modulo 運算的實作 :notes: jserv :::