# 2020q3 Homework2 (quiz2)
contributed by < `nelsonlai1` >
## 測驗 1
```c=
#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;
}
```
逐一字元比對範圍內是否全是有效 ASCII 字元的 code,由於 ASCII 範圍是 0 到 127,也就是七個 bits,所以若是 & 0x80(10000000) 不為 0 的話代表大於 127 ,也就不是有效的 ASCII 字元。
```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 & MMM)
return false;
i += 8;
}
while (i < size) {
if (str[i] & 0x80)
return false;
i++;
}
return true;
}
```
此題利用上述 code 的基礎,改為一次比對八個字元,增加效率,而最後不足八個字元的部分再使用逐一字元比對。
由於 payload 是將八個字元串接在一起,所以可以推斷出 `MMM = 0x8080808080808080`。
### 延伸問題
1. 為何用到 memcpy?
根據 <s>http://www.cplusplus.com/reference/cstring/memcpy/</s> 提到的特性,確保 payload 複製到的值正好為 64 個位元。
> The underlying type of the objects pointed to by both the source and destination
pointers are irrelevant for this function; The result is a binary copy of the data.
>
> The function does not check for any terminating null character in source - it always
copies exactly num bytes.
:::danger
:warning: 改用 [Linux online manpage](https://man7.org/linux/man-pages/index.html) 作為 C 標準函式庫的主要參考資訊。
另外,如果不用 memcpy,而是直接透過另一個指標來指向特定的地址,可以嗎?這才是該思考的議題。
:notes: jserv
:::
> [name=nelsonlai1]
>
> 後來查看 memcpy 在 [glibc](https://github.com/lattera/glibc/blob/master/string/memcpy.c) 中的原始碼發現,當要複製的 byte 數大於一定值
> (len >= OP_T_THRES) 時,會先複製幾個 bytes 來達成 alignment。
> (BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);),增加效率。
2. 將上述技巧應用於「給予一個已知長度的字串,檢測裡頭是否包含有效的英文大小寫字母」
可以利用測驗 5 的技巧來寫這題
```c=
#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);
payload |= PACKED_BYTE(0x20);
if ((payload & PACKED_BYTE(0x80)) == 0) {
uint64_t a = payload + PACKED_BYTE(128 - 'a');
uint64_t z = payload + PACKED_BYTE(128 - 'z' - 1);
if ((a ^ z) & PACKED_BYTE(0x80) ^ PACKED_BYTE(0x80))
return false;
}
i += 8;
}
while (i < size) {
if ((str[i] | 0x20) < 97 || (str[i] | 0x20) > 122)
return false;
i++;
}
return true;
}
```
前半段一次比對八個字元時,先將複製進來的字串 `|= PACKED_BYTE(0x20)` 這樣如果有大寫英文就會變小寫,再用測驗 5 的技巧找出 'a'~'z'。
後面逐字比對的部分也是 `| 0x20` 後判斷是不是落在小寫字母的範圍內。
## 測驗 2
```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;
}
```
這題可以從最後一行開始看起,由於是 &0xf,可以先看後四個 bits。如此一來,0~9 需要的
(in + shift) 等於 in,而 A~F (a~f) 需要的 (in+shift) 則分別是 1010, 1011, 1100, 1101, 1110, 1111。可以推斷出 in 是數字時,shift 是 0000,而 in 是字母時,shift 是1001。
這時可以知道,letter 在 in 是字母時才會有值,所以能選的 MASK 只有 0x40。而得到的 letter 為 01000000,然後就可以算出 AAA 和 BBB 分別是 3 跟 6。
### 延伸問題
1. 解釋上述程式的運作原理
一開始的 MASK 用來分辨數字和字母,並且忽視大小寫。而 shift 用來讓 in 是字母時回傳對應的值。之所以能這麼寫,也是得利於 ASCII 的編排方式。
2. 將 hexchar2val 擴充為允許 "0xFF", "0xCAFEBABE", "0x8BADF00D" 等字串輸入,且輸出對應的十進位數值
```c=
uint64_t hexchar2val(char *in)
{
uint64_t result = 0;
for (int i = 0; i < strlen(in); i++) {
uint64_t letter = in[i] & 0x40;
uint64_t shift = (letter >> 3) | (letter >> 6);
result = result << 4 | ((in[i] + shift) & 0xf);
}
return result;
}
```
沿用此題概念,從第一個字元開始做,每次都會把之前的結果左移四次 (* 16) 後,or 這次的結果 (加上這次結果)。
## 測驗 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;
}
```
其中第六行在做的是題目敘述中提到的 $n \times \dfrac{\dfrac{2 ^ N}{d}}{2 ^ N}$,所以我們可以得知 N = 64,而 M = $\dfrac{2 ^ N}{d}$。
然而其實題目中的 M 不完全是這樣,但選一個最相近的解就是`XXX = 0xFFFFFFFFFFFFFFFF`,所以這裡的 M 其實是 $\dfrac{2 ^ {64} - 1}{d} + 1$。這樣又會衍伸一個問題,兩者出來的結果會相同嗎?
一樣從第六行來驗證結果,$\dfrac{(\dfrac{2 ^ {64} - 1}{d} + 1) \times n}{2 ^ {64}} = \dfrac{\dfrac{n \times 2 ^ {64} - n}{d} + n}{2 ^ {64}} = \dfrac{n \times 2 ^ {64} - n}{d \times 2 ^ {64}} + \dfrac{n}{2 ^ {64}}$
$= \dfrac{n}{d} - \dfrac{n}{d \times 2 ^ {64}} + \dfrac{n}{2 ^ {64}} = \dfrac{n}{d} + (\dfrac{d \times n - n}{d \times 2 ^ {64}}) = \dfrac{n}{d} + (\dfrac{(d - 1) \times n}{d \times 2 ^ {64}}) < \dfrac{n}{d} + \dfrac{n}{2^{64}}$。
而因為 n, d 的上限是 $2^{32} - 1$,所以 $\dfrac{n}{2^{64}} < \dfrac{1}{2^{32}}$,而 $\dfrac{n}{d}$ 的小數點部分最高是 $\dfrac{2^{32}-2}{2^{32}-1}$,即使加上此時的 $\dfrac{n}{2^{64}}$ 也不會進位,因此可以確定 M 不管是 $\dfrac{2 ^ {64}}{d}$ 或 $\dfrac{2 ^ {64} - 1}{d} + 1$,出來的結果都是一樣的。
---
後來想到算 M 時就應該考慮取整數問題了,所以這裡先分兩種情況探討 :
1. $\dfrac{2^{64}}{d}$ 是整數(這裡假設是 K ),這樣求出的 M 會是 $\dfrac{2 ^ {64} - 1}{d} + 1 = (K - 1) + 1 = K$
2. $\dfrac{2^{64}}{d}$ 不是整數(假設是 K.x ),這樣求出的 M 變成 K + 1,比原本結果 K 大了 1
第一種情況不影響 quotient 結果, 而第二種情況會讓 quotient 變成 $\dfrac{n}{d} + \dfrac{n}{2^{64}}$,但根據我上述的推論,最後出來的結果依然是一樣的。
### 延伸問題
1. 解釋上述程式的原理
這題其實就是把敘述中的概念應用出來而已,其中 quotient 是 $\dfrac{n}{D}$ 的商,所以回傳的餘數為
n (被除數) - quotient (商) * D (除數)。
2. 由 Facebook 公司所維護的 jemalloc 是個高效能的記憶體配置器 (memory allocator,即 malloc 和 free 函式對應的實作),特別在多核多執行緒的環境有優異表現,在其原始程式碼 include/jemalloc/internal/div.h 和 src/div.c 也運用類似的技巧,請閱讀原始程式碼,並抽離快速除法實作為獨立的程式碼,說明運作原理,也該撰寫測試程式,比較透過處理器的整數除法指令及本技巧的效能差距;
此程式原理跟這題類似,先假設一組 n, d,且 d 可以整除 n。
$\left \lfloor \left \lceil \dfrac{2^k}{d} \right \rceil \times \dfrac{n}{2^k} \right \rfloor = \left \lfloor \dfrac{2^k + r}{d} \times \dfrac{n}{2^k} \right \rfloor , r = d - 2^k mod \space d$
$= \left \lfloor \dfrac{2^k}{d} \times \dfrac{n}{2^k} + \dfrac{r}{d} \times \dfrac{n}{2^k} \right \rfloor = \left \lfloor \dfrac{n}{d} + \dfrac{r}{d} \times \dfrac{n}{2^k}\right \rfloor$
$= \dfrac{n}{d} + \left \lfloor \dfrac{r}{d} \times \dfrac{n}{2^k}\right \rfloor$ (因為 d 整除 n)
接著只要確保 $\dfrac{r}{d} \times \dfrac{n}{2^k} < 1$ 就能用一開始的算式來求 $\dfrac{n}{d}$
首先 $r < d$ 所以 $\dfrac{r}{d} < 1$,而把 k 設為 32 的話,$\dfrac{n}{2^k} < 1$,因為 $n <= 2^{32} - 1$
```c=
#ifndef JEMALLOC_INTERNAL_DIV_H
#define JEMALLOC_INTERNAL_DIV_H
#include "assert.h"
typedef struct div_info_s div_info_t;
struct div_info_s {
uint32_t magic;
};
void div_init(div_info_t *div_info, size_t divisor);
static inline size_t
div_compute(div_info_t *div_info, size_t n) {
assert(n <= (uint32_t)-1);
size_t i = ((uint64_t)n * (uint64_t)div_info->magic) >> 32;
return i;
}
#endif /* JEMALLOC_INTERNAL_DIV_H */
void div_init(div_info_t *div_info, size_t d) {
assert(d != 0);
assert(d != 1);
uint64_t two_to_k = ((uint64_t)1 << 32);
uint32_t magic = (uint32_t)(two_to_k / d);
if (two_to_k % d != 0) {
magic++;
}
div_info->magic = magic;
```
JEMALLOC_DEBUG 只是用來確保計算結果正確,但不影響程式邏輯,這裡就沒放入了。
div_init 部分先確定 d 不等於 0 或 1,確保不會讓除數為零或是 magic 超過上限 ($2^{32} - 1$)。
接著讓 two_to_k = $2^{32}$,magic = $\left \lfloor \dfrac{2^{32}}{d} \right \rfloor$,而因為要取的是 ceiling,所以若 d 不整除 $2^{32}$,magic 要再加一。
div_compute 的部分則是先確認 $n <= 2^{32} - 1$ (在 uint32_t 中,-1 代表 0xFFFFFFFF),然後計算 $\left \lfloor magic \times \dfrac{n}{2^{32}} \right \rfloor$ 得到最後的結果。
## 測驗 4
```c=
bool divisible(uint32_t n)
{
return n * M <= YYY;
}
```
這題的 D, M 沿用上一題,所以我們可以知道 D = 3, M = 0x5555555555555556。
而 n 可以分成 3k, 3k + 1, 3k + 2 (k 為非負整數)。
if n = 3k, n * M = 3k * (0x5555555555555556) = (0xFFFFFFFFFFFFFFFF) * k + 3k
= 2k (因為 uint64_t 超過 0xFFFFFFFFFFFFFFFF 會變回 0) (max = 0xAAAAAAAA)
if n = 3k + 1, n * M = 2k + M (min = M)
if n = 3k + 2, n * M = 2k + 2M (min = 2M)
如此一來, a, b ,e 都不是答案,而 c(M - 1), d(M >> 1) 都可以是答案。(雖然答案給 c)
## 測驗 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);
}
```
PACKED_BYTE(b) 取出 b 最後兩位數並將其變成八倍長度,也就是若 b 最後兩位是 87, PACKED_BYTE(b) = 0x8787878787878787。
可以看到它一次從 s 裡取出八個字元,如果其中有不為 ASCII 的字元(`if ((*chunk & PACKED_BYTE(VV1)) == 0) { /* is ASCII? */`),就會利用 strlower 來做逐一字元判斷以及轉換。而取不到八個字元時,也會用 strlower。
因為第 13 行在判斷是不是有效 ASCII 字元,根據測驗 1 的結果,VV1 就是 0x80。
14, 15 行的 A, Z 是用來找出大寫字母的。當任一英文字母 ('A'~'Z') + (128 - 'A' + VV2),都會 >= 128,也就是首個 bit 為 1。而當('A'~'Z') + (128 - 'Z' + VV3) 時會 < 128,首個 bit 為 0。
而從上面的式子可以看出,為了精準找出大寫字母,VV2 = 0 (如果為 1,雖不影響結果,但會把 '@' 一同算進來,為 -1,則會在'A'出錯),VV3 = -1 (如果為 0 或 1,會在 'Z' 或 'Y', 'Z' 出錯)。
16 行的 mask 利用技巧來得到 0x32(00100000),當某字元是大寫字母, A ^ Z 對應的位置會是 (1XXXXXXX),與 0x80(10000000) 做 and 變成 (10000000),再右移兩次就變 (00100000) 了。所以 VV4 = 0x80。
最後再用`*chunk ^= mask;`來對大寫字母做轉換。(詳見[你所不知道的 C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics))
```
('a' ^ ' ') // 得到 'A'
('A' ^ ' ') // 得到 'a'
```
### 延伸問題
1. 將前述測試程式 main 函式的 char str[] 更換為 char *str,會發生什麼事?請參閱 C 語言規格書,說明 string literal 的行為
```c=
#include <stdio.h>
#include <string.h>
int main()
{
/* quote from https://www.gutenberg.org/files/74/74-h/74-h.htm */
char str[] =
"This eBook is for the use of anyone anywhere at no cost and with \
almost no restrictions whatsoever. You may copy it, give it away or \
re-use it under the terms of the Project Gutenberg License included \
with this eBook or online at www.gutenberg.net";
int n = strlen(str);
strlower_vector(str, n);
puts(str);
}
```
會出現
```
Segmentation fault (core dumped)
```
根據 [ISO/IEC 9899:1999](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf) p.130 中一段
```
char s[] = "abc", t[3] = "abc";
defines ‘‘plain’’ char array objects s and t whose elements are initialized with character string literals.
This declaration is identical to
char s[] = { 'a', 'b', 'c', '\0' },
t[] = { 'a', 'b', 'c' };
The contents of the arrays are modifiable. On the other hand, the declaration
char *p = "abc";
defines p with type ‘‘pointer to char’’ and initializes it to point to an object with type ‘‘array of char’’
with length 4 whose elements are initialized with a character string literal. If an attempt is made to use p to
modify the contents of the array, the behavior is undefined.
```
中的這句
> If an attempt is made to use p to modify the contents of the array, the behavior is undefined.
在 `strlower` 這個 function 中就有對陣列內容做修改,也因此出現 undefined behavior。
## 測驗 6
```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;
}
```
這題的 nums 裡的元素除了一個以外,其餘都會重複三次。所以每個 bit 以三個狀態來表示,分別是00 (出現 0, 3 次),01 (出現 1 次),10 (出現 2 次)。
```graphviz
digraph {
rankdir = LR
00 -> 01 [label = "input = 1"]
00 -> 00 [label = "input = 0"]
01 -> 10 [label = "input = 1"]
01 -> 01 [label = "input = 0"]
10 -> 00 [label = "input = 1"]
10 -> 10 [label = "input = 0"]
}
```
畫成真值表如下 :
| higher | lower | input | higher_out | lower_out |
| ------ | ----- | ----- | ---------- | --------- |
| 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 |
$lower = higher' \times lower \times input' + higher' \times lower' \times input$
$= higher' \times (lower \times input' + lower' \times input)$
$= higher' \times (lower \oplus input)$
而因為 higher 運算時 lower 已經變成 lower_out 了,所以真值表為 :
| higher | lower_out | input | higher_out |
| ------ | --------- | ----- | ---------- |
| 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 0 |
| 1 | 0 | 0 | 1 |
| 0 | 1 | 1 | 0 |
| 0 | 0 | 1 | 1 |
| 1 | 0 | 1 | 0 |
$higher = higher \times lower' \times input' + higher' \times lower' \times input$
$= lower' \times (higher \times input' + higher' \times input)$
$= lower' \times (higher \oplus input)$
因此 KKK = ~higher, JJJ = ~lower。
### 延伸問題
1. 請撰寫不同上述的程式碼,並確保在 LeetCode 137. Single Number II 執行結果正確且不超時;
```c=
int singleNumber(int* nums, int numsSize){
int lower = 0, higher = 0;
for (int i = 0; i < numsSize; i++) {
int tmp = lower;
lower = ~higher & (lower ^ nums[i]);
higher = (higher & ~nums[i] & ~tmp) | (~higher & nums[i] & tmp);
}
return lower;
}
```
<s>跟原題目有 87% 像</s>
:::danger
你如何估算出 0.87 這數值呢?工程人員說話要精準。
:notes: jserv
:::
,只是把 lower 先存在 tmp 中,這樣 higher 在計算時就可以代入未變動的 lower 值。
![](https://i.imgur.com/0gP57Nh.png)
2. 延伸題意,將重複 3 次改為重複 4 次或 5 次,並找出可容易推廣到多次的程式碼;
重複四次以及其他偶數次都可以用以下程式碼,因為只有出現奇數次,偶數次兩種狀態。
```c=
int singleNumber(int* nums, int numsSize){
int lower = 0;
for(int i = 0; i < numsSize; i ++) {
lower ^= nums[i];
}
return lower;
}
```
重複五次則相較重複三次多了兩個狀態,但基本概念一樣。
```graphviz
digraph {
rankdir = LR
000 -> 000 [label = "input = 0"]
000 -> 001 [label = "input = 1"]
001 -> 001 [label = "input = 0"]
001 -> 010 [label = "input = 1"]
010 -> 010 [label = "input = 0"]
010 -> 011 [label = "input = 1"]
011 -> 011 [label = "input = 0"]
011 -> 100 [label = "input = 1"]
100 -> 100 [label = "input = 0"]
100 -> 000 [label = "input = 1"]
}
```
真值表 :
| left | middle | right | input | left_out | middle_out | right_out |
| ---- | ------ | ----- | ----- | -------- | ---------- | --------- |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | 1 | 0 | 0 | 0 | 1 |
| 0 | 1 | 0 | 0 | 0 | 1 | 0 |
| 0 | 1 | 1 | 0 | 0 | 1 | 1 |
| 1 | 0 | 0 | 0 | 1 | 0 | 0 |
| 0 | 0 | 0 | 1 | 0 | 0 | 1 |
| 0 | 0 | 1 | 1 | 0 | 1 | 0 |
| 0 | 1 | 0 | 1 | 0 | 1 | 1 |
| 0 | 1 | 1 | 1 | 1 | 0 | 0 |
| 1 | 0 | 0 | 1 | 0 | 0 | 0 |
$right = ... = left' \times (input \oplus right)$
由於我程式碼變動的順序為 right -> middle -> left,所以跟前面一樣,在計算 middle 時代入right_out,而計算 left 時代入 middle_out, right_out。
$middle = ... = left' \times (right' \times (middle \oplus input) + (middle \times right))$
$left = ... = middle' \times right' \times (left \oplus input)$
```c=
int singleNumber(int *nums, int numsSize)
{
int left = 0, middle = 0, right = 0;
for (int i = 0; i < numsSize; i++) {
right = ~left & (nums[i] ^ right);
middle = ~left & ((~right & (middle ^ nums[i])) | (middle & right));
left = ~middle & ~right & (left ^ nums[i]);
}
return right;
}
```
但利用這種方法的壞處是如果要重複更多奇數次的話,就要每次都算真值表以及邏輯表示式(應該是取最小質因數才對,也就是重複九次可以沿用重複三次的函式,但遇到質數就沒辦法了),好處是運作效率很高,只有一個迴圈而且都在做位元操作。
如果要簡單推廣到多次的方法可以參考 [RinHizakura](https://hackmd.io/@RinHizakura/SJ5NuIANP#%E4%B8%8D%E5%90%8C%E7%9A%84%E8%A7%A3%E9%A1%8C%E6%80%9D%E8%B7%AF) 同學的方式,計算每個位元 "1" 出現的次數並取餘數,最後只有出現一次 "1" 的位元被保留。
但因為上述方法需要對每個位元做運算,以及使用 % 運算,所以效率會稍微低一些,單看使用者如何取捨。