# 2020q3 Homework2 (quiz2) contributed by < `dalaoqi` > ## Outline [TOC] ## 測驗一 ```cpp #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; } ``` 7 位碼 ASCII 是以 7 位元二進位數字進行編碼,可表示 128 個字元,第 128~255 號為擴展字元 (extended ASCII)。 上面 code 是以一個字元的方式判斷是否是有效的 ASCII code (0~127) 。 若 & 0x80(1000000) 不為0,代表該字元大於127,不是有效的 ASCII code。 但上述的 code 逐一字元比對的效率低,在 64 位元的架構下,我們嘗試一次比對 64 位元的資料 (即 word size),程式碼改寫如下: ```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 & 0x8080808080808080) return false; i += 8; } while (i < size) { if (str[i] & 0x80) return false; i++; } return true; } ``` 改善原本 code 效率低落的問題,一次比對8個字元,因此 `MMM` 為 `0x8080808080808080`。 ### 延伸問題 Why memcpy? > 為了和 後面的 0x8080808080808080 做 and operation。也就是在While 中每次 iteration 會將 string 的其中 8個字元位址複製給 `payload`。 ## 測驗二 ```cpp uint8_t hexchar2val(uint8_t in) { const uint8_t letter = in & 0x40; const uint8_t shift = (letter >> 3) | (letter >> 6); return (in + shift) & 0xf; } ``` >已知 in 一定隸屬於 '0', '1', '2', …, '9' 及 'a', 'b', 'c', …, 'f' (小寫) 及 'A', 'B', 'C', …, 'F' (大寫) 這些字元。預期 hexchar2val('F') 和 hexchar2val('f') 回傳 15,且 hexchar2val('0') 回傳 0,其他輸入都有對應的數值。 hexchar2val(‘F’) 和 hexchar2val(‘f’) 回傳 15、hexchar2val(‘A’) 和 hexchar2val(‘a’) 回傳 10。 | char | hex | binary | | -------- | -------- | -------- | | 0 | 0x30 | 00110000 | | 9 | 0x39 | 00111001 | | A | 0x41 | 01000001 | | a | 0x61 | 01100001 | | F | 0x46 | 01000110 | | f | 0x66 | 01100110 | 根據表格可以找出規律: * 0~9 的後四位,就是要回傳的值。 * 非數字字元和數字字元可以由第二位元判斷。 且根據 `(in + shift) & 0xf` 可以得知回傳最低4 bits。因此可以推論出 `MASK` 為 `0x40` (01000000)。 所以根據 `MASK` ,若輸入是數字的話, `letter` 就會是 `00000000` , `shift` 也會是 `00000000` 。 因此直接回傳輸入的最低4 bits。 若輸入的是非數字, `letter` 就會是 `01000000` 。 根據`(in + shift) & 0xf` 推論, `F`、`f` 應該要回傳15(1111),因此可以得知 `shift` 要是 1001,因此 `AAA` 為3, `BBB` 為6。 ## 測驗三 ```cpp const uint32_t D = 3; #define M ((uint64_t)(UINT64_C(0xFFFFFFFFFFFFFFFF) / (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; } ``` 從 return 可以看出餘數是被除數 - 商 * 除數得到的,套用$n \times \frac{\frac{2 ^ N}{d}}{2 ^ N}$ 推論可得: \begin{align} n \% 3 &= n - \left \lfloor {\frac{n}{3}} \right \rfloor \times 3 \\ &= n - \left \lfloor n \times \frac{\frac{2 ^ {64}}{3}}{2 ^ {64}}\right \rfloor \times 3 \\ &= n - \left \lfloor n \times {\frac{2 ^ {64}}{3}} \times {\frac{1}{2 ^ {64}}}\right \rfloor \times 3 \\ \end{align} 從式子中可以看出 $M = {\frac{2 ^ {64}}{3}}$,`XXX` 最接近的答案就是 `0xFFFFFFFFFFFFFFF`。 但其實程式碼轉成數學式其實是:$\frac{2 ^ {64}-1}{3} + 1$,把它代回上面的式子: \begin{align} \frac{{\frac{2 ^ {64}-1}{3} + 1} \times n}{2 ^ {64}} &= \frac{{\frac{2 ^ {64} \times n -n}{3} + n}}{2 ^ {64}} \\ &= \frac{2 ^ {64} \times n -n}{3 \times 2 ^ {64}} + \frac{n}{2 ^ {64}} \\ &= \frac{n}{3} - \frac{n}{3 \times 2 ^ {64}} + \frac{n}{2 ^ {64}} \\ &= \frac{n}{3} + \frac{2n}{3 \times 2 ^ {64}} < \frac{n}{3} + \frac{n}{2 ^ {64}} \end{align} 因為 $n, d < 2^{32}$, $\dfrac{n}{d}$ 的餘數最大值為 $\dfrac{2^{32}-2}{2^{32}-1}$ 又 $\dfrac{n}{2^{64}} < \dfrac{1}{2^{32}}$,因此$M = {\dfrac{2 ^ {64}}{3}}$ 或 $M = \dfrac{2 ^ {64}-1}{3} + 1$ 結果都會是一樣的。 ## 測驗四 延伸測驗 3,我們想判斷某個數值能否被指定的除數所整除,在 D 和 M 都沿用的狀況下,程式碼如下: ```cpp bool divisible(uint32_t n) { return n * M <= M-1; } ``` 從前一題可以看出 $(n \times M) >> 64 = \left \lfloor {\dfrac{n}{3}} \right \rfloor$ ,也就是商的部分, $n \times M$ 則是保留小數的部分。 根據上一題: \begin{align} M &= \dfrac{2 ^ {64}-1}{3} + 1 \\ &= \left \lfloor {\dfrac{2 ^ {64}}{3}} \right \rfloor + 1 \\ M - 1 &= \left \lfloor {\dfrac{2 ^ {64}}{3}} \right \rfloor \end{align} $M-1 = \left \lfloor {\frac{2 ^ {64}}{3}} \right \rfloor$ 表示為 $\frac{1}{3}$ 的小數部分,若 $n \times M < M - 1$,表示可以被3整除。 ## 測驗五 ```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(0x80)) == 0) { /* is ASCII? */ uint64_t A = *chunk + PACKED_BYTE(128 - 'A' + 0); uint64_t Z = *chunk + PACKED_BYTE(128 - 'Z' + (-1)); uint64_t mask = ((A ^ Z) & PACKED_BYTE(0x80)) >> 2; *chunk ^= mask; } else strlower(s, 8); } k = n % 8; if (k) strlower(s, k); } ``` * `PACKED_BYTE(b)` 用處是將 b 重複擴充成64位元的 unsign int。 * 第一小題和測驗一很像,來判斷式是否為 ascii ,因此 `VV1` 為 `0x80`。 * 變數 A、Z 是用來判斷大寫字母的區間。 A 變數的部分會去計算0x80和 `'A'`之間的距離再加上待測字元,若結果的MSB = 1, 代表待測字元 >= `'A'`。 Z 變數的部分會去計算0x80和 `'Z'-1`之間的距離再加上待測字元,若結果的MSB = 1, 代表待測字元 >= `'Z'`,MSB = 0,代表待測字元 <= `'Z'`。 * 上述的例子分別代入 `'A'` 和 `'Z'` 會更清楚, `VV2` = 0,`VV3` = (-1) 。 * 看到 `*chunk ^= mask;` 這行,就是在做大小寫的轉換,只是前面已經先篩選過了,所以這行只是純粹的大寫轉小寫。參考[運用 bit-wise operator](https://hackmd.io/@sysprog/c-numerics#%E9%81%8B%E7%94%A8-bit-wise-operator) :::info :mag_right::mag_right::mag_right: ('a' ^ ' ') // 得到 'A' ('A' ^ ' ') // 得到 'a' ::: * 空白space為 `0b00100000` 因此在變數 mask 的地方才會去做 `>>2`。而剛剛已經得知若字元處在 `'A'` ~ `'Z'` 區間,`A ^ Z`會是 0b10000000 ,因此 `VV4` 為 `0x80`。 ## 測驗六 ```cpp int singleNumber(int *nums, int numsSize) { int lower = 0, higher = 0; for (int i = 0; i < numsSize; i++) { lower ^= nums[i]; lower &= ~higher; higher ^= nums[i]; higher &= ~lower; } return lower; } ``` ### XOR * 做 XOR 運算時,不同得1,相同得0,如下方表格所示。 | A | B | A ^ B | | -------- | -------- | -------- | | 0 | 0 | 0 | | 0 | 1 | 1 | | 1 | 0 | 1 | | 1 | 1 | 0 | * `A ^ A` = 0 * 交換率 * 運算順序不影響運算結果。 XOR 以上的特性可以得出 `A ^ B ^ A = B` 這種特性,找出出現一次的數值。此題就是運用此方式來解決。 `lower` 和 `higher` 是用來紀錄數值讀累計次數, `lower` 代表出現過一次, `higher` 代表出現兩次。 為了說明方便假設變數都是只有一個位元。 若陣列的第一個 input 為 1 ,所以 `lower ^ nums[0]` 就會是 `1`。 在經過 `lower &= KKK;` 運算後也要為 `1`,所以可能的答案為 `~higher` 或 `0xFFFFFFFF`。不過考慮到 `lower` 應該要像 `higher` 一樣受到另一個變數的影響,且若選 `0xFFFFFFFF` ,和沒有做這行 `&` 運算是一樣的,因此 `KKK` 為 `~higher`。 而在 `higher ^ nums[0]` 後, `higher` 也變為 `1`,不過應該要是 `0` 。因此, `higher &= JJJ;` , `JJJ` 應該為 `~lower`。