# 2020q3 Homework2 (quiz2) contributed by < `Rsysz` > ###### tags: `sysprog` > [測驗題](https://hackmd.io/@sysprog/2020-quiz2) ## 1. ASCII 編碼判斷 目前電腦中用得最廣泛的字元集及其編碼,是美國國家標準局 (ANSI) 制定的 ASCII 碼,被國際標準化組織 (ISO) 採納為 ISO 646 標準。7 位碼 ASCII 是以 7 位元二進位數字進行編碼,可表示 128 個字元,第 128~255 號為擴展字元 (extended ASCII)。 如果我們明確界定 7 位元編碼的字元隸屬於 ASCII,可透過以下函式來判斷指定的記憶體範圍內是否全是有效的 ASCII 字元 ```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; } ``` 其中 `str` 是開頭的記憶體地址,`size` 則是要檢驗的範圍。逐一字元比對的效率低,在 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 & MMM) return false; i += 8; } while (i < size) { if (str[i] & 0x80) return false; i++; } return true; } ``` 已知 7 位元編碼的字元隸屬於 ASCII,透過 `uint64_t` 也就是 `long int` `payload` 截取 `str` 字串陣列中前8位進行判斷,如下圖所示,與 `0x80` 進行 `&` 運算後必須等於==0== 而 `payload` 內有 8 個bytes對應8個字元,因此每個字元對應一組 `0x80` MMM = `0x8080808080808080` | Hexadecimal | Binary | Decimal | | ---- | ---- |----| | 0x80 | 1000 0000 | 128 | | 0x7F | 0111 1111 | 127 | :::info `memcpy` 常見的實做是先檢查 memory address 是否 ==word aligned==,如果不是會先 copy 前幾 bytes 使目前位址到 word aligned 的地方,然後以 word 而不是 byte 為單位做資料 copy。 因此同樣一個 instruction 可以搬多個 bytes,會比用迴圈每個 byte 逐一 copy 要快,有時候甚至可以用 Intel cpu 特殊的專用指令做一次 copy 整個區塊。 ::: > 還要考慮 MIPS/Arm 架構中,如果硬體無法處理 misalignment,上述程式碼如果沒有 memcpy 這樣事先考慮到 alignment 的處理,會發生什麼事? > :notes: jserv ARM 架構中部分CPU“部分支援”非對齊訪問其“單指令”操作支援非對齊,但“群指令”操作(SIMD)則不支援,如 LDM、STM、LDRD、STRD。ARMv5 指令集的CPU(一般是arm9架構)預設不支援非對齊記憶體訪問,ARMv6及以上的CPU預設支援處理大部分的非對齊記憶體地址訪問。 MIPS 架構中硬體不支援對齊訪問,但通過軟體支援,通過核心中對 alignment fault 異常處理流程中進行處理,比如將非對齊的資料訪問,通過多次訪存操作和拼接操作來處理,也可以使用類似 memcpy的方式來處理,當然代價是更嚴重的效能損失。 通常,在出現 alignment fault 時,需要分析定位原因,而不能簡單的通過核心的 fixup 或者忽略,由於 alignment fault 帶來的效能損耗是非常大的因此在很多CPU中,會將此類問題當做一種異常上報,目的就是希望工程師能修正程式碼,提升效能。 而最常見的可能導致 alignment fault 的程式碼編寫方式如: * 指標轉換:將低位寬型別的指標轉換為高位寬型別的指標 * 使用packed屬性或者編譯選項。這樣的操作會關閉編譯器的自動填充功能,從而使結構體中各個欄位緊湊排列,如果排列時未處理好對齊,則可能導致alignment fault [參考資料](https://www.itread01.com/content/1544492598.html) [檢測英文大小寫字母](https://github.com/Rsysz/quiz2/blob/master/test1.c) ## 2. 16進位字串轉換 ```cpp= uint8_t hexchar2val(uint8_t in) { const uint8_t letter = in & MASK; const uint8_t shift = (letter >> AAA) | (letter >> BBB); 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 * `F` 對應`0x46`, `f` 對應 `0x66` | Character | Hexadecimal | Binary | | ---- | -------- | -------- | | F | 0x46 | 0100 0110 | | f | 0x66 | 0110 0110 | | | 0xf | 0000 1111 | 再來看到 `return (in + shift) & 0xf` 於是知道 `F` 與 `f` 兩者輸入的 `in + shift` ==最低位的 4 個 bits== 必定為 `1111`,轉為十進位也就是 15 | Character | Binary | | ---- | ---- | | F | 0==1==00 0110| | f | 0==1==10 0110| | 0x40 | 0==1==00 0000| 回到表格我們發現兩者在最高位 4 個 bits 僅有 1 個 bit 相同,於是MASK = ? `0x40` | Character | Binary | | ---- | ---- | | 0x40 | 0==1==00 0000| | letter| 0==1==00 0000| | F | 0==1==00 0110| | f | 0==1==10 0110| | in+shift|XXXX 1111| 根據 `MASK = 0x40` 的結果我們可以知道 `letter` 在兩種輸入情況下皆等於 `0100 0000` 又`in + shift` 最低位的 4 個 bits 等於 `1111`,於是讓 `letter` 分別做兩次右移補滿 `F` 和 `f` 的最低位 `0110` 於是 AAA = `3` BBB = `6` [擴充版本](https://github.com/Rsysz/quiz2/blob/master/test2.c) ## 3. 快速除法 > 不懂就說不懂,不要說「不是很懂」這樣不精準的話,而且你已經不懂,還「僅供參考」什麼?會不會誤導自己和他人呢? > :notes: jserv ```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進行加速,透過將\dfrac{n}{d}=n\times\dfrac{\dfrac{2^N}{d}}{2^N}$將除法轉換為乘法和位移操作,進而減少運算成本。 已知 `quotient = ((__uint128_t) M * n) >> 64` 對應$\dfrac{1}{2^N}$ 所以 $N=64$ 但對應的$M卻不等於\dfrac{2^N}{d}$而是$M=\dfrac{2^N-1}{d} + 1$ 因此要證明兩者最終結果是相等的。 若以數學的角度考量$\dfrac{M \times n}{2 ^ {64}} = \dfrac{(\dfrac{2 ^ {64} - 1}{d} + 1) \times n}{2 ^ {64}} = \dfrac{n \times 2 ^ {64} - n}{d \times 2 ^ {64}} + \dfrac{n}{2 ^ {64}}$ $= \dfrac{n}{d} - \dfrac{n}{d \times 2 ^ {64}} + \dfrac{n}{2 ^ {64}} = \dfrac{n}{d} + (\dfrac{d \times n - n}{d \times 2 ^ {64}}) = \dfrac{n}{d} + (\dfrac{(d - 1) \times n}{d \times 2 ^ {64}}) < \dfrac{n}{d} + \dfrac{n}{2^{64}}$。 由於$n,d < 2^{32} - 1因此\dfrac{n}{2 ^ {64}} < \dfrac{1}{2^{32}} < \dfrac{1}{2^{32} - 1}且\dfrac{n}{d}的餘數部分最大值為\dfrac{2^{32}-2}{2^{32} - 1}$ 所以$\dfrac{n}{d}的餘數部分加上\dfrac{n}{2^{64}}也不會進位$ --- 但若以程式碼的角度考量 若$M=\dfrac{2^{64}}{d}為整數(設為K),因\dfrac{2^{64} - 1}{d} < K所以M=\dfrac{2^{64} - 1}{d}+1=(K-1)+1 = K$ 若$M=\dfrac{2^{64}}{d}為非整數,因\dfrac{2^{64} - 1}{d} > K所以M=\dfrac{2^{64} - 1}{d}+1=K+1$ 兩種情況會讓 `quotient` 分別等於$\dfrac{n}{d}和\dfrac{n}{d}+\dfrac{n}{2^{64}}$但根據上述數學推論的結果,兩者得到的答案相等 因此 XXX = `(f) 0xFFFFFFFFFFFFFFFF` **jemalloc 的除法原理** $n$為**被除數**, $q$為**商**, $d$為**除數** 假設$n = q \times d,已知 n,d 求q = n / d$ $=\left \lfloor \left \lceil \dfrac{2^k}{d} \right \rceil \times \dfrac{n}{2^k} \right \rfloor = \left \lfloor \dfrac{2^k + r}{d} \times \dfrac{n}{2^k} \right \rfloor,k為任意整數,而r = (- 2)^k mod \space d$ $將\left \lfloor \dfrac{2^k + r}{d} \times \dfrac{n}{2^k} \right \rfloor展開為\left \lfloor \dfrac{2^k}{d} \times \dfrac{n}{2^k} + \dfrac{r}{d} \times \dfrac{n}{2^k}\right \rfloor = \left \lfloor \dfrac{n}{d} + \dfrac{r}{d} \times \dfrac{n}{2^k} \right \rfloor$ $因假設n /d為整除,所以n/d的小數部分為0,因此式子可以化簡為\dfrac{n}{d} + \left \lfloor \dfrac{r}{d} \times \dfrac{n}{2^k} \right \rfloor$ 也就是當$\dfrac{r}{d} \times \dfrac{n}{2^k} < 1$,$n/d = \left \lfloor \left \lceil \dfrac{2^k}{d} \right \rceil \times \dfrac{n}{2^k} \right \rfloor$就會成立 因$r是\dfrac{2^k}{d}的餘數因此r<d且r/d<1$總是成立,所以只需滿足$\dfrac{n}{2^k} < 1$ 又$n為32bits的數,最大值為2^{32}-1,因此可以令k = 32來確保\dfrac{n}{2^k} < 1$ ## 4. 倍數判斷 已知 `D = 3` `M = (0xFFFFFFFFFFFFFFFF / D) + 1 = 0x5555555555555556` ```cpp bool divisible(uint32_t n) { return n * M <= YYY; } ``` 令 `n=3k, 3k+1, 3k+2` (k為非負整數) `n * M` 等於 若 `k=1` 因為 `3M > 0xFFFFFFFFFFFFFFFF` (overflow) 所以 `3M = 0x0000000000000002` 求三者最小值: * 3kM = 2k * (3k+1)M = 3kM + M = 2k+M * (3k+2)M = 3kM + 2M = 2k + 2M 因此只有 `(c) M - 1, (d) M >> 1` 符合要求 但答案的 YYY = `(c) M - 1` :::danger :warning: 注意共筆書寫要求:中英文之間用一個半形空白字元區隔,從小處做起! :notes: jserv ::: ## 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` 的效果是將輸入 LSB 的 8 bits 擷取後重複擴充至 64 個 bits e.g. PACKED_BYTE(0xAB) 會得到 0xABABABABABABABAB 如同題一一般以0x80對ASCII範圍內的字元進行檢測 因此 VV1 = `(e) 0x80` 再來看到`uint64_t mask = ((A ^ Z) & PACKED_BYTE(VV4)) >> 2`而上課時學到 > ('a' ^ ' ') // 得到 'A' ('A' ^ ' ') // 得到 'a' 此外`0x80` >> 2 = `' '`可以知道`A ^ Z`的數值可以決定這8個字元哪一位要做大小寫轉換 也就是說只有字元為大寫時,`A ^ Z`的MSB才能為1 `(128 - 'A' + char + VV2) ^ (128 - 'Z' + char + VV3)` | char | Decimal | Binary | | ---- | ------- |------- | | A | 65 |0100 0001| | Z | 90 |0101 1010| | |128 |1000 0000| 因此可得知當 `char >= 'A'` 時 `uint64_t A` 的MSB 為1 `char >= 'Z'` 時 `uint64_t Z` 的MSB 為1 但當 `char = Z` 時為了使 `A ^ Z` 的MSB為1 還需額外減1 VV2 = `(b) 0` VV3 = `(a) (-1)` VV4 = `(e) 0x80` 延伸問題 將前述測試程式 main 函式的 char str[] 更換為 char *str,會導致 `Segmentation fault (core dumped)`的發生。 根據C語言規格書[ISO/IEC 9899:1999](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf)p.130 > 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. 嘗試使用 p 去修改 arrary 內的值是屬於 undefined behavior **6.4.5 String literals** >The multibyte character sequence is then used to initialize an array of **static** storage duration and length just sufficient to contain the sequence. 又提到會將內容初始化為指定大小的**靜態**陣列,因此使用 **char \*str** 相當於 **const char \*str** ## 6. 單次數尋找 ```cpp 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; } ``` 是有限狀態機的味道,而本題題解屬於Mealy機,輸出依賴於輸入和狀態 首先構建一個真值表如下圖所示 | higher | lower | input | higher_output | lower_output | | ------ | ----- | ----- | ------ | ------ | | 0 | 0 | 0 | 0 | 0 | | 0 | 1 | 0 | 0 | 1 | | 1 | 0 | 0 | 1 | 0 | | 0 | 0 | 1 | 0 | 1 | | 0 | 1 | 1 | 1 | 0 | | 1 | 0 | 1 | 0 | 0 | 當出現第三次時代表此數並非我們所尋找的數,因此直接重置狀態為 00 $lower\_output = \overline{input} \times \overline{higher} \times lower + input \times \overline{higher} \times \overline{lower} =\overline{higer} \times input \oplus lower$ $higher\_output = \overline{input} \times higher \times \overline{lower} + input \times \overline{higher} \times lower$ 但題解中 `higer` 計算並未使用到前次`lower`的狀態,因此修改真值表 嘗試使用 `lower_output` 計算 `higher_output` | higher | lower(lower_output) | input | higher_output | | ------ | ----- | ----- | ------ | | 0 | 0 | 0 | 0 | | 0 | 1 | 0 | 0 | | 1 | 0 | 0 | 1 | | 0 | 1 | 1 | 0 | | 0 | 0 | 1 | 1 | | 1 | 0 | 1 | 0 | $higher\_output = \overline{input} \times higher \times \overline{lower} + input \times \overline{higher} \times \overline{lower} =\overline{lower} \times input \oplus higher$ 因此改用`lower`計算`higher`,得到題解程式碼如上圖。 [延伸問題 2](https://github.com/Rsysz/quiz2/blob/master/test6.c) [延伸問題 3](https://github.com/Rsysz/quiz2/blob/master/test6_extend.c)