# 2020q3 Homework2 (quiz2)
contributed by < `shauming1020` >
###### tags: `homework`
## 測驗1
```c=
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. 解釋上述程式的運作原理,特別是為何用到 memcpy 呢?
- 一個 ASCII 字元開頭的 bit 為 0,可利用 mask 0x80 (0b10000000) 直接與一個字元做比較,原第一段程式碼 ```if (str[i] & 0x80) /* i.e. (unsigned) str[i] >= 128 */``` 逐一**比較 1 個字元 (8-bits)**
- 上述程式碼改採用一次**比較 8 個字元 (64-bits)** 來判斷,因此可以很直觀的得到 **mask MMM 為 0x8080808080808080***
- 優化的 memcpy 算法會 data alignment,加速 cpu 運算
https://www.embedded.com/optimizing-memcpy-improves-speed/
i = 0, str + i:
| Address | 1 byte | Char |
| -------- | -------- | -------- |
| 0x56440dc9e9a0 | 0100 0001 | A |
| 0x56440dc9e9a1 | 0100 0010 | B |
| ...| ...| ...|
| 0x56440dc9e9a7 | 0100 1000 | H |
i = 8, str + i:
| Address | 1 byte | Char |
| -------- | -------- | -------- |
| 0x56440dc9e9a8 | 0100 0001 | A |
| 0x56440dc9e9a9 | 0100 0010 | B |
| ...| ...| ...|
| 0x56440dc9e9af | 0100 1000 | H |
payload
| 0100 1000 0100 0111 ... 0100 0010 0100 0001 |
| --- |
Mask (MMM)
| 1000 0000 1000 0000 ... 1000 1000 1000 1000 |
| --- |
### 2.將上述技巧應用於「給予一個已知長度的字串,檢測裡頭是否包含有效的英文大小寫字母」
- 逐一字節的判斷
```c=
bool is_alpha(const char str[], size_t size)
{
if (size == 0)
return false;
for (int i = 0; i < size; i++) {
int diff_A = str[i] - 'A', diff_a = str[i] - 'a';
if ((0 <= diff_A && diff_A < 26) ||
(0 <= diff_a && diff_a < 26))
return true;
}
return false;
}
```
### 3. 承 (2),考慮常見的英文標點符號,判斷輸入字串是否為有效的字元集,避免逐一字元比對
---
## 測驗2
### 1.解釋上述程式的運作原理
```c=
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,其他輸入都有對應的數值。
``` #3 const uint8_t letter = in & MASK; ```
- 觀察字母 'A', 'a' 的 ASCII,可以發現兩者皆**大於 0x0100_0000 (0x40)**,因此可以用 Mask 0x40 來**偵測該 in 是否為字母**,如果是字母的話則 letter 就會被設成 0x40
```#4 const uint8_t shift = (letter >> AAA) | (letter >> BBB);```
- 觀察字母 'A', ..., 'F' 及 'a', ..., 'f' 的後 4 個 bits,為 '0001', ..., '0110'
- 而要將 'A', 'a' 輸出成 10 (0b1010) 且 'F', 'f' 輸出成 15 (0b1111)
- 根據最後一行提示 ```#4 return (in + shift) & 0xf;```
- **&0xf 保留後 4 個 bits**
- in + shift 要恰為 (0b1010), ..., (0b1111)
- 因此 shift **得為 0b00001001**
- 可利用 letter >> **3** | letter >> **6** 得到
- **MASK = 0x40,AAA = 3,BBB = 6**
### 2.將 hexchar2val 擴充為允許 "0xFF", "0xCAFEBABE", "0x8BADF00D" 等字串輸入,且輸出對應的十進位數值
```c=
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
uint8_t hexchar2val(uint8_t in)
{
const uint8_t letter = in & 0x40;
const uint8_t shift = (letter >> 3) | (letter >> 6);
return (in + shift) & 0xf;
}
uint64_t hexspeak(char str[])
{
int len = strlen(str);
if (len > 18)
return NULL;
if (str[0] != '0' || (str[1] != 'x' && str[1] != 'X'))
return NULL;
uint64_t res = 0, x = 0;
for (int i = len-1, n = 0; i >= 2; i--, n+=4) {
x = hexchar2val(str[i]) << n;
res += x;
}
return res;
}
main () {
uint64_t x = hexspeak("0x0B00B135"); // 184594741
printf("%i" , x);
}
```
---
## 測驗3
```c=
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;
}
```
### 1.解釋上述程式的原理
乘法和位移操作 $\dfrac{n}{d}$ = ${n}*{\dfrac{\dfrac{2^N}{d}}{2^N}}$
```#6 uint64_t quotient = ((__uint128_t) M * n) >> 64;```
- 右移 64 bits 相當於除 $2^{64}$
- 用 __uint128_t 來 catch ${M} * {n}$ 的高位 bit,擴大乘法後可表示的數值範圍
- 將程式碼整理成數學式 ${\dfrac{({M}*{n})}{2^{64}}}$
```#2 #define M ((uint64_t)(UINT64_C(XXX) / (D) + 1))```,其中 ${M}={\dfrac{2^{64}}{d}}$ $=$ ${\lfloor}{\dfrac{{{XXX}}}{d}{\rfloor} +1}$
- ${\dfrac{2^{64}}{d}}$ 無法總是用整數來表達,利用可表示數值範圍的最大值來估算除 d 的結果,再將差值補回去
- 64-bits 的**最大值 XXX = 0xFFFFFFFFFFFFFFFF** 與 $2^{64}$ 恰為 1,而除 d 後最壞情形是如 ${\lfloor}1.999..{\rfloor} = 1$,捨去了小數點後非常接近 1 的值,因此一概將取完 floor 的值補上修正值 1
### 2. jemalloc
---
## 測驗4
延伸測驗 3,我們想判斷某個數值能否被指定的除數所整除,在 D 和 M 都沿用的狀況下,程式碼如下:
```c=
bool divisible(uint32_t n)
{
return n * M <= YYY;
}
```
- ${\lfloor} \dfrac{n}{d} {\rfloor} = (n * M) >> 64$
- M 為估算 ${\dfrac{2^{64}}{d}}$ 的結果,$n * M$ 為 $\dfrac{n}{d}$ 乘上 $2^{64}$,即 $\dfrac{n}{d}$ 小數的部份
- ${M} - 1 =$ ${\lfloor}{\dfrac{{{0xFF..FF}}}{d}{\rfloor}}$ 為 $\dfrac{1}{d}$ 乘上 $2^{64}-1$,即 $\dfrac{1}{d}$ 小數的部份
- 如果 n 要可被 d 整除,表 $\dfrac{n}{d}$ 小數的部份 < $\dfrac{1}{d}$ 小數的部份
- **YYY = M - 1**
---
## 測驗5
```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(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.解釋上述程式的原理
- 從 ```#13 if ((*chunk & PACKED_BYTE(VV1)) == 0) { /* is ASCII? */ ``` 給的提示可以知道此行程式碼在檢測 *chunk 是否為 ASCII
- ``` #5 #define PACKED_BYTE(b) (((uint64_t)(b) & (0xff)) * 0x0101010101010101u) ```
- ((uint64_t)(b) & (0xff)) 保留所有 (b) 的 bits
- 與 0x0101010101010101u 相乘相當於將 (b) 每隔兩個 bit 在放值
- 假設 (b) = 0xb0 ,得 0xb0b0b0b0b0b0b0b0,因此 **PACKED_BYTE(b) 將輸入做相同值的 padding**
- 根據測驗一的結果,**VV1 為 0x80**
- 改成 ```#define PACKED_BYTE(b) ((uint64_t)(b) * 0x0101010101010101u)```
- 從 strlower 函式觀察,```#7 if (s[j] >= 'A' && s[j] <= 'Z')``` then ```s[j] ^= 1 << 5;```
- 得知可透過 **mask 0x20 (1 << 5)** 與大寫字母做 XOR 轉成小寫字母
- ```#17 *chunk ^= mask;``` ,*chunk 與 mask 做 XOR
- 因此當字母為大寫時,mask 得為 0x20
- 字母為小寫時,讓 **mask 為 0x00** 則可保留原 bit,保持小寫字母
- 觀察 ```A = *chunk + PACKED_BYTE(128 - 'A') ``` 和 ```Z = *chunk + PACKED_BYTE(128 - 'Z') ```
| char | char + (128 - 'A') | char + (128 - 'Z') |
| -------- | -------- | -------- |
| 'A'(65) | 128 | 103 |
| 'Z'(90) | 153 | 128 |
| 'a'(97) | 160 | 135 |
| 'z'(122) | 185 | 160 |
- 當字母為 'a' ~ 'z' 時,其最高位的 bit 皆為 1 (>= 128)
- 而在字母為 'Z' 時,char + (128 - ‘Z’) 的結果也 >= 128
- 因此若想要透過 ```#16 uint64_t mask = ((A ^ Z) & PACKED_BYTE(VV4)) >> 2;``` 來做出 **0x00 或是 0x20 的 mask**
- 可以先將 char + (128 - ‘Z’) **減1**,**使其最高位 bit 在大小寫字母不同時可以區別**
| char | char + (128 - 'A') | char + (128 - 'Z') - 1|
| -------- | -------- | -------- |
| 'A'(65) | 128 | 102 |
| 'Z'(90) | 153 | 127 |
| 'a'(97) | 160 | 134 |
| 'z'(122) | 185 | 159 |
- char 在大寫時,(A ^ Z) **最高位為 1,小寫時為 0**
- 再與 0x80 做 & **保留最高位 bit 後,進行右移 2 bit**
- 因此當 char 大寫,0x80 >> 2 得 0x20,同理小寫 0x00 >> 2 得 0x00
### 2. 將前述測試程式 main 函式的 char str[] 更換為 char *str,會發生什麼事?請參閱 C 語言規格書,說明 string literal 的行為
- char str[] 更換為 char *str,會 Segmentation fault
- 規格書 6.4.5 String literals 提到
> a byte or code of value zero is appended to each multibyte character sequence that results from a string literal or literals.66) The multibyte character sequence is then used to **initialize an array of static storage** duration and length just sufficient to contain the sequence.
靜態配置足夠大的陣列空間存放每個字元
> It is unspecified whether these arrays are distinct provided their elements have the appropriate values. If the program attempts to **modify such an array, the behavior is undefined**.
但是修改其內容是未定義的行為
- char *str 是指向 string literal 的指標,如果對該指標變數操作則會直接更改到原 string literal 的內容,為未定義行為
- 而 char str[] 則配置新的字元陣列來存放與 string literal 相同的值,因此對其操作不會更改到原 string literal 的內容
> [What is the type of string literals in C and C++?](https://stackoverflow.com/questions/2245664/what-is-the-type-of-string-literals-in-c-and-c)
---
## 測驗6
LeetCode 137. Single Number II:
> Given a non-empty array of integers, every element appears three times except for one, which appears exactly once. Find that single one.
>
> Note:
Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?
>
> Example 1:
Input: [2,2,3,2]
Output: 3
>
>Example 2:
Input: [0,1,0,1,0,1,99]
Output: 99
```c=
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;
}
```
### 1. 解釋上述程式的原理
- 將整數看成數個獨立 bit,如 2 = 0b0010、3 = 0b0011
- 當一個數最多出現 2 次時,可以只用 1 bit 來描述,而當一個數最多出現 3 次時,則需要用 2 bit 來描述
- 00 表一個數未出現、01 表出現一次、10 表出現兩次
- 當出現三次時應為 11,但將其重置為 00 來表示該數已達上限,即該數不是要找的**恰出現一次**的數,因此給定一個數,其出現次數的規律為 00 -> 01 -> 10 -> 00
- 真值表如下,其中 high 表計數過程中的高位,low 表對應的低位,input 表下一個輸入, **high' 和 low' 表對應的高低位輸出**
| high| low |input| high'| low'|
| --- | --- |-----| ---- | ----|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 0 | 1 |
| 1 | 0 | 0 | 1 | 0 |
| 0 | 0 | 1 | 0 | 1 |
| 0 | 1 | 1 | 1 | 0 |
| 1 | 0 | 1 | 0 | 0 |
- 最後一行當 high/low 狀態為 10 時,輸入 1 後輸出為 00 而非 11
- 高低位的邏輯表達式
- ${low} = {low} \cdot {\overline {high}} \cdot {\overline {input}} + {\overline {low}} \cdot {\overline {high}} \cdot {input}$
$= {\overline {high}} \cdot ({low} \cdot {\overline {input}} + {\overline {low}} \cdot {input)}$
$= {\overline {high}} \cdot ({low} \oplus {input})$
- ${high} = {high} \cdot {\overline {low}} \cdot {\overline {input}} + {\overline {high}} \cdot {low} \cdot {input}$
- low' 即表示出現一次的整數,因為 0、2、3次對應的 low' 皆為 0,這也解釋為何要將 11重置為 00,**避免兩種情形的 low' 為 1**
- 利用上述式子計算 low' 和 high' 時,皆是沿用舊的 low 值,需要先暫存起來以待計算,嘗試修改真值表,避免用舊值計算
- 將先前真值表的 low' 輸出取代掉 low,即 low 的輸出重新作為輸入
| high|**low**|input| high'|
| --- | --- |-----| ---- |
| 0 |**0**| 0 | 0 |
| 0 |**1**| 0 | 0 |
| 1 |**0**| 0 | 1 |
| 0 |**1**| 1 | 0 |
| 0 |**0**| 1 | 1 |
| 1 |**0**| 1 | 0 |
- 重新計算高位表達式
${high} = {high} \cdot {\overline {low}} \cdot {\overline {input}} + {\overline {high}} \cdot {\overline {low}} \cdot {input}$
$= {\overline {low}} \cdot ({high} \cdot {\overline {input}} + {\overline{high}} \cdot {input})$
$= {\overline{low}} \cdot ({high} \oplus {input})$
- 從上述得 KKK = ~higher,JJJ = ~lower
> [參考] https://blog.csdn.net/yutianzuijin/article/details/50597413
### 2. 請撰寫不同上述的程式碼,並確保在 LeetCode 137. Single Number II 執行結果正確且不超時
- 撰寫沿用舊 low 值的版本,額外宣告 tmp 變數暫存舊 low 值來計算 higher、lower
```c=
int singleNumber(int *nums, int numsSize)
{
int lower = 0, higher = 0, tmp = 0;
for (int i = 0; i < numsSize; i++) {
tmp = lower;
lower = ~higher & (lower ^ nums[i]);
higher = (higher & ~tmp & ~nums[i]) | (~higher & tmp & nums[i]);
}
return lower;
}
```
![](https://i.imgur.com/bmdF64z.png)
### 3. 延伸題意,將重複 3 次改為改為重複 4 次或 5 次,並找出可容易推廣到多次的程式碼
- 重複 4 次,用 3 個 bit 描述,000 -> 001 -> 010 -> 100 -> 000
- 重複 5 次,用 4 個 bit 描述,0000 -> 0001 -> 0010 -> 0100 -> 1000 -> 0000
- 重複 n 次,用 n-1 個 bit 描述
- 建真值表後計算邏輯表達式,撰寫程式碼