# 2020q3 Homework2 (quiz2) contributed by < `erickuo5124` > ###### tags: `2020進階電腦系統理論與實作` --- ## 測驗`1` 目前電腦中用得最廣泛的字元集及其編碼,是由美國國家標準局(ANSI)制定的ASCII碼,這些碼可以排列成一個十進位序號0~127。所以,7 位ASCII碼是用七位元二進位數字進行編碼的,可以表示128個字元。 :::warning 在電腦的存儲單元中,一個ASCII碼值占一個位元組(8個二進位位元),其最高位元(b7)用作奇偶校驗位。 ::: 下面的程式碼式用來判斷指定的記憶體範圍內是否全是有效的 ASCII 字元,且一次比對 64 位元的資料 (word size): ```c= #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; } ``` 一開始處理 `size` 為 0 的情況。進到第一個 while 迴圈之後,一次處理 8 個字元 (一個字元為 8 bits, 8 * 8 = 64),並使用 `memcpy` 將資料複製到 `payload` 裡面。接著用 0x8080808080808080 對它作 `AND` (0x80 的值是 $10000000_2$ , ASCII 字元理應不超過 127) ,若是有 0 以外的值就回傳 false。 e.g. $$ \ \ \ \ \ \ \ \ \ \ \ \ 10000000_2\\ \frac{AND\ \ 10100110_2}{\ \ \ \ \ \ \ \ \ \ \ \ 10000000_2} $$ 第二個 while 迴圈則處理不滿 8 個字元的情況。 :::success 延伸問題: 1. 為了提升程式效能,並考慮到 data alignment ,使用 `memcpy` 複製指定長度的資料。 2. 必須判斷是否在 `'A'-'Z'` 或 `'a'-'z'` 間,類似測驗`5`,因此將部份程式碼來改寫: ```c= #include <stdbool.h> #include <stddef.h> #include <stdint.h> #include <string.h> #define PACKED_BYTE(b) (((uint64_t)(b) & (0xff)) * 0x0101010101010101u) 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); uint64_t A = *payload + PACKED_BYTE(128 - 'A'); uint64_t Z = *payload + PACKED_BYTE(128 - 'Z' - 1); uint64_t a = *payload + PACKED_BYTE(128 - 'a'); uint64_t z = *payload + PACKED_BYTE(128 - 'z' - 1); uint64_t mask = ((A ^ Z) | (a ^ z)) & PACKED_BYTE(0x80); if(mask ^ PACKED_BYTE(0x80)) return false; i += 8; } while (i < size) { if (str[i] < 65 || (str[i] > 90 && str[i] < 97) || str[i] > 122) return false; i++; } return true; } ``` ::: 參考資料 * [你所不知道的 C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory) * [ASCII 碼介紹](http://kevin.hwai.edu.tw/~kevin/material/JAVA/Sample2016/ASCII.htm) --- ## 測驗`2` 下面程式碼會將十六進位的字串轉為數值 ```c= uint8_t hexchar2val(uint8_t in) { const uint8_t letter = in & 0x40; const uint8_t shift = (letter >> 3) | (letter >> 6); return (in + shift) & 0xf; } ``` 第 3 行的 `letter` 用來判斷 `in` 是否為英文字母, 又第 7 個 bit 為英文字母都有而數字沒有的,因此選 0x40。 因為英文字母的後面 4 bit 剛好跟數值差 9 ,因此第 2 行的 `shift` 將第 7 個 bit 分別移到 1 跟 4 的位置。若 `letter` 有值, `shift` 會為 9 (0000 1001) ex: - `'A'`: 0100==0001== - `'C'`: 0100==0011== 數字的後面 4 bit 則剛好為其數值,且不是字母 `letter` 會為 0, `shift` 也會為 0 ex: - `'1'`: 0011==0001== - `'6'`: 0011==0110== 第 5 行則將結果加上原本的數值,並傳後面 4 bit 回去 :::success 延伸問題: 1. 利用 acsii 中相對應的位置,找出字元的特性,利用 bitwise 操作讓實作達到不需要使用分支 2. ::: --- ## 測驗`3` 下方程式碼實作取餘數 (除數為 3),並用乘法來代替除法運算,減少運算成本 原理: $$ \frac{n}{d}=n\times\frac{\frac{2^N}{d}}{2^N} $$ 當 $d$ (除數) 的數值是已知的狀況下,就可以先計算$\frac{2^N}{d}$,將它轉換為乘法和位移操作 ```c= 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; } ``` 第 7 行即為上面所提到的乘法及位移操作,其中的 `M` 先使用預處理器完成,乘上 `n` 之後向右移 64 位元,因此可以知道本題的 $N$ 為 64 :::warning `M` 在預處理器那邊所使用的算式並非 $\frac{2^N}{d}$ ,而是 $\frac{2^N-1}{d}+1$ ,因為這裡考量到了並非所有 $d$ 都能整除,且: $$ \frac{2^N}{d}=\lfloor\frac{2^N}{d}\rfloor\\ \frac{2^N-1}{d}+1=\lceil\frac{2^N}{d}\rceil $$ 這裡的實作選擇後者將差值補償回去 (不過我不知道為什麼:joy:) ::: 最後用 $被除數-商\times除數$ 得到餘數 :::success 延伸問題: 1. **除數已知的情況下**,對它做預處理之後可將除法轉換成乘法及位移運算,如此可有效降低運算成本 2. ::: --- ## 測驗`4` ```c= bool divisible(uint32_t n) { return n * M <= M - 1; } ``` - $n\times M = n\times \lceil\frac{2^N}{d}\rceil$ => $\frac{n}{d}$ 的小數部份 - $M-1 = \lfloor\frac{2^N}{d}\rfloor$ => $\frac{1}{d}$ 的小數部份 沒有餘數的情況只會是當 $n=0$ 或 $n=d$ ,其他狀況下 $\frac{n}{d}$ 皆會大於等於 $\frac{1}{d}$ ,因此選擇 `M-1` --- ## 測驗`5` 下面的程式碼將字串內所有大寫字母轉換成小寫字母,且一次對 64 位元的資料進行操作 ```c= #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` 擴充成重複 8 次的 64 位元無號整數 第 13 行為判斷該 `chunk` 是否在 ascii 的範圍內,因此會對 `PACKED_BYTE(0x80)` 做 AND 運算 先看第 16 行, `mask` 對 `A^Z` 跟 `PACKED_BYTE(0x80)` 做 AND ,可以知道重點應該著重在每個資料的最高位。 那再往回看到 14, 15 行, `uint64_t A` 在字元大於等於 `'A'` 的時候, MSB 會是 1; `uint64_t Z` 則是在字元小於等於 `'Z'` 的時候, MSB 會是 0。 如此一來,若該字元在 `'A'` 到 `'Z'` 之間時, `A^Z` 的值將會是 0x80 ,作 AND 運算之後往右移的結果會是 0x20 。又大寫字母對 0x20 作 XOR 會得到小寫的字母,即: $$ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 0100\ 0001_2(A)\\ \frac{XOR\ \ 0010\ 0000_2} {\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 0110\ 00001_2(a)} $$ :::success 延伸問題: 1. 利用 `PACKED_BYTE` 擴充每次做運算的長度,並用 bitwise opration 來判斷是否在範圍內,進而達到增加程式效率 2. ::: 參考資料 * [你所不知道的 C 語言: bitwise 操作](https://hackmd.io/@sysprog/c-bitwise) --- ## 測驗`6` 下面的程式碼用來找出陣列中指出現一次的元素 ```c= 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$ 的**交換律**在此可以允許輸入不須按照順序,即: $$ a\oplus b\oplus c = a\oplus c\oplus b $$ **歸零律**則讓它有儲存的功能,即: $$ a\oplus a = 0 $$ 而因為除了那個指出現一次的元素之外,其他的元素均會出現 3 次,因此這裡使用了 `higher` 跟 `lower` 來儲存。若存在 `lower` 中,則代表第一次出現;若存在 `higher` 中,則代表出現第 2 次;第三次出現的話會把兩邊都消除掉。 第 6 行會在第三次出現時將它清除掉,選 `~higher` 第 7 行則是檢查是否出現第 2 次,選 `~lower`