# 2020q3 Homework2 (quiz2) contributed by < `RainbowEye0486` > ## 目錄 [TOC] ## 作業區 ### 測驗一 首先了解題目之前的敘述,ASCII碼表示的是128個字元,因此我們的辨識方式就是觀察編碼的範圍是否有超過128,超過的話,有可能是 extended ASCII code 。透過以下程式碼: ```clike= #include <stddef.h> bool is_ascii(const char str[], size_t size) { if (size == 0) return false; for (int i = 0; i < size; i++) if (str[i] & 0x80) /* i.e. (unsigned) str[i] >= 128 */ return false; return true; } ``` 解讀程式碼,我們知道傳入的是一個字元陣列,透過 for 迴圈一個一個檢查字串中每個字元是否符合 ASCII code (也就是在128之內),關鍵便是 `str[i] & 0x80` 這行,透過 AND 的方式能夠知道最高 bit 是否是1(相當於實作一個 bitmask ),因為`0x80`代表++1000++ ++0000++,也就是128或以上的數 `&` 都不會是 ++0000++ ++0000++,因此直接回傳 `false` 代表字串中至少出現一個或以上的字元不符合 ASCII 標準的字元。 接著,回到正式的題目: ```clike= #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; } ``` 我們希望能夠一次比對更多的字元(64位元),也就是一個word的大小,也就是8個字元,因此在 ```clike=10 while ((i + 8) <= size){ ... ``` 就是說一次抓8個字元比較,剩下不足8個的部分再17~22行依照一開始的方式比對。 按照原本的方式比較字串,會是以下表格呈現 | \迴圈數 |1|2|3|4|5|6|7|8| | --- | --- | --- | --- | --- | --- | --- | --- | --- | |**比較值**|0x80|0x80|0x80|0x80|0x80|0x80|0x80|0x80| |**字串**|'c'|'o'|'m'|'p'|'l'|'e'|'t'|'e'| 改寫過後的版本將八個字元合併在一起比較,只要任何一個字串的編碼值大於128,整串64 bit 二進制得到的無號數字必定不等於0,因此回傳 `false` ,如下表格: | \迴圈數 |1| | | | | | | | | --- | --- | --- | --- | --- | --- | --- | --- | --- | |**比較值**|0x80|80|80|80|80|80|80|80| |**字串**|'c'|'o'|'m'|'p'|'l'|'e'|'t'|'e'| 因此我們要選擇的是 **0x8080808080808080** 題目 MMM = ? `(a)` 0x0080008000800080 `(b)` 0x8000800080008000 `(c)` 0x0808080808080808 `(d)` 0x8080808080808080 `(e)` 0x8888888888888888 `(f)` 0xFFFFFFFFFFFFFFFF **答案選擇**`(d)`**0x8080808080808080** ### 測驗二 關於大小寫轉換的問題,我們實作一個不需要 branch 的案例,將大小寫都轉換為同一個數字,原本為數字的話維持不變 ```clike= uint8_t hexchar2val(uint8_t in) { const uint8_t letter = in & MASK; const uint8_t shift = (letter >> AAA) | (letter >> BBB); return (in + shift) & 0xf; } ``` 這題我覺得十分的巧妙,由於不能使用任何的 branch ,代表任何 if 、 while 、 for 都必須被禁用,這邊實作的原理是利用到 ASCII code 自己本身的特性,數字皆為`0x3..`作為開頭,小寫為`0x4..`作為開頭,大寫則是`0x6..`作為開頭,只看這樣可能不夠直覺,所以我們將他換成 bit 的方式表示: | 數字 | 大寫 | 小寫 | | --- | --- | --- | | ++0011++ ++(0000~1001)++|++0100++ ++(0000~0110)++|++0110++ ++(0000~0110)++| 我們再回來看程式碼: ```clike=3 const uint8_t letter = in & MASK; ``` 我們可以猜到這行的目的是要將數字以及字母分隔開來,所以我們需要做一個 `bit mask` 做出區別: MASK = ? `(a)` 0x10 `(b)` 0x20 `(c)` 0x40 `(d)` 0x60 `(e)` 0x80 透過觀察,我們發現數字與字母的最大差別便是第二個 bit 不一樣,因此選項我們必須選擇++0100++ ++0000++並且與我們原本的 `in` 做 AND 運算,這樣如果是數字輸出結果便會是++0000++ ++0000++,字母的話則是++0100++ ++0000++。 **答案選**`(c)` **0x40** 接著我們還需要將大小寫通通轉成同一個數字輸出,首先看到 ```clike=4 const uint8_t shift = (letter >> AAA) | (letter >> BBB); ``` `letter` 在這邊數字是0,因此經過 shift 之後仍然是0;字母的話是++0100++ ++0000++,其實很好的為我們只留了一個1,因此透過這個1我們就可以製作另一個遮罩,將高位元的四個 bit 變成是一樣的,參考範例,以 `f` 及 `F` 為例,回傳值皆為15,也就是`基準數9`加上原本 `f` 、 `F` 共同的末4個 bit ,也就是6 (`in` + `shift`),再去除高位元的4個 bit ,輸出就會都是15了,解決完字母,我們再檢查數字,因為 `shift` 是0,其實就是單純的遮蔽掉最高四個位元而已。 回到答案的部分,為了製作出9這個基準數,我們將原本得到的++0100++ ++0000++ `shift` 3 和 6 並 OR 運算成功獲得 ++0000++ ++1001++,對於數字基準數則是++0000++ ++0000++。 AAA = ? `(a)` 3 `(b)` 2 `(c)` 1 `(d)` 0 **答案選** `(a)` **3** BBB = ? `(a)` 7 `(b)` 6 `(c)` 5 `(d)` 4 **答案選** `(b)` **6** ### 測驗三 ```cpp= 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; } ``` 根據題目提示: >對 2 的 N 次方 (power of 2) 進行除法,在無號整數的操作就等同於右移 (shift) N 個位元,又當d (除數) 的數值是已知的狀況下,$\frac{2^N}{d}$可預先計算 我們猜測這邊的 M 可能就是$\frac{2^N}{d}$,既然如此,後面的$n × \frac{\frac{2^N}{d}}{2^N}$便是由 ```cpp=7 uint64_t quotient = ((__uint128_t) M * n) >> 64; ``` 實現。因此,我們知道後面這 `shift` 64位元的動作就是相當於除於$2^N$,所以$N$就是64,即$2^{64}$ ,回推回去$M$就是$\frac{2^{64}}{d}$,但是因為能夠紀錄的位數不夠,所以必須先用**0xFFFFFFFFFFFFFFFF**暫時替代,並且在計算完後再把**1**加回來,同時也可以預先支付之後右移64位元的無條件捨去。 XXX = ? `(a)` 0x00F000F000F00080 `(b)` 0xF000F000F000F000 `(c)` 0x0F0F0F0F0F0F0F0F `(d)` 0xF0F0F0F0F0F0F0F0 `(e)` 0x8888888888888888 `(f)` 0xFFFFFFFFFFFFFFFF **答案選擇**`(f)`**0xFFFFFFFFFFFFFFFF** ### 測驗四 ```clike= bool divisible(uint32_t n) { return n * M <= YYY; } ``` 這題想了很久還是想不出來,後來是看了[fwfly](https://hackmd.io/@fwfly/quiz2)同學的共筆,才發現原來是使用 overflow 的方式判斷有沒有辦法整除。 如果是整除,回傳值應該是1,代表整數時,` n * M <= YYY` 為真。 1. $M=\frac{0xFFFFFFFFFFFFFFFF}{3}+1=0x5555555555555556$ 2. $n$總共有三種可能性: $3K$ 、 $3K+1$ 和 $3K+2$ , $K>=0$ `case1:`$3K$ >3KM = K(0xFFFFFFFFFFFFFFFF+3),這個數字已經超過64 bit 能夠表示的最大上限,所以 overflow 之後的結果會是一個蠻小的數字。 `case2:`$3K+1$ >(3K+1)M = K(0xFFFFFFFFFFFFFFFF+3)+0x5555555555555556,前面我們已經知道K(0xFFFFFFFFFFFFFFFF+3)會是一個跟M比起來足夠小的數字,所以我們知道(3K+1)M會是比M大一些些的數字 `case3:`$3K+2$ >(3K+2)M = K(0xFFFFFFFFFFFFFFFF+3)+0xaaaaaaaaaaaaaac,必定也是比 M 大的一個數字 直覺上來說,我們選擇 M 當作答案應該不會有錯,因為 overflow 的關係,被整除的數字 n * M 都會得到一個跟 M 比起來特別小的數字,但是當 $K=0$ 的時候有個例外,也就是得到 `(3K+1)M = K(0xFFFFFFFFFFFFFFFF+3)+0x5555555555555556 =0x5555555555555556`,而這個答案並不滿足 `n * M <= M`必須等於0,所以我們要選擇**YYY=M-1**這個答案來避免這個特殊情形。 YYY = ? `(a)` M + 1 `(b)` M `(c)` M - 1 `(d)` (M >> 1) `(e)` (M << 1) **答案選** `(c)` **M - 1** ### 測驗五 考慮 `strlower` 函式的作用是將指定的字串 (或部分字串) 的英文字母全改為小寫 ```cpp= #include <ctype.h> #include <stddef.h> /* 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]); } } ``` 實作原理,首先我們根據第三題,知道大寫的最高4 bit 為++0100++ , 小寫是 ++0110++,所以大小寫轉換成小寫的方式就是把第3個 bit 設定成1,就可以讓大小寫的開頭皆為++0110++(小寫),設定方式: ```cpp=7 if (s[j] >= 'A' && s[j] <= 'Z') ``` 先檢查是否是大寫 ```cpp=8 s[j] ^= 1 << 5; ``` 這邊的動作是 `XOR` 了 ++0010++ ++0000++,對於0來說,原本的 bit 會被留下來,如果是1的話,會將 bit 反向處理,將大小寫通通變成 ++0110++ 開頭。 在 64 位元處理器架構 (以下針對 `x86_64`, little endian),我們可引入向量化 (vectorized) 的手段,避免一次比對一個字元,而是直接操作一整個 word (對 `x86_64` 來說,就是 64 bits,或 8 個位元組)。沿用上述的 `strlower` 函式,我們用這樣的思路實作向量化的 `strlower`,程式碼列表如下: ```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); } ``` 首先,我們要先了解巨集 ```cpp= #define PACKED_BYTE(b) (((uint64_t)(b) & (0xff)) * 0x0101010101010101u) ``` 的意思,這邊其實是先將傳進來64 bit 的無號整數作 `bit mask` ,留下最後需要"複製"的字元(8 bits ),最後透過乘上 `0x0101010101010101u`一次比較8個字元是否在範圍內,萬一任何一個字元編碼大於經過 `bit mask` 的 b ,得到最後的結果都會大於0。 對於**VV1**,目的是想要一次比對8個字元,判別是不是全部都在 ASCII code 的範圍內,所以每個字元都應該不比128,也就是++1000++ ++0000++ (**0x80**)還要大,如果其中一個比128大,這個判斷式就會大於0 ```cpp=13 if ((*chunk & PACKED_BYTE(VV1)) == 0) { /* is ASCII? */ ``` VV1 = ? `(a)` 0x10 `(b)` 0x20 `(c)` 0x40 `(d)` 0x60 `(e)` 0x80 **因為遮罩條件是 ASCII 上限128,所以選擇遮罩為**`0x80`,`(e)` **0x80** ```cpp=14 uint64_t A = *chunk + PACKED_BYTE(128 - 'A' + VV2); ``` ```cpp=15 uint64_t Z = *chunk + PACKED_BYTE(128 - 'Z' + VV3); ``` 我們知道這兩行的目的是作一個遮罩的上限和下限, VV2 選擇0,因為讓大寫的字母相加後的最後一個位元一定是 1,VV3 選擇-1 是因為需要讓 A~Z 相減必須是 26 以容納 26 個英文字母。 VV2 = ? `(a)` (-1) `(b)` 0 `(c)` 1 **答案選擇**`(b)` **0** VV3 = ? `(a)` (-1) `(b)` 0 `(c)` 1 **答案選擇**`(a)` **(-1)** ```cpp=16 uint64_t mask = ((A ^ Z) & PACKED_BYTE(VV4)) >> 2; ``` 這邊考的觀念其實是我們想要設定的 bit 到底是在哪個位置而已,因為我們知道大寫跟小寫的差別是在第三個 bit ,所以由後面的 `shift` 2 bit 知道前面放的應該是++1000++ ++0000++,也就是0x80。 VV4 = ? `(a)` 0x10 `(b)` 0x20 `(c)` 0x40 `(d)` 0x60 `(e)` 0x80 **答案選擇**`(e)` **0x80** ### 測驗六 ```clike= 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; } ``` KKK = ? `(a)` higher `(b)` lower `(c)` ~higher `(d)` ~lower `(e)` 0xFFFFFFFF JJJ = ? `(a)` higher `(b)` lower `(c)` ~higher `(d)` ~lower `(e)` 0xFFFFFFFF :::success TODO:解釋程式碼 :::