# 2020q3 Homework2 (quiz2)
contributed by < `ccs100203` >
###### tags: `linux2020`
- [2020q3 第 2 週測驗題](/@sysprog/2020-quiz2)
:::info
TODO 一些延伸問題
:::
## 測驗 1
從題目敘述得知,7 位碼 ASCII 是以 7 位元二進位數字進行編碼,可表示 128 個字元,第 128~255 號為擴展字元 (extended ASCII)。
而 0x80 為 1000 0000,所以當以下成立時,代表 `str` 已經超過 ASCII 的範圍了。
```cpp
if (str[i] & 0x80)
return false;
```
題目要我們在 64 位元的架構下,嘗試一次比對 64 位元的資料 (即 word size),從上面的例子可以推斷出每個 byte 都要與 0x80 做比較,所以在第 4 行填入 0x8080808080808080
```cpp=
while ((i + 8) <= size) {
uint64_t payload;
memcpy(&payload, str + i, 8);
if (payload & 0x8080808080808080)
return false;
i += 8;
}
```
### 延伸問題
#### 1. 解釋上述程式的運作原理
題目敘述說一次要比對 64 bits
```cpp
while ((i + 8) <= size)
```
藉由 `i+8` 去移動,會一次比對 8 bytes,如果 if 為 true 則代表有找到不符合的,就會直接 return false
```cpp
while (i < size)
```
如果剩餘的無法一次比對 64 bits,那麼就會回歸為比較一個 byte
- 參考[記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory#data-alignment)
`memcpy` 的用途為 data alignment,e.g. 至少抓 1 byte,如果不對齊資料,會導致 cpu 的存取因 address 不是在 4 的倍數上而變慢。而現代 x86 處理器允許 data unalignment
#### 2.給予一個已知長度的字串,檢測裡頭是否包含有效的英文大小寫字母
TODO
## 測驗 2
這題的 function 為一個 parser,給定的條件為
```cpp
hexchar2val(a~f) return 10~15
hexchar2val(A~F) return 10~15
hexchar2val(0~9) return 0~9
```
當我們從二進位的時候可以簡單地看出答案,所以先用二進位表示
| Example | 0</br>MSB | 1 | 2 | 3 | 4 | 5 | 6 | 7</br>LSB |
| :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| <span style="color:green">**F**</span> | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 |
| <span style="color:green">**f**</span> | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 |
| <span style="color:green">**8**</span> | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 |
```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. 觀察以上程式碼,會發現 `shift` 是遇到英文字母的時候才會使用到的,因為第 5 行做 `(in + shift) & 0xf` 後只會保留右邊的 4 個 bits,而 0~9 在一開始的右邊 4 個 bits 就已經是答案了,所以沒有必要多做操作
2. 這樣就可以知道 `MASK` 是要分辨出==字母==與==數字==,從 column 1 就可以分辨出兩者,所以 `MASK` 為 0x40
3. 如果是字母,做完第 3 行後會得到 0x40 ( 0100 0000 ),因為最後是 `(in + shift)` ,推敲出字母都與所需的數字差 9, e.g. f 為 0110,但是答案需要輸出 15。這樣我們只要把 `shift` 變成 9 就好了
4. 因為 9 等於 1001,所以 `letter >> 3 | (letter >> 6)` 就是我們要的答案,下面是完整的程式碼
```cpp
uint8_t hexchar2val(uint8_t in)
{
const uint8_t letter = in & 0x40;
const uint8_t shift = (letter >> 3) | (letter >> 6);
return (in + shift) & 0xf;
}
```
### 延伸問題:
很直接的先把 input 改為 pointer 來處理多個 char,再修改下面的第 14 行,把每個數值轉換過後再乘上對應的 $16^n$,這邊用 shift 去代替乘法。
```cpp=
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
uint64_t hexchar2val(uint8_t* in)
{
int len = strlen(in);
uint32_t ans = 0;
for (int i = 0; i < len; i++)
{
const uint8_t letter = in[i] & 0x40;
const uint8_t shift = (letter >> 3) | (letter >> 6);
ans |= ((in[i] + shift) & 0xf) << (4 * (len - i - 1));
}
return ans;
}
int main(){
u_int8_t* c1 = "0FF1CE";
u_int8_t* c2 = "FEE1DEAD";
u_int8_t* c3 = "FFBADD11";
u_int32_t a = hexchar2val(c3);
printf("%u\n", a);
return 0;
}
```
## 測驗 3
- 將除法改為乘法運算
為了將除法轉換為
1. 預先計算的 $\frac{2^N}{d}$
2. 乘法
3. shift
使用
$\frac{n}{D} = n * \frac{\frac{2^N}{D}}{2^N}$
```cpp=
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;
}
```
第 2 行的 M,就是為了預先計算 $\frac{2^N}{D}$,這時我們不知道應該要填多少。所以繼續往下,看到第 6 行,這邊就是要算 $n * \frac{\frac{2^N}{D}}{2^N}$,我們把他轉換為 $M * n / 2^{64}$,這時候就很容易看出 $2^N$ 即為 $2^{64}$,那麼為什麼是選擇 0xFFFFFFFFFFFFFFFF 呢,因為已經 overflow 了,所以索性 -1,再從後面的 +1 做無條件進位補回來,第 6 行的 shift 是無條件捨去,也能補回誤差。這樣我們就得到 `quotient` 了,於是就能在第 7 行得到餘數。
### 延伸問題:
TODO
## 測驗 4
```cpp
bool divisible(uint32_t n)
{
return n * M <= M - 1;
}
```
因為沿用 D 跟 M,D = 3, M = 0x55...56,所以
左邊會是 n * (0x5555555555555555 + 1)
右邊會是 0x5555555555555555
我把 n 轉換為 3k、3k+1、3k+2 這三種情況
1. 3k * (0x5555555555555555 + 1) = (0xff...ff + 3) * k = (0 + 2) * k = 2k
2. (3k + 1) * (0x5555555555555555 + 1) = 2k + 0x55...56 = 2k + M
3. (3k + 2) * (0x5555555555555555 + 1) = 2k + 0x55...56 * 2 = 2k + 2M
由於 n 是 unsigned 32 bits,所以 n >= M 不可能發生,只有 2k 會 <= M - 1,可以得知 n 在拆成 3k 的時候才能符合判斷式 n * M <= M - 1,故可判斷是否為 3 的倍數
## 測驗 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(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` 的用處是把最小的兩個 bytes 補滿 64 bits,
e.g. PACKED_BYTE(0x80) 會得到 0x8080808080808080
- 因為單純看程式碼實在推敲不出答案,所以我直接執行程式,來推敲原因
`PACKED_BYTE(128 - 'A' + 0)` = 0x3f...3f
`PACKED_BYTE(128 - 'Z' + (-1))` = 0x25...25
```cpp
printf("%ld, %lx, %lx, %lx\n", i, A, Z, mask);
```
input "This eBo"
output: 0, ae81a45fb2a8a793, 94678a45988e8d79, 20000000000020
- 藍色是程式第 14~15 行的 `A` 跟 `Z`,綠色是 ASCII 內的字母
| | 0</br>MSB | 1 | 2 | 3 | 4 | 5 | 6 | 7</br>LSB |
| :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| <span style="color:blue">**A**</span> | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
| <span style="color:blue">**Z**</span> | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 1 |
| <span style="color:green">**a**</span> | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 |
| <span style="color:green">**z**</span> | 0 | 1 | 1 | 1 | 1 | 0 | 1 | 0 |
| <span style="color:green">**A**</span> | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
| <span style="color:green">**Z**</span> | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 0 |
1. 可以看出大寫的時候會出現 2,從上面的表看出 0x20 在 ASCII 上剛好可以把大寫轉換為小寫,所以這個 `mask` 的用處很明顯就是要找出需要轉換為小寫的 char,並且在第 17 行 `*chunk ^= mask` 把他轉換為小寫,而小寫字母就保留原樣。 (與 1 做 xor 是 toggle)
2. 而會是 2 的原因是因為往右 shift 2,所以原本是 8,而這個 8 很明顯是從第 16 行 `((A ^ Z) & PACKED_BYTE(0x80))` 來的,從第 1 點我們知道大寫的時候應該要產生 0x80,小寫會是 0x00,所以接下來要推斷 `A^Z` 如何在 MSB 產生 1 或 0。
3. 大寫字母需要在 `A^Z` 的 MSB 產生 1,所以需要各一個 0 跟 1,而小寫字母則是需要兩個 0 或 1,程式內的 `(128 - 'A' + 0)` 跟 `(128 - 'Z' + (-1))` 相減得到 26,剛好可以容納下所有英文字母,嘗試 0x25 與 A~Z 相加可以發現最大是 0x7F 剛好會保持 MSB 為 0,而從 0x3F 加的話,就會從 0X80 開始,MSB 肯定是 1,於是 `(A ^ Z)` 過後的 MSB 就會得到 1。
4. 如果是小寫字母,無論如何我們都會得到大於 0x80 的數字,所以 `A` 跟 `Z` 的 MSB 都會是 1,`(A ^ Z)` 過後的 MSB 就會得到 0,在 `mask` 的地方就會產生 0。
6. 這樣就釐清為什麼程式會是這樣設計了,`VV1` 選 0x80 是為了驗證是否屬於 ASCII,`VV2` 選 0 是為了讓大寫的字母相加後的 MSB 一定是 1,`VV3` 選 -1 是因為需要多 -1 才能夠容納 26 個英文字母 (原本只相差 25),`VV4` 選 0x80 是為了讓 `mask` 只保留需要轉換為小寫的 MSB,並將他 shift 到 0x20 的位置。
### 延伸問題
`char str[]` 就如同一般的 array 一樣,可以對他做任何的操作。但 `char *str` 是 string literal,屬於 read-only part of memory,任何想要對他們的修改都會變成 undefined behavior。由於題目的程式是 in-place 的演算法,所以不能夠用 `char*`
## 測驗 6
這題為 LeetCode 137. [Single Number II](https://leetcode.com/problems/single-number-ii/),要找出只出現過一次的 value,而其他的 value 都會出現 3 次
```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;
}
```
- 在解這題前我想要先解釋如果其他 value 是出現 2 次的情況
由於 xor 的特性可以簡單的寫出下面的 code,當相同的 bit 出現兩次時,就會被清空成 0,最後就只會留下出現一次的 value 的 bits
```cpp
int singleNumber_even(int* nums, int numsSize){
int a = 0;
for(int i=0; i<numsSize; ++i)
a ^= nums[i];
return a;
}
```
- 回到這題,由於其他 value 會出現三次,所以使用兩個變數去紀錄,第一次出現時會存到 `lower` 裡,第二次會把他放到 `higher`,第三次就會把他從 `higher` 移除,這樣全部做完時就會在 `lower` 得到想要的 number,看下面的 code:
1. 第 5 行 會將 num 放進去 `lower` 裡面,但是如果 num 已經在裡面,就會把他移除,代表他已經出現第二次
2. 第 6 行 我們只會將第一次出現的 num 放進去 `lower`,所以如果在 `higher` 裡就會被移除,這樣可以檢測他是否為第三次出現
3. 第 7~8 行 `higher` 紀錄的是第二次出現的 num,所以當這個 num 是第一次出現時,會因為 num 在 `lower` 裡面也有,而藉由第 8 行把 `higher` 內的 num 清除。第二次遇到時就會直接放入 `higher`,第三次遇到時就會因為已經存在 `higher` 內而藉由第 7 行清除掉,換句話說,`higher` 最後會是 0
```cpp=
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;
}
```
### 延伸問題
其實就是一直增加變數去紀錄
- 將程式推廣成 3、4、5.... 次
1. 如果是偶數次的話,直接使用上面的如果是 2 次的情況就好,因為 xor 的特性每兩次就會清空一次,所以只要是偶數次都可以使用
2. 奇數次就需要增加變數去紀錄 //todo