# 2020q3 Homework2 (quiz2) contributed by < `unknowntpo` > ## 測驗 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 & 0x8080808080808080) // MMM return false; i += 8; } while (i < size) { if (str[i] & 0x80) return false; i++; } return true; } ``` >Answer: MMM = (d) 0x8080808080808080 :::success 自我問答 * payload 的作用是? 把要比對的數值 (大小為 8 bytes) copy 到 payload 上,在對 payload 做位元操作 * :question: 可以直接對 `str[]` 做 check 而不用額外 copy 到 payload 上嗎? * 沒辦法,以 `str[1]` 這個 object 為例,它的大小是 1 個 byte,而如果要做一次 8 個 byte 的比對的話就需要另一個大小為 8 bytes 的 object,這也是 payload 的作用 ::: * :question: 第一個 while 判斷式為 `while ((i + 8) <= size)`, * 假設 size == 8, 在 str[] 後的第一個 byte 藏有 non-ascii code 的話,這樣會不會出現明明 `str[]` 內都是 ascii, 但是判斷結果卻是 non-ascii 呢? * 我的解答: * 事實上 str[size] 永遠都會是 null character , 所以並不會發生 copy 到不屬於 `str[]` 的東西 * 第一次與第二次的 while loop 對於 i 的邊界不一樣,或許能改成 * `while((i + 8) < size)` * `while (i < size)`? * 答案是不行 * 第一個 while 是確認是否有足夠的 8-byte chunck 來複製到 payload * 第二個 while 是確認一次檢查一個 byte 的時候不會超出邊界 * 在第二個 while 如果使用 i <= size 的話,就會檢查到 null character 了! * 這不符合我們的預期 :::success TODO: 做實驗來驗證自己的假設 ::: ```cpp assume str[] = "a" size == 8 i + 8 i size idx 0 1 2 3 4 5 6 7 8 str 0 1 1 0 0 0 0 1 x | ____________| 'a' ``` is ### 完整 test code :::spoiler ```cpp=1 /* * isascii: Check if all the elements at an array is ascii character or not * Ref: * 2020q3 Homework2 (quiz2) Question 1: https://hackmd.io/@sysprog/2020-quiz2 * My note: https://hackmd.io/@unknowntpo/quiz2 */ /* Change to 1 if we want to test non ascii character */ #define TEST_NON_ASCII 1 /* (a) 0x0080008000800080 (b) 0x8000800080008000 (c) 0x0808080808080808 (d) 0x8080808080808080 <-- right answer (e) 0x8888888888888888 (f) 0xFFFFFFFFFFFFFFFF */ /* Define MMM */ #define MMM 0x8080808080808080 #include <stdio.h> #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; /* Check the character 8 bytes (1 word in 64-bit cpu) at a time */ while ((i + 8) <= size) { uint64_t payload; memcpy(&payload, str + i, 8); if (payload & MMM) // MMM return false; i += 8; } /* * Not enough words, * so check the character 1 byte at a time */ while (i < size) { if (str[i] & 0x80) return false; i++; } return true; } int main() { char c[] = "12345678"; /* Insert non ascii character */ #if TEST_NON_ASCII c[1] = 128; #endif printf("is %s ascii ? %s\n", c, is_ascii(c, strlen(c)) ? "true" : "false"); return 0; } ``` ::: ## 測驗 2 開發解析器 (parser) 時,常會將十六進位表示的字串 (hexadecimal string) 轉為數值,例如 `'0xF'` (大寫 `F` 字元) 和 `'0xf'` (小寫 `f` 字元) 都該轉換為 `15`。考慮以下不需要分支 (branchless) 的實作: ```cpp=1 uint8_t hexchar2val(uint8_t in) { const uint8_t letter = in & MASK; const uint8_t shift = (letter >> AAA) | (letter >> BBB); return (in + shift) & 0xf; } ``` AAA = ? 首先觀察字元的==規律==(用==黃色螢光筆==標示) | char | ascii code | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | number to transfer: | |------|------------|----|----|----|----|----|----|----|----|---------------------| | `'0'` | 0x30 | 0 | 0 | 1 | 1 | ==0== | ==0== | ==0== | ==0== | ==0000== | | `'1'` | 0x31 | 0 | 0 | 0 | 0 | ==0== | ==0== | ==0== | ==1== | ==0001== | | `'2'` | 0x32 | 0 | 0 | 0 | 0 | ==0== | ==0== | ==1== | ==0== | ==0010== | | `'9'` | 0x39 | 0 | 0 | 1 | 1 | ==1== | ==0== | ==0== | ==1== | ==1001== | | `'A'` | 0x41 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 1010 | | `'a'` | 0x61 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1010 | | `'F'` | 0x46 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 1111 | | `'f'` | 0x66 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1111 | 可以發現: 要轉換的值其實就是那個 character 的 lower 4 bits e.g. `'0'` = 0x30 = [001100000]~2~ 我們要擷取的就是後 4 bits `0000` :::success 設計 ascii table 的人真的太有智慧了,處處是玄機呢! ::: 由解答回推運算流程 MASK = `(c) 0x40` [0100 0000]~2~ AAA = `(a) 3` [0000 0011]~2~ BBB = `BBB = (a) 6` [0000 0110]~2~ ```cpp=1 uint8_t hexchar2val(uint8_t in) { const uint8_t letter = in & MASK; const uint8_t shift = (letter >> AAA) | (letter >> BBB); return (in + shift) & 0xf; } ``` assume `in` 為 `'f'`[0110 0110]~2~ * line 3: * 經由上面的表格,我們可以觀察到,只有當某個數字是英文字母而不是數字時,`b6` 才是 1 * 所以可以透過這個特性檢查我們的 `in` 是否為英文字母 * 如果 `in` 是英文字母,`letter == 0x40` * 如果 `in` 是數字,`letter == 0x0` ```shell letter := 'f' & 0x40 = [0100 0001] & [0100 0000] = [0100 0000] ``` * line 4,5: ```shell shift := (letter >> 3) | (letter >> 6) = [0000 1000] | [0000 0001] = [0000 1001] ``` * 發現 shift 的作用是如果 in 是 letter 的話,就讓 in + 9 * 因為 a 在 16 進位理面代表 10, | char | ascii code | b3 | b2 | b1 | b0 | lower 4 bits 代表的數值 | |------|------------|----|----|----|----|-------------------------| | 'a' | 0x41 | 0 | 0 | 0 | 1 | 1 | | 'b' | 0x42 | 0 | 0 | 1 | 0 | 2 | | 'c' | 0x43 | 0 | 0 | 1 | 1 | 3 | | 'd' | 0x44 | 0 | 1 | 0 | 0 | 4 | | 'e' | 0x45 | 0 | 1 | 0 | 1 | 5 | | 'f' | 0x46 | 0 | 1 | 1 | 0 | 6 | 可以發現只要把每個 letter 的數值 + 9,再 Mask 掉不必要的 higher 4 bits, 就會等於最終我們要轉換的數字了! * `+9` 這個動作 就對應到 `(letter >> 3) | (letter >> 6)` * 因為如果 in 是英文字母的話, letter == 0x40 * 0x40 >> 3 == 0x08 * 0x40 >> 6 == 0x01 * 兩者做完 `|` 運算剛好是 `9` ! * line 5: ```shell return value := (in + shift) & 0xf = ([0110 0110] + [0000 1001]) & [0000 1111] = [0110 1111] & [0000 1111] = [0000 1111] = 15 ``` :::success * 其實 shift 這個變數改成 offset 更為恰當! * 因為他就是把字母從 lower bits == 1 (a), 2 (b) 3, (c),給他們一個 offset, 讓他們對應到 10 (a), 11 (b), 12 (c) ,13 (d) ... ::: ------ :::success TODO: 修飾說明語句 >[name=Chang Chen Chien][time=Fri, Sep 25, 2020 8:10 AM] ::: :::success 延伸問題: 1. 解釋上述程式的運作原理; 2. 將 hexchar2val 擴充為允許 `"0xFF"`, `"0xCAFEBABE"`, `"0x8BADF00D"` 等字串輸入,且輸出對應的十進位數值 >[Hexspeak](https://en.wikipedia.org/wiki/Hexspeak) ::: ## 測驗 3 除法運算的成本往往高於加法和乘法,於是改進的策略被引入。其中一種策略是將除法改為乘法運算,例如 $\frac{n}{d}$ 在分子和分母分別乘上 $2^N$ 後,得到等價的 $n\times\frac{\frac{2^N}{d}}{2^N}$,其中對 2 的 N 次方 (power of 2) 進行除法,在無號整數的操作就等同於右移 (shift) N 個位元,又當 $d$ (除數) 的數值是已知的狀況下,$\frac{2^N}{d}$可預先計算,也就是說,$\frac{n}{d}$ 可轉換為乘法和位移操作,進而減少運算成本。不過,顯然 $\frac{2^N}{d}$ 無法總是用整數來表達 (僅在 $d$ 是 power of 2 的時候才可),因此,我們可先估算,並將差值補償回去。 重新檢視 $\frac{n}{d}=n\times\frac{\frac{2^N}{d}}{2^N}$,當我們想將 $n$ 除以 $4$ 時,就相當於乘以 $0.25$,所以我們可將 $\frac{5}{4}$ 改為 $5 \times 0.25$,我們得到整數的部分 (即$1$),和小數部分 (即$0.25$),後者乘以 $4$ 就是 $1$,也就是餘數。下方程式碼展示這概念: ```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; } ``` 其中 __uint128_t 是 GCC extension,用以表示 128 位元的無號數值。 >[GCC: C Extensions: 128-bit Integers](https://gcc.gnu.org/onlinedocs/gcc/_005f_005fint128.html) 預期 `quickmod(5)` 會得到 `2`, `quickmod(55)` 得到 `1`, `quickmod(555)` 得到 `0` (`555` 是 `3` 的倍數)。 請補完程式碼。 作答區 XXX = ? * `(a)` `0x00F000F000F00080` * `(b)` `0xF000F000F000F000` * `(c)` `0x0F0F0F0F0F0F0F0F` * `(d)` `0xF0F0F0F0F0F0F0F0` * `(e)` `0x8888888888888888` :::success 完全看不懂 ... 先從 整數除法開始下手! [LeetCode 29: Divide Two Integers 解析](/ubTTbJcnRrq8r0ybHqfOZQ) ::: ### 數學推導 >摘自 < [WeiCheng14159](https://hackmd.io/5EJdbAmHTFWHfdqbqx2lwQ?view)> 同學的數學推導 \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 \left( n \times \frac{2^{64}}{3} \right) \times \frac{1}{2^{64}} \right \rfloor \times 3\\ &= n - ((n \times M) >> 64) \times 3 \\ \end{align} :::success > ## Macro M 的解析 > `#define M ((uint64_t)(UINT64_C(XXX) / (D) + 1))` * 第二行同乘 $2^{64}$ 答案是否與之前一樣呢? * $\frac{2^{64}}{D}$ 何時為整數? * 如何用 integer 計算 $\frac{n}{d}$ * 如何確保精確度,不論 $\frac{2^{64}}{D}$ 是否為整數 * `UINT64_C` 是什麼? * integer lieral * ref: [gnu.org](https://www.gnu.org/software/libcdio/doxygen/types_8h.html#a26a7bac63d90ef61175acb9f6fc4f2ca) * UINT64_C 功用是在輸入的東西後面利用 `##` 接上 `ULL` * `ULL` 作用是? * specify 這個 literal 的 data type `unsigned long long` * 確保運算結果不會 overflow * [stackoverflow 解答](https://stackoverflow.com/questions/13134956/what-is-the-reason-for-explicitly-declaring-l-or-ul-for-long-values) * [示範](https://difyel.com/c/literal/integer-literals-in-c/) ::: :::success TODO: run `fastdiv` to profile the behavior of macro `M` 回答 如何確保精確度,不論 $\frac{2^{64}}{D}$ 是否為整數 ::: :::info 不懂: 整數除法上下乘以 $2^{64}$ 後結果是否一樣 ::: ### jemalloc 的 `div.c` 假設我們有 $n = q \times d$, 其中 $n$, $q$, $d$ 都是正整數,$n$ 和 $d$ 已知 我們的目標是不使用除法運算來求出 q = n / d 對於任何 k, 我們可以得到 q = $$ \left\lfloor{\left\lceil\frac{2^k}{d}\right\rceil \times \frac{n}{2^k}} \right\rfloor $$ 我們要找出他在何種條件下等於 $\frac{n}{d}$ 上式又可寫成 $$ \left\lfloor\frac{2^k+r}{d} \times \frac{n}{2^{k}} \right\rfloor $$ 其中 * :question: How do we get r? $\left\lceil\frac{2^k}{d}\right\rceil$ 如何變成$\frac{2^k+r}{d}$ ? $$ \quad r = (-2^{k})\mod d $$ 代入 r, 展開式子可得 $$ =\left\lfloor \frac{2^k}{d} \times \frac{n}{2^{k}} + \frac{r}{d} \times \frac{n}{2^k} \right\rfloor $$ $$ =\left\lfloor \frac{n}{d} + \frac{r}{d} \times \frac{n}{2^k} \right\rfloor $$ 因為前提是 $\frac{n}{d}$ 為整數,所以上式也可以寫成 $$ \frac{n}{d} + \left\lfloor \frac{r}{d} \times \frac{n}{2^k} \right\rfloor $$ 如果可以滿足 $$ \frac{r}{d} \times \frac{n}{2^k} < 1 $$ 則 $\frac{n}{d}$ (除法運算) 就可以簡化成 $\left\lfloor{\left\lceil\frac{2^k}{d}\right\rceil \times \frac{n}{2^k}} \right\rfloor$ (乘法與 bitwise shift) 因為 r < d 且 r / d < 1 總是滿足,為使 $\frac{n}{2^k}$ ,對於不會超出 $2^{32}$ 的 n,可以令 k = 32 來滿足此條件 * :question: How do we get r? * 參考解釋 * [nelsonlai1 同學](https://hackmd.io/@nelsonlai1/2020q3_quiz2#%E6%B8%AC%E9%A9%97-3) * [eechang 同學合理推導](https://hackmd.io/@eecheng/HJgpR7RNw#%E6%B8%AC%E9%A9%97-3) * [oOcarShiang 同學對 quickmod 解釋較為清楚](https://hackmd.io/@oscarshiang/sysprog_quiz2#%E6%B8%AC%E9%A9%97%E4%B8%89) * [justapig9020 同學對於誤差補償的說明](https://hackmd.io/@justapig9020/SJGJAARVw#%E6%B8%AC%E9%A9%97-3) * [ChongMingWei 同學對於各項說明與實驗都很完整!!明天先參考他的](https://hackmd.io/@cmwchw/2020q3-quiz2#2020q3-Homework2-quiz2) * [WeiCheng14159 同學對 n % 3 的數學推導很棒 還有 jemalloc 講得很完整](https://hackmd.io/@WeiCheng14159/HkesfMvBP#Test3) :::success TODO: 解釋 magic number ::: <br><br><br><br><br><br> <br><br><br><br><br><br> <br><br><br><br><br><br> <br><br><br><br><br><br> ## 測驗 4 ## 測驗 5 ## 測驗 6 # Reference [2020q3 第 2 週測驗題](/@sysprog/2020-quiz2) [作業區](https://hackmd.io/@sysprog/2020-homework2) [`RinHizakura` 同學共筆](https://hackmd.io/@RinHizakura/SJ5NuIANP)