# 2020q3 Homework2 (quiz2) contributed by < `ZhuMon` > ###### tags: `sysprog2020` `quiz` > [2020q3 第 2 週測驗題](https://hackmd.io/@sysprog/2020-quiz2) ## :memo: Table of Contents [TOC] --- ## :page_facing_up: [測驗 `1`](https://hackmd.io/@sysprog/2020-quiz2#%E6%B8%AC%E9%A9%97-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; } ``` ### 解答 * `MMM` = `(d)` `0x8080808080808080` * 從原始程式碼可知,`is_ascii` 藉由將 byte 與 `0x80` 做 and,確認 byte 的第一個 bit 是否為 1,藉此確認是否為 ASCII 的有效字元 * 改寫後,為了達成一次比對 8 個 byte,一次將 8 個 byte 存到 `payload` (第 12 行),因此 mask 也要擴展,從只能判斷 1 個 byte 的 `0x80` 擴展到可以比對 8 個 byte 的 `0x8080808080808080` ### 延伸問題 1. 為何使用 `memcpy`? 根據 glibc 2.33 的 [memcpy.c](https://sourceware.org/git/?p=glibc.git;a=blob;f=string/memcpy.c;h=2cb4c76515f476f36a9a8d5dd258ea98e36792b2;hb=4c56bcbceb05b44965d48e701711f850b83d7c69),將註解去掉後,如下 ```cpp= #include <string.h> #include <memcopy.h> #ifndef MEMCPY # define MEMCPY memcpy #endif void * MEMCPY (void *dstpp, const void *srcpp, size_t len) { unsigned long int dstp = (long int) dstpp; unsigned long int srcp = (long int) srcpp; if (len >= OP_T_THRES) { len -= (-dstp) % OPSIZ; BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ); PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len); WORD_COPY_FWD (dstp, srcp, len, len); } BYTE_COPY_FWD (dstp, srcp, len); return dstpp; } libc_hidden_builtin_def (MEMCPY) ``` 以下幾個 macro 會根據不同的架構而有所不同,在 glibc 的 generic [memcopy.h](https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/generic/memcopy.h) 定義如下 ```cpp #define OP_T_THRES 16 #define OPSIZ (sizeof(op_t)) #define op_t unsigned long int ``` * `memcpy` 解釋 若是要複製長度大於 `OP_T_THRES` 的記憶體,先將沒有對齊的資料複製完成後 (line==17==),便可以直接以 Page 作為單位快速的複製記憶體(`PAGE_COPY_FWD_MAYBE`),接著以 word 作為單位複製剩下的記憶體,然後複製剩下的 byte ```mermaid graph TD memcpy[dstp = dstpp<br>srcp = srcpp] if{len >= OP_T_THRES} bcf[BYTE_COPY_FWD] pcfm[PAGE_COPY_FWD_MAYBE] wcf[WORD_COPY_FWD] bcf2[BYTE_COPY_FWD] memcpy --> if if -- true --> bcf --> pcfm --> wcf --> bcf2 if -- false --> bcf2 ``` * `memcopy.h` 對於 `BYTE_COPY_FWD(dst_bp, src_bp, nbytes) ` 的描述為 > Copy exactly NBYTES bytes from SRC_BP to DST_BP, > without any assumptions about alignment of the pointers. 也就是說,`BYTE_COPY_FWD` 會從 `src_bp` 開頭逐一複製 `nbytes` 個 byte 到 `dst_bp` 的開頭 (另一個 macro `BYTE_COPY_BWD` 則是將來源位址和目標位址都視為結束位址來複製) ```c #define BYTE_COPY_BWD(dst_ep, src_ep, nbytes) \ do \ { \ size_t __nbytes = (nbytes); \ while (__nbytes > 0) \ { \ byte __x; \ src_ep -= 1; \ __x = ((byte *) src_ep)[0]; \ dst_ep -= 1; \ __nbytes -= 1; \ ((byte *) dst_ep)[0] = __x; \ } \ } while (0) ``` --- 2. 給予一個已知長度的字串,檢測裡頭是否包含有效的英文大小寫字母 查看 ASCII Table,可以知道英文大小寫字母所對應的 ASCII code 為 `0x41~0x5A`, `0x61~0x7A`,以二進位表示如下 | Char | Bin | Hex | | -------- | -------- | -------- | | A | 0100 0001 | 0x41| | Z | 0101 1010 | 0x5A| | - | ---- | - | | a | 0110 0001 | 0x61| | z | 0111 1010 | 0x7A| > 以 `*` 代表 don't care 觀察可知,英文字母的 ASCII code 前兩個 bit 必為 01,但是在 `01** ****`的範圍中,{`01000000`, `01011011 ~ 01100000`, `01111011 ~ 01111111`} 這三個區間內所對應的字元不是英文字母,總共有 12 個 ($2^6 - 26*2 = 12$) 繼續觀察,將上述 12 個字元以第三個 bit 分成兩類,可以再歸納出三組 1. `01*0 0000` 2. `01*1 1011` 3. `01*1 11**` |hex|bin|hex|bin| |-|-|-|-| |0x40 | 0100 0000 | 0x60 | 0110 0000 | |0x5b | 0101 1011 | 0x7b | 0111 1011 | |0x5c | 0101 1100 | 0x7c | 0111 1100 | |0x5d | 0101 1101 | 0x7d | 0111 1101 | |0x5e | 0101 1110 | 0x7e | 0111 1110 | |0x5f | 0101 1111 | 0x7f | 0111 1111 | 因此只要 ASCII code 同時符合以下四點,便為英文字母 1. `(char & 11000000) == 01000000` 2. `(char & 00011111) != 0` 3. `(char & 00011111) != 00011011` 4. `(char & 00011100) != 00011100` 但是要在單個 byte 實作很簡單,多個 byte 同時判斷就很困難 > TODO: 第五題似乎有較好的解決方法 --- ## :page_facing_up: 測驗 `2` ### 題目 ```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`,其他輸入都有對應的數值。 ### 解答 `MASK` = `0x40` `AAA` = `3` `BBB` = `6` 1. 從結尾回推 從第 5 行的 return 值可以看到 `(in + shift)` 與 `0xf` 做 mask,因此 return 值只會在 `0~15` 之間。 * 數字部分 因為數字的 ASCII code 介於 `0x30~0x39`,因此 `shift` 只要不會影響 `in` 的後 4 bit,便可以使數字部分達成目的,預計 `shift` 為 `0x?0` * 英文字母 要將 `0x61` (`a`) 和 `0x41` (`A`) 對應到 10 (`b'1010`),因此最簡單的想法就是將 `shift` 設為 9 (`0x09`) 明顯兩個規則矛盾,因此 `shift` 應該符合下述條件 * `shift` = `0x?0`, if `in` $\in$ {`'0'`, `'1'`, ..., `'9'`} * `shift` = `0x?9`, if `in` $\in$ {`'A'`, ..., `'F'`, `'a'`, ..., `'f'`} 2. 判斷 `letter` 為何 從字面意義大概猜出 `letter` 代表英文字母,從選項來看,可以知道要將 `in` 的前 4 bit 的某些 bit 取出來 * `(a)` `0x10` * `(b)` `0x20` * `(c)` `0x40` * `(d)` `0x60` * `(e)` `0x80` 而可以判斷英文字母與數字的 bit 為第 2 個 bit 和第 4 個 bit * 數字: `0011 xxxx` * 大寫: `0100 xxxx` * 小寫: `0110 xxxx` 稍微瞄到下一行 (line 4),`shift` 會以 `letter` 來決定值,而我們的 `shift` 在 `in` 為數字時,不需要有值,因此應該以第 2 個 bit 作為 mask,也就是 * `MASK` = `(c)` `0x40` 3. `AAA` & `BBB` 由於前面已經知道在 `in` 為英文字母時,`letter` 為 `b'0100 0000`,而要讓 `shift` 的後 4 bit 為 9 (`b'0000 1001`),必須讓 `letter` 向左位移 3 位和向左位移 6 位,因此可以得到 * `AAA` = `3` * `BBB` = `6` --- ## :page_facing_up: 測驗 `3` ### 題目 ```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; } ``` ### 解答 為了得到餘數,function 必須返回 $\begin{split} n\ \%\ d &= n - x \times d \end{split}$ 其中 $x$ 為 $n/d$ 的整數部分,也就是 `quotient`,將 $M = \dfrac{XXX}{d}+1$ 帶入下式 $\begin{split}x &= quotient \\ &= \dfrac{M \times n}{2^{64}} \\ &= \dfrac{(\dfrac{XXX}{d}+1) \times n}{2^{64}} \ \ \ \ \ 重新排列後 \\ &= n \times \dfrac{\dfrac{XXX}{d}+1}{2^{64}} \end{split}$ 對照題目可知 (XXX / d) + 1 便是 2^64^/d,~~而 XXX 最接近的選項便是 `0xFFFFFFFFFFFFFFFF`~~ ~~> 關於為甚麼要先除以 d 再加 1,可以參考 [@cmwchw](https://hackmd.io/@cmwchw/2020q3-quiz2#Why--Mdfrac2N-1d1-) 的解說~~ 也就是說 $n \times \dfrac{\dfrac{XXX}{d}+1}{2^{64}} = n \times \dfrac{\dfrac{2^{64}}{d}}{2^{64}}$ 設 $Q = \dfrac{n}{d}$ $=> \dfrac{XXX}{2^{64}}Q+\dfrac{n}{2^{64}}=Q$ 由於 $n << 2^{64}$ 所以 $\dfrac{n}{2^{64}}$ 可以捨去 $=> \dfrac{XXX}{2^{64}}Q=Q$ $XXX = 2^{64}$ --- ## :page_facing_up: 測驗 `4` ### 題目 ```cpp= bool divisible(uint32_t n) { return n * M <= YYY; } ``` 判斷 n 是否被 d 整除 ### 解答 將 M 帶入後可得 $n \times (\dfrac{2^{64}-1}{d}+1) \leq YYY$ ... 將 n = 1 帶入,可以得到選項 (a) (b) (e) 都矛盾,因此只有可能是 $M-1$ 或 $M>>1$ 試過所有值(0~2^32^-1)後發現兩者依舊可以達成目的,懷疑等號右方只要小於 $M$ 便可以達成目的 --- ## :page_facing_up: 測驗 `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); } ``` --- ## :page_facing_up: 測驗 `6`