# 2020q3 Homework2 (quiz2) contributed by <`joe-U16`> ###### tags: `sysprog2020` ## 測驗 `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; ``` ### Q1 :::info 解釋上述程式的運作原理,特別是為何用到 memcpy 呢?提示: 與 data alignment 有關,詳閱 [C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory) ::: * MMM=0x8080808080808080 * ASCII 定義了0-127 共128個字元,若 payload 這個字元的第 8個 bit 有值,則會讓 `(payload & MMM)` 為 1,進而 return false。 * 在 x86_64 中 unsigned long long 一次對齊是 8 bytes * 使用memcpy 將 8 個 bytes 大小的字元 payload 這個變數內 ### Q2 :::info 將上述技巧應用於「給予一個已知長度的字串,檢測裡頭是否包含有效的英文大小寫字母」 ::: ### Q3 :::info 承 (2),考慮常見的英文標點符號,判斷輸入字串是否為有效的字元集,避免逐一字元比對 > 提示: Intel SSE 4.2 加入 STTNI (String and Text New Instructions) 指令,可加速字串比對,[詳見: Schema Validation with Intel® Streaming SIMD Extensions 4](https://software.intel.com/content/www/us/en/develop/articles/schema-validation-with-intel-streaming-simd-extensions-4-intel-sse4.html) ::: ## 測驗 `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; } ``` ### Q1 :::info 解釋上述程式的運作原理; ::: * `第5行` 跟 `0xf` 做 `&` ,因為要表達的值只有0-15 * 可以發現做 `第5行` 的話 `0-9` 不用處理,就可以得到答案了。 * `f` = 0110 0110 * `F` = 0100 0110 * `9` = 0011 1001 * `9` 不需要shift,所以MASK 是為了讓 `0-9` 跟 `a-f` 做出區別, * MASK=0x40, `0-9` 不會有 letter ,`a-f` 有letter * 如果只看 `8` `4` `2` `1` ,4個bit, `f` 為 `0110` 所以想要讓他變 `15` ,要再讓他加 9,所以 AAA=3 BBB=6 可以讓 shift=9 * 成功了 * Ans: * MASK=0x40 * AAA=3 * BBB=6 ### Q2 :::info 將 hexchar2val 擴充為允許 "0xFF", "0xCAFEBABE", "0x8BADF00D" 等字串輸入,且輸出對應的十進位數值 ::: ## 測驗 `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; } ``` ### Q1 :::info 解釋上述程式的原理; ::: * $M$ 即為 ${2^N \over d}$ * ${n \over d} = n * {{2^N \over d} \over 2^N}$ * `quodient` 為 ${n \over d}$ 的商數 * 最後n - `quodient` * d 即為餘數。 * Ans: * `XXX=0xFFFFFFFFFFFFFFFF` ### Q2 :::info 由 Facebook 公司所維護的 jemalloc 是個高效能的記憶體配置器 (memory allocator,即 malloc 和 free 函式對應的實作),特別在多核多執行緒的環境有優異表現,在其原始程式碼 include/jemalloc/internal/div.h 和 src/div.c 也運用類似的技巧,請閱讀原始程式碼,並抽離快速除法實作為獨立的程式碼,說明運作原理,也該撰寫測試程式,比較透過處理器的整數除法指令及本技巧的效能差距; ::: ## 測驗 `4` 判斷某個數值能否被指定的除數所整除,在 D 和 M 都沿用的狀況下,程式碼如下: ```cpp= bool divisible(uint32_t n) { return n * M <= YYY; } ``` $n * M$ 即為 $n * ({{2^N-1} \over d}+1)$ * `n * M = n * ((0xFFFFFFFFFFFFFFFF / d) + 1)` => `(n * (0xFFFFFFFFFFFFFFFF / d)) + (n * 1)` * 如果 n 可被 d 整除,則 $n*M$ 會溢位得到很小的數 * 運用上述規則來判斷 n 是否可整除 d * 考慮 `n=1` 且 `d=2` ,此條件下 不可整除 n ,函式該 return `0` * 推導 * $n * ({{2^N-1} \over d}+1)\lt= YYY$ * 代入 n=1 d=2 * $1 * ({{2^N-1} \over 2}+1)\lt= YYY$ * $M <= YYY$ 需return `0` * 可推知YYY為選項 M-1 或 (M >> 1) * 找 M>>1的反例 * 如果 n 不可整除 d 則 YYY 需小於 M, (M >> 1) 可達成 * 如果 n 可整除 d 則 YYY 須大於等於 M * 找不到反例 * M-1 和 (M>>1) 都對 ## 測驗 `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); } // VV1=0x80 VV2=0 VV3=(-1) VV4=0x80 ``` ### Q1 :::info 解釋上述程式的原理 ::: :::success 以下討論為不使用PACKED_BYTE()這個 macro ,使用一個字元8 bits 的情況,可較直觀的理解程式碼的目的。 ::: * VV1 * PACKED_BYTE() 這個 macro 功能是只取 input 前面 8 個 bits ,並將得到的資料重複擴展到 64 bits * ASCII 定義了0-127 共128個字元 透過 ==#13==` (*chunk & 1000 0000(b)) == 0` 可判斷是否為 ASCII ,若是才繼續做,不是的話就做extended ASCII * `VV1 = 0x80` * ==#16== mask * ==#17== 如果`(*chunk ^= ' ')` ,可讓\*chunk 這個字元大小寫互換,而`' '`即為`0b00100000` * 若要讓mask 符合此值,==#16==`(A ^ Z)` 的第 8 bit 需為 1 * 再與`0x80`做`&`,可得`0b10000000` * 再右移2格可得`0b00100000`,即為`' '` * 可知 `VV4=0x80` * `A ^ Z` 的第8 bit 為 1 * `A = (*chunk) + (128 - 65 + 0)`,若 \*chunk 大於等於65,則A可得大於128的值 * `B = (*chunk) + (128 - 90 + (-1))`,若 \*chunk 小於等於`90`,則B可得小於128的值 * 可知 `A^Z` 目的為判斷 \*chunk 是否為字元 A-Z ,若是,才需大小寫轉換。 ### Q2 :::info 將前述測試程式 main 函式的 char str[] 更換為 char *str,會發生什麼事?請參閱 C 語言規格書,說明 string literal 的行為 ::: * char str[] 更換為 char *str * 會發生 segmentation fault(core dumped) * 在[你所不知道的C語言:指標篇](https://hackmd.io/@sysprog/c-prog/%2Fs%2FHyBPr9WGl)中有提到: * definition/statement, 如 char x[10] →不能變更為 pointer 的形式 * 請參閱 C 語言規格書,說明 string literal 的行為 * 是一種字串常值的型態 ## 測驗 `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; } // KKK=~higher JJJ=~lower ``` ### Q1 :::info 解釋前述程序的原理; ::: * 取一個例子 [2, 2, 2, 3] * 第一個迴圈做完後 `lower=2` `higher=0` * 第二個 `lower=0` `higher=2` * 第三個迴圈 `lower=0` `higher=0` 像是一切重新計算 * 第四個迴圈 `lower=3` `higher=0` higher = return lower * 不管排列組合怎麼排,都可以讓數字做三次後就歸零 ### Q2 :::info 請撰寫不同上述的程式碼,並確保在[LeetCode 137 Single Number](https://leetcode.com/problems/single-number-ii/) 可成功執行 ::: ### Q3 :::info 延伸題意,將重複3次改為重複重複4次或5次,並發現可容易推廣到多次的程式碼; ::: 如果要重複次數為偶數次的話,使用 `Xor` ,利用相同數字 `Xor` 後會為0的特性,即可得解: ```cpp= int singleNumber(int *nums, int numsSize) { int tmp = 0; for (int i = 0; i < numsSize; i++) { tmp ^= nums[i]; } return tmp; } ```