# 2020q3 Homework2 (quiz2)
contributed by < `grb72t3yde` >
###### tags: `sysprog2020`
---
## 測驗題目
> [第二週測驗題](https://hackmd.io/@sysprog/B1zAbkAEP)
## 測驗`一`
目前電腦中用得最廣泛的字元集及其編碼,是美國國家標準局 (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),程式碼改寫如下:
```clike=
#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;
}
```
---
### 個人解題思路
#### 1. MMM = ?
1. 首先觀察改寫後之 `is_ascii`, 我從 `line 10` 的 `while loop condition` 以及 `payload` 的 `data type` 得知, 這個 while loop 是以 `word width` 來做是否為 `ascii` 的判斷。
2. 考慮對於任一 7位元編碼的 ASCII 字元, 其 `MSB` 必為 0 (其範圍在0~127); 因此, `MMM` = `0x8080808080808080`。
3. 對於不滿一個 `word` 的 `bytes` 組, 會在第二個 `while loop` 對 `byte` 逐一檢查。
### 延伸問題
#### 1. 解釋上述程式的運作原理,特別是為何用到 memcpy 呢?
* 參考提示 [你所不知道的 C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory)後得知, "編譯器會自動幫我們以 data 的大小做 alignment"; 因此在 `is_ascii` 函式中之 argument `const char str[]` 接受的引數以 `const char` 之寬度對齊
* 如今, 我們想要以 `word` 的寬度進行 bit-wise operation, 因此使用 `memcpy`, 將 8個 byte 的資料複製到已宣告且以 `uint64_t` type 來對齊的變數 `payload` 中
* 以此達到 "alignment 寬度" 之轉換
> 思考 : 如果不使用 `memcpy`?
考慮下列修改 :
```diff=
#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;
+ uint64_t *payload = (uint64_t)str;
while ((i + 8) <= size) {
- uint64_t payload;
- memcpy(&payload, str + i, 8);
if (*payload & MMM)
return false;
+ payload++;
i += 8;
}
while (i < size) {
if (str[i] & 0x80)
return false;
i++;
}
return true;
}
```
使用 `gdb` 觀察, 仍能達到類似效果; 但是參考 [Should I worry about the alignment during pointer casting?](https://stackoverflow.com/questions/13881487/should-i-worry-about-the-alignment-during-pointer-casting) 後得知, 這是相較於使用 memcpy() 較危險且不具效率的方法 -> 待研究細節。
#### 2. 將上述技巧應用於「給予一個已知長度的字串,檢測裡頭是否包含有效的英文大小寫字母」
* 查詢 `ASCII` 表
```shell=
$ man ascii
```
![](https://i.imgur.com/2aOpOMZ.png)
得知:`A` 至 `Z` 的範圍在 `0x41 ~ 0x5a` ; `a` 至 `z` 的範圍在 `0x61 ~ 0x7a` 。
* 想法 : 將所有輸入字串轉為大寫 (或是小寫), 再判斷其是否在對應的區間
* 實驗 : TODO
#### 3. 承 (2),考慮常見的英文標點符號,判斷輸入字串是否為有效的字元集,避免逐一字元比對
* TODO
## 測驗`二`
開發解析器 (parser) 時,常會將十六進位表示的字串 (hexadecimal string) 轉為數值,例如 '0xF' (大寫 F 字元) 和 '0xf' (小寫 f 字元) 都該轉換為 15。考慮以下不需要分支 (branchless) 的實作
```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;
}
```
### 個人解題思路
#### 1. MASK = ?
1. 我首先觀察到的是 return value 的部分, 使用 mask 遮去了高位的4個位元; 因此, 我判斷此作法利用 shift 將原 input 的低位4個位元修正成我們想要的結果 (0~15的任一個值)
2. 乘上, 我觀察大小寫英文字母與數字字元在 `ASCII` 編碼中的低位4個 byte, 發現 : 數字字元不需要修正, 而英文字母需要 `+ 0x09` (e.g. 0x41 ('A') + 0x09 = 0x4a, mask 掉高位的4個 bit 後得到 0x0a = 10)
3. 考慮到數字與字母需要用到不同的 `shift` 修正值, 我思考要用 MASK 留下 in 中**得以區別字母與數字的部分**
4. 觀察大寫, 小寫字母與數字的高位4個bit, 如下 :
> 大寫字母字元 : 0==1==00
小寫字母字元 : 0==1==10
數字字元 : 0==0==11
發現可以用 `0100` 來 mask 出字母與數字! 因此初步判定 `MASK` = `0x40`
#### 2. AAA = ?, BBB = ?
1. 基於`MASK` = `0x40` 以及 `in` 只會是對應到 `ASCII` 中大小寫字母與數字假設, 我們得到
> letter = 0x40, if `in` 對應到的 `ASCII` 為大小寫字母
> letter = 0x00, else
2. 基於前述觀察 (字母需要 `+0x09` 的修正, 而數字不用), 可以判斷 AAA 和 BBB 其一為3, 另一個則為6
### 延伸問題
* 將 `hexchar2val` 擴充為允許 `"0xFF"`, `"0xCAFEBABE"`, `"0x8BADF00D"` 等字串輸入,且輸出對應的十進位數值
想法 : 首先考慮輸入字元的個數, 如果我們想要以 `word` 來處理資料, 需要考慮到補 `0`
實驗 : TODO
## 測驗`三`
* 快速除法概念 : 將除法運算轉換成 `shift operation` 以及 `乘法` 的組合
```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 位元的無號數值。
### 個人解題思路
#### 1. XXX = ?
1. 觀察 line6, 將此行對應至題目給訂的公式 $n * ((2^N/d) / 2^N)$, 可初步判斷這裡的 N 取的是64
2. 觀察到 `D = 3` 不為一個 `power of 2` 的數值, 因此思考題目敘述 : "我們可先估算,並將差值補償回去" 後, 判斷 line2 的 macro 應該在做此 "估算並補償的動作"
3. 有了這個初步的想法卻卡住了, 不知道為何聚集是如此定義, 因此決定直接先去 trace [src/div.c](https://github.com/jemalloc/jemalloc/blob/dev/src/div.c)
4. 程式碼的註解中解釋並證明了藉由使用 `ceil(2^k / d)` 作為 `magic`, 並且設定 $k = 32$ (能使得 n / 2^k 恆小於 1) 的情況下 :
能使得 `floor(ceil(2^k / d) * n / 2^k)` 得到 $n/d$ 在數學上正確的 quotient
5. 此時回頭檢視我們定義的 macro `M`, 並根據 `ISO/IEC 9899:201x`, `UINT64_C(XXX)` 將 `XXX` expand 至 `unsigned long long int`, 這個 data type 沒辦法正確地表示出 $2^{64}$
6. 已知 `C` 使用 `floor` 處理商數的小數點, 於是我們使用 `+1` 來達到 `ceiling`
7. 我們要證明 $\ floor(2^{64}/3) + 1 = floor((2^{64}-1)/3) + 1$; 事實上, 因為 $2^{64} \mod 3 \neq0$, 我們定義的 macro M 在 `d = 3` 時永遠可以得到對的結果;
### 延伸問題
TODO
## 測驗`四`
延伸測驗 `3`,我們想判斷某個數值能否被指定的除數所整除,在 D 和 M 都沿用的狀況下,程式碼如下:
```cpp=
bool divisible(uint32_t n)
{
return n * M <= YYY;
}
```
### 個人解題思路
#### 1. YYY = ?
1. 先思考 $\forall n, n = 3k \lor 3k + 1 \lor 3k + 2, n <= 0xffffffff$
2. 考慮 $n*M$ 在這三種情況下分別可**表示**成以下三種形式
* if $n = 3k, 此時k<=0x55555555$
* 此時$n*M = 3k * ((2^{64} - 1)/3+1)$
* 則$n*M = k*(0xffffffffffffffff + 3) = k*2 = 2k$, 因為此時$(0xffffffffffffffff + 3)$ 溢位回 `+2`
* if $n = 3k + 1, n*M = 2k + M$
* if $n = 3k + 2, n*M = 2k + 2M$
可以看出在 n 這三種情況下, n*M 的值會因為數值溢位會有對應且固定的表示公式, 再來我們從答案選出一個定值, 能夠分界出 `n = 3k` 與 `n = 3k + 1 和 n = 3k + 2` 的 case
## 測驗`五`
考慮 `strlower` 函式的作用是將指定的字串 (或部分字串) 的英文字母全改為小寫,其 in-place 的實作如下:
```cpp=
#include <ctype.h>
#include <stddef.h>
/* in-place implementation for converting all characters into lowercase. */
void strlower(char *s, size_t n)
{
for (size_t j = 0; j < n; j++) {
if (s[j] >= 'A' && s[j] <= 'Z')
s[j] ^= 1 << 5;
else if ((unsigned) s[j] >= '\x7f') /* extended ASCII */
s[j] = tolower(s[j]);
}
}
```
在 64 位元處理器架構 (以下針對 x86_64, little endian),我們可引入向量化 (vectorized) 的手段,避免一次比對一個字元,而是直接操作一整個 word (對 x86_64 來說,就是 64 bits,或 8 個位元組)。沿用上述的 strlower 函式,我們用這樣的思路實作向量化的 strlower,程式碼列表如下:
```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);
}
```
### 個人解題思路
#### 1. VV1 = ?
1.
## 測驗`六`