# 2020q3 Homework2 (quiz2)
contributed by < `zzzxxx00019` >
## 測驗 1
### 題目:
目前電腦中用得最廣泛的字元集及其編碼,是美國國家標準局 (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),程式碼改寫如下:
```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 & MMM)
return false;
i += 8;
}
while (i < size) {
if (str[i] & 0x80)
return false;
i++;
}
return true;
}
```
請補完程式碼。
### 原理:
透過 `uint64_t payload` 一次擷取8個陣列字元進行比對,而ASCII是透過7位元進行編碼的編碼技術,且 `char` 一般被配置大小為8bit,因此一個透過ASCII編碼的字元與 `0x80` 進行and運算需得到的理論結果為 `0x00` ,從下表 `binary` 的部分可以很清楚觀察到 `0x7F & 0x80` 後的運算結果;因此如果想對存有8個字元的字串直接進行比較,則需將8個字元都對 `0x80` 做 `&` 運算。
| | Binary | Hexadecimal |
| --- | --- | --- |
| 7bit | 0111 1111 | 0x7F |
| 8bit | 1000 0000 | 0x80 |
### 延伸問題:
* 解釋上述程式的運作原理,特別是為何用到 memcpy 呢?提示: 與 data alignment 有關。
:::info
透過memcpy,會先檢查 memory 是否為 data alignment ,如果不是的話會先針對 memory 做對齊的動作,即先 copy 幾個 byte 使 memory 狀態為 data alignment ,再以 word 為單位進行複製。
:::
* 將上述技巧應用於「給予一個已知長度的字串,檢測裡頭是否包含有效的英文大小寫字母」
* 參考`測驗 5` 的 mask 方法,透過將字元 `+128-'A'` 與 `+127-'Z'` 後,兩者做 `xor` 計算後再對 `0x80` 做 `and` 計算,如果存在著 A~Z 的字元,計算後的結果會為 `0x80` 。
* 最後因剩餘的字串長度不足 8 bytes ,因此透過逐一檢查的方式進行。
```cpp=
#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);
uint64_t A = payload + PACKED_BYTE(128 - 'A') ;
uint64_t Z = payload + PACKED_BYTE(128 - 'Z' - 1) ;
uint64_t a = payload + PACKED_BYTE(128 - 'a') ;
uint64_t z = payload + PACKED_BYTE(128 - 'z' - 1) ;
if( (A^Z) & PACKED_BYTE(0x80) || (a^z) & PACKED_BYTE(0x80))
return true ;
i += 8;
}
while (i < size) {
char tmp_c = str[i] ;
if(tmp_c > 'Z') tmp_c^=' ';
if(tmp_c >= 'A' && tmp_c <= 'Z')
return true;
i++;
}
return false;
}
```
## 測驗 2
### 題目:
```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;
}
```
已知 in 一定隸屬於 '0', '1', '2', …, '9' 及 'a', 'b', 'c', …, 'f' (小寫) 及 'A', 'B', 'C', …, 'F' (大寫) 這些字元。預期 hexchar2val('F') 和 hexchar2val('f') 回傳 15,且 hexchar2val('0') 回傳 0,其他輸入都有對應的數值。
以下摘自 ASCII 表格
* '0', '1', '2', …, '9' 對應到 0x30, 0x31, 0x32, … 0x39
* 'a', 'b', 'c', …, 'f' (小寫) 對應到 0x61, 0x62, 0x63, …, 0x66
* 'A', 'B', 'C', …, 'F' 對應到 0x41, 0x42, 0x43, …, 0x46
請補完程式碼。
### 原理:
首先先觀察程式碼最後的部分為 `return (in + shift) & 0xf` ,意思是前面的運算主要是為了達到 `(in + shift)` 範圍為 `0x0f ~ 0xff` 的目的。透過將 `f(0x66)` 與 `F(0x66)` 轉換為二進制,可以更加直覺觀察。
| | Binary |
| --- | --- |
| f | 0110 0110 |
| F | 0100 0110 |
為了將不必要的字元過濾掉,透過上表可以發現將 MASK 設為 `0x40` 是留下 `'f'` 與 `'F'` 共通點的最佳數值。而這兩字元經過與 MASK 進行 `&` 運算後,可以得到` 0x40` 這個結果。
而為了符合最後 `return` 的結果,最後 shift 結果需為 `'xxxx 1001'` ,因此可以很直觀的推測出 ==0x40 >> 3 | 0x40 >> 6== 輸出結果會符合預期目標。
### 延伸問題:
* 將 hexchar2val 擴充為允許 "0xFF", "0xCAFEBABE", "0x8BADF00D" 等字串輸入,且輸出對應的十進位數值。
* 透過修改原本的方法,將字串逐一比對並將結果加入 `uint_64 result` 此變數
* 因每個字元算出來的結果為 `4 bit` 的值,因此當有新的值要加入 result 時,對 result 當前數值左移 4 位。
```cpp=
uint64_t hexchar2val(char in[])
{
int i=0, in_len = strlen(in) ;
uint64_t result=0 ;
if(in_len>2 && in[0]=='0' && in[1]=='x') i = 2 ;
for( ; i < in_len ; i++)
{
const uint8_t letter = in[i] & 0x40;
const uint8_t shift = (letter >> 3) | (letter >> 6);
result = (result << 4) | ((in[i] + shift) & 0xf);
}
return result ;
}
```
## 測驗 3
### 題目:
* 快速除法運算
```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 位元的無號數值
預期 quickmod(5) 會得到 2, quickmod(55) 得到 1, quickmod(555) 得到 0 (555 是 3 的倍數)。
請補完程式碼。
### 原理:
根據快速除法的思維,將透過等式 $\dfrac{n}{d}=n\times\dfrac{\dfrac{2^n}{d}}{2^n}$ 進行運算。
* `#define M ((uint64_t)(UINT64_C(XXX) / (D) + 1))` 部分以數學式來表達可寫為: $M=\dfrac{x}{d}+1$
* `uint64_t quotient = ((__uint128_t) M * n) >> 64` 這段程式碼中 `>>64` 即為除 $2^{64}$;而將整段解析以數學方式表達則為 $n\times\dfrac{M}{2^{64}}$ ,因此可推得 M 為接近 $2^{64}$ 的值 `0xFFFFFFFFFFFFFFFF`。
* 因預設處理器為 64 位元的電腦,$M$ 如果代入 $2^{64}$ 會導致溢位問題,因此將代入 $M=2^{64}-1$
:::info
證明 $\dfrac{n}{d}=((\dfrac{2^k-1}{d}+1)\times\dfrac{n}{2^k})$
$((\dfrac{2^k-1}{d}+1)\times\dfrac{n}{2^k})=(\dfrac{2^k-1+d}{d})\times\dfrac{n}{2^k}=(\dfrac{2^k}{d}-\dfrac{1}{d}+1)\times\dfrac{n}{2^k}=\dfrac{n}{d}+\dfrac{n(d-1)}{2^k\times d}$
等式結果必須符合 $\dfrac{n(d-1)}{2^k\times d}=0$,根據計算機無條件捨去特性,必須符合 $\dfrac{n(d-1)}{2^k\times d}<1$
稍微整理一下可得到不等式 $n(1-\dfrac{1}{d})<2^k$ ,因為 $n$ 為 `uint32_t` 的資料型態,因此 $n<2^{32}$
又因為 $d$ 為正整數,因此 $(1-\dfrac{1}{d})<1$ ,且 $k=64$ ,故不等式 $n(1-\dfrac{1}{d})<2^k$ 結果成立
得證 $\dfrac{n}{d}=((\dfrac{2^k-1}{d}+1)\times\dfrac{n}{2^k})$
:::
### 延伸問題:
由 Facebook 公司所維護的 [jemalloc](https://github.com/jemalloc/jemalloc) 是個高效能的記憶體配置器 (memory allocator),特別在多核多執行緒的環境有優異表現,在其原始程式碼 [include/jemalloc/internal/div.h](https://github.com/jemalloc/jemalloc/blob/dev/include/jemalloc/internal/div.h) 和 [src/div.c](https://github.com/jemalloc/jemalloc/blob/dev/src/div.c) 也運用類似的技巧,請閱讀原始程式碼,並抽離快速除法實作為獨立的程式碼,說明運作原理,也該撰寫測試程式,比較透過處理器的整數除法指令及本技巧的效能差距
:::info
$\dfrac{2^k}{d}\times\dfrac{n}{2^k}=\dfrac{2^k+r}{d}\times\dfrac{n}{2^k}$ , $r=-2^k mod$ $d$
$\dfrac{2^k}{d}\times\dfrac{n}{2^k}+\dfrac{r}{d}\times\dfrac{n}{2^k}=\dfrac{n}{d}+(\dfrac{r}{d}\times\dfrac{n}{2^k})$ ,因此只要證明 $\dfrac{r}{d}\times\dfrac{n}{2^k}<1$ ,就可以用上述方法得到 $\dfrac{n}{d}$ 的值。
已知 $r=-2^k mod$ $d$ 且 $k=32$ ,因此$r<d$
又因為 n 為 uint32_t 型態,最大值為 $2^{32}-1$,故 $n<2^{32}$
得證 $\dfrac{r}{d}\times\dfrac{n}{2^k}<1$
:::
## 測驗 4
### 題目:
```cpp
#define M ((uint64_t)(UINT64_C(0xFFFFFFFFFFFFFFFF) / (D) + 1))
bool divisible(uint32_t n)
{
return n * M <= YYY;
}
```
以 D = 3 來說,divisible(7) 要得到 0 (即 7 無法被 3 整除),divisible(87) 要得到 1 (即87是三的倍數)
請補完程式碼。
### 原理:
因為實在摸不清這段程式的邏輯,因此參考< `quantabase13` >的代數方法去解釋。
* 根據已知參數 $M=\dfrac{0xFF...F}{3}+1$ ,可先算出 $M=0x5555555555555556$
* 已知 $D=3$ ,因此可以令 $n=3k$、$3k+1$、$3k+2$
* $if(n=3k)$:$n\times{M}=3k\times{0x555...6}$,經過處理可得$k\times{0x55...5}+k+2k$,基於 64 位元處理器下限制下,$k\times{0x55...5}+k$ 會發生溢位操作而歸 0 ,因此結果為$2k$。
* $if(n=3k+1)$:如同上述計算可得結果為 $0x55..5+2k+1$,即 $2k+M$ 。
* $if(n=3k+2)$:如同上述計算可得結果為 $0xAA..A+2k+2$,即$2k+2M$。
* 在 $k$ 為非負整數的狀態下,只有 $M-1$ 符合要求結果。
## 測驗 5
### 題目:
在 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);
}
```
對應的測試程式碼如下:
```cpp
#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);
}
```
參考執行輸出:
>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
請補完程式碼。
### 原理:
* `PACKED_BYTE(b)` 用途是將一個 16-bit 的數值擴張為 64-bit ,且以 16-bit 為一個單位。如 `PACKED_BYTE(0x80)` 結果就會為 `0x8080808080808080` 。
* 因 ASCII Code 為 7-bit 編碼,因此最大值只會來到 `'0x7F'` ,對 `'0x80'` 進行 `&` 運算結果必定為 0 ,因此推測 `VV1=0x80` 。
* 根據課堂講義 [C 語言: 數值系統 - 運用 bit-wise operator](https://hackmd.io/@sysprog/c-numerics#%E9%81%8B%E7%94%A8-bit-wise-operator) 所提到內容「大小寫互轉: 避免使用分支」,推測 `*chunk ^= mask` 用意即是為了做這個動作。
```
('a' ^ ' ') // 得到 'A'
('A' ^ ' ') // 得到 'a'
```
* mask 的用意在於,將對應為大寫的部分以 `0x20(' ')` 填入,其他部分以 `0x00` 填入,如此大寫字元將會被降為小寫,而其他字元則維持原貌。
* 為了達到上述目的, `(A ^ Z) & PACKED_BYTE(VV4)` 所對應的大寫字元部分結果應為 `0x80` ,如此透過 `>>2` 的步驟,才能得到預期的結果 `0x20` ,因此知 `VV4=0x80` 。
* 為了得到 `0x20` 這個結果,對應的字元必須滿足 `(A ^ Z)` 的第 8 個 bit 為 1 ,即必須滿足以下兩式,根據代入 'A' 與 'Z' 計算,`VV2=0` 且` VV3=(-1)` 可以滿足需求。
```
input + 128 - 'A' + VV2 >= 128
input + 128 - 'Z' + VV3 < 128
```
### 延伸問題:
* 將前述測試程式 main 函式的 char str[] 更換為 char *str,會發生什麼事?
參考自 < `RinHizakura` > 所擷取的 C 語言規格書內容片段:
>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
:::info
* `str[]` 是在 memory 上,透過靜態配置產生 string literal ,去儲存字串內容,而 `*str` 則是一個指標型態,指向字串儲存的記憶體位置。因此兩者最明顯的差異在於 `str[]` 是一個陣列型態,依據需求去進行記憶體的配置,而 `*str` 則是一個指標型態,指向 array of char。
* 根據 C 語言規格書所記載,透過指標 "pointer to char" ,去對 string literal 進行修改,此動作是 undefined 的。
:::
因此產生錯誤訊息:
```
[1] 22494 segmentation fault (core dumped) ./a.o
```
## 測驗 6
### 題目:
>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
```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;
}
```
請補完程式碼。
### 原理:
* `XOR` 運算特性:
* `XOR` 在 `bit` 世界中,即是對相對應的 `bit` 做相加的動作,且不做進位的動作,根據此特性可以得到 `a ^ a = 0` 的特性。
* 在 `XOR` 運算式中,符合交換率的規則,即 `a ^ b ^ c = a ^ c ^ b` 。
* 綜合以上兩點,可以歸納出 `a ^ b ^ a = b` 的小結論,意即透過 `XOR` 運算的方法,可以求出出現 `1` 次的數值。
* 將觀念延伸到此題,透過 lower 、 higher 與初始化去紀錄每個數值出現的狀況,如果以兩個 bit 去記錄各種不同的狀態,簡單表示下即為 `(00 -> 01 -> 10 ->00)`:
|higher|lower|input|higher'|lower'|
|- | -| -| -|- |
|0 |0 |0 |0 |0 |
|0 |0 |1 |0 |1 |
|0 |1 |0 |0 |1 |
|0 |1 |1 |1 |0 |
|1 |0 |0 |1 |0 |
|1 |0 |1 |0 |0 |
根據邏輯設計課程所學,將增值表以邏輯運算表示:
$lower = higher' \cdot lower' \cdot input + higher' \cdot lower \cdot input'=higher' \cdot (lower' \cdot input + lower \cdot input') = higher' \cdot (lower\oplus input)$
此外根據程式撰寫流程, higher 在更新時,所採用的是已更新過的 lower ,得到邏輯運算式:
$higher = higher \cdot lower \cdot input + higher \cdot lower' \cdot input' = lower' \cdot (higher' \cdot input + higher \cdot input') = lower' \cdot (higher \oplus input)$
因此推斷 `KKK=~higher` 、 `JJJ=~lower` 。
### 延伸問題:
* 請撰寫不同上述的程式碼,並確保在 LeetCode 137. Single Number II 執行結果正確且不超時
* 透過原題的邏輯運算,可將程式碼縮短為更簡短且更直觀的寫法
* 在設計 `higher` 那部分,也可直接透過邏輯運算,不採用更動後的 `lower` ,只是這樣會使程式碼變得更加冗長,增加計算負擔
```cpp=
int singleNumber(int *nums, int numsSize)
{
int lower = 0, higher = 0 ;
for(int i = 0 ; i < numsSize ; i++)
{
lower = (~higher)&(lower^nums[i]) ;
higher = (~lower)&(higher^nums[i]) ;
}
return lower ;
}
```
![](https://i.imgur.com/gXcu0mz.png)
* 延伸題意,將重複 3 次改為改為重複 4 次或 5 次,並找出可容易推廣到多次的程式碼
* 設計題目為陣列中,所有字都會出現 4 次,只有一個字會出現 1 次
* 依照想法, low 必須保留給只出現一次的字,因此還需要額外兩個變數紀錄出現兩次與三次的狀況 (第四次會歸 0 ),故為 000 -> 001 -> 010 -> 100 ,因為最右邊的 bit 要保留給 low
|high 2|high 1|low|input|high2'|high1'|low'|
|-| -| -| -| -| -| -|
|0| 0| 0| 0| 0| 0| 0|
|0| 0| 0| 1| 0| 0| 1|
|0| 0| 1| 0| 0| 0| 1|
|0| 0| 1| 1| 0| 1| 0|
|0| 1| 0| 0| 0| 1| 0|
|0| 1| 0| 1| 1| 0| 0|
|1| 0| 0| 0| 1| 0| 0|
|1| 0| 0| 1| 0| 0| 0|
依照增值表可設計出以下的邏輯運算規則 (尚未化簡):
$outputlow = high2'\cdot high1'\cdot low'\cdot input + high2'\cdot high1'\cdot low \cdot input'$
$outputhigh1 = high2'\cdot high1' \cdot low \cdot input + high2'\cdot high1\cdot low'\cdot input'$
$outputhigh2 = high2'\cdot high1\cdot low'\cdot input + high2\cdot high1'\cdot low'\cdot input'$
而運算式中的 $high2$ 、 $high1$ 、 $low$ 皆為前面運算的結果,因此依照 C 依序執行的概念,需將上次的運算結果儲存在一個變數中,才不會導致結果錯誤。
```cpp=
#include <stdio.h>
#include <stdlib.h>
int singleNumber(int *nums, int numsSize)
{
int low = 0, high1 = 0, high2 = 0 ;
for(int i = 0 ; i < numsSize ; i++)
{
int h1 = high1, h2 = high2, lo = low ;
low = ( ~h2 & ~h1 & ~lo & nums[i] )|( ~h2 & ~h1 & lo & ~nums[i] ) ;
high1 = (~h2 & ~h1 & lo & nums[i] )|( ~h2 & h1 & ~lo & ~nums[i] ) ;
high2 = (~h2 & h1 & ~lo & nums[i] )|(h2 & ~h1 & ~lo & ~nums[i] ) ;
}
return low ;
}
int main()
{
int nums[] = {2, 2, 4, 2, 3, 2, 4, 4, 4} ;
printf("result = %d\n", singleNumber(nums, 9)) ;
return 0 ;
}
```
```
result = 3
```
依照此邏輯去設計可以推展出 4 次、 5 次、 6 次等演算法,只要記得將最右邊的 bit 留給 lower 使用即可,如 5 次可設計為 000 -> 001 -> 010 -> 100 -> 110 -> 000 以此類推。