# 2020 sysprog Homework2 (quiz2)
###### tags: `sysprog`
contributed by < `sciyen` >
[TOC]
## Q1 is_ascii()
題目:
```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)
return false;
i += 8;
}
while (i < size) {
if (str[i] & 0x80)
return false;
i++;
}
return true;
}
```
已知 extended ASCII 的第8個 bit 為1,若以單一個 byte 來看,用以下方式可以判斷第8個 bit 是否為1
```cpp=
int b = byte & 0x80;
```
:::info
For example:
0b11000111
0b10000000 -> 0xf0
0b10000000
:::
利用相同方式,若有 8 byte,則 Mask 以一個 byte 為單位,用 8組。
```cpp=
0x8080808080808080
```
:::warning
**Memory alignment problem**
由於傳入的參數 `str` 可能是記憶體中的任意位置(不一定會 alignment ),因此若直接對該記憶體操作有可能因為 unalignment 必須讀兩個記憶體區段再合併導致效能變差,因此利用 `memcpy()` 強制將記憶體 alignment 後再進行後續操作。
:::
#### 延伸問題 - 給予一個已知長度的字串,檢測裡頭是否包含有效的英文大小寫字母
## Q2 hexchar2val()
題目:將十六進位表示的字串 (hexadecimal string) 轉為數值
```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;
}
```
:::info
注意 ASCII 中數字與英文字的表示方式
- 數字 0x30~0x39, 前4個 byte 皆為 0b0011
- 小寫英文 0x61~0x66, 前4個 byte 為0b0110
- 大寫英文 0x41~0x46, 前4個 byte 為0b0100
:::
1. 為了判斷是否為英文,利用第一個 MASK 判斷前 4 byte 中的 4 位置是否為1, 因此 MASK 為0x40。因此 `letter` 的意義為 `in` 是 `>=10` 的值。
2. 接下來,若 `in` 是 `>=10` 的值,則必須要創造出一個 `10` 加進去答案裡面,又若 `in>=10` , `letter` = `0b01000000`,因此將 `letter` 右移3格得到 `0b00001000`,右移6格得到 `0b00000010`,做 or 運算後得到 `0b00001001` 也就是9。
:::warning
| |`0b01000000`|
|-|------------|
|3|`0b00001000`|
|6|`0b00000001`|
|OR|`0b00001001`|
:::
4. 最後將 `in` 與 `shift` 相加,若 `in` 為數字,則 `shift` 為0;若 `in` 為 A~Z 或 a~z ,則 `shift` 為9,由於 A~Z 及 a~z 的後4個 byte 皆對應到 A=1, B=2,...,因此 A 為9+1=10,以此類推...
5. 最後,與0x0f做 `AND 運算`將剩餘的前 4 byte 濾掉。
## Q3 quickmod(uint32_t n)
題目:該算法將除法變形為 乘法 與 shift 的組合。
```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;
}
```
#### 問題解析
由原式中
\begin{equation}
\frac{n}{d}=n \times \frac{\frac{2^N}{d}}{2^N}
\end{equation}
可以發現 `M` 應為式中 $\frac{2^N}{d}$ 的部分,而 `N` 為64,但由於 $2^{64}$ 恰好進一位,導致記憶體位置不夠,因此用 `0xFFFFFFFFFFFFFFFF` 代替,最後再加1。
#### 程式碼說明
1. 對於 `#define` 代表利用預處理器先行計算 `((uint64_t)(UINT64_C(0xFFFFFFFFFFFFFFFF) / (D) + 1))` 並視為 constant 處理,因此可以大幅減少執行時期的計算。
2. 對於式中的 $\frac{1}{2^{64}}$ ,由於其為2的倍數,可以用 shift 快速處理。
:::info
**Proof**
$\frac{2^N}{d}$ 於程式中寫為 $\frac{2^N-1}{d}+1$
假設:商數為 `q` ,餘數為 `r` 且 $q, r \in \mathbb{N}$
依據情況可以分為下列兩種:
- $2^N$ 整除 `d`:
\begin{equation}
\left\{
\begin{array}{lr}
2^N=q\times d \\
2^N-1=(q-1)d+(d-1), \ if \ d>1 \ then \ d-1>0
\end{array}
\right.
\end{equation}
因此 `q` 必定比原式少1
- $2^N$ 不整除 `d`:
\begin{equation}
\left\{
\begin{array}{lr}
2^N=q\times d +r, \ where \ 1<r<d \\
2^N-1=(q-1)d+(r-1)
\end{array}
\right. \\
\because 1<r<d \\
\therefore 0<r-1<d-1
\end{equation}
`d-1`必定無法進位,因此 `q` 必定比原式少1
**結論:**
不管$\frac{2^N-1}{d}$中的 `N` 及 `d` 為多少,所得的 `q` 必定較 $\frac{2^N}{d}$ 少1,因此$\frac{2^N}{d}$ 於程式中寫為 $\frac{2^N-1}{d}+1$。
:::
#### 延伸問題
## Q4 divisible(uint32_t n)
## Q5 strlower
### 解析 `void strlower(char *s, size_t n)` 程式碼原理
```c=
#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]);
}
}
```
觀察 ASCII code 可以發現,其大寫字母與小寫字母差在第 5 個 bit ,例如:
A: 0x41 0100 0001
B: 0x61 0110 0001
因此若要將大小寫互換,只要將第 5 個 bit 作 not 運算即可。
### 利用 64 bit 架構做運算
一次對 8 個 byte 做處理以加速計算:
```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);
}
```
- 12行:由於 char array 為連續的記憶體,透過 uint64_t 指標的方式取值可以獲得連續 8 byte 的記憶體。
- 5行:由於要做 64 bit 乘法運算,因此將 `b` 強制轉為 uint64_t ,同時,為確保只有第一個 byte
## Q6 LeetCode [137. Single Number II](https://leetcode.com/problems/single-number-ii/):
```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;
}
```
### 首先看 `XOR` 的特性:
XOR 重複做兩次後會變回原本的數字
```
5: 00000101
5: 00000101
------------
A: 11111111
5: 00000101
------------
A: 00000101
```
如果從 0 開始,對 0 做 2 次一樣的 `XOR` 後會變回 0 。
**Note**:可以觀察到,對任意數做兩次一樣的 `XOR` 後都會變回原本的數,其原因也很明確,因為相同的數做 `XOR` 後必定全為 1 ,然後任意數對 1 做 `XOR` 後,其值不變。
```
0: 00000000
5: 00000101
------------
A: 11111010
5: 00000101
------------
A: 00000000
```
```
5: 00000101
3: 00000011
------------
A: 11111001
5: 00000101
------------
A: 00000011
5: 00000101
------------
A: 11111001
5: 00000101
------------
A: 00000001
```