# 2020q3 Homework2 (quiz2)
contributed by < `StevenChen8759` >
###### tags: `linux2020`
> 相關連結:
> [2020秋季進階電腦系統理論與實作 quiz2](https://hackmd.io/@sysprog/2020-quiz2)
> [2020秋季進階電腦系統理論與實作 quiz2 作業說明](https://hackmd.io/@sysprog/B1zAbkAEP)
> [2020秋季進階電腦系統理論與實作 quiz2 作業繳交區](https://hackmd.io/@sysprog/2020-homework2)
> [color=green]
---
# :hammer_and_wrench: 測驗一
```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;
}
```
## :building_construction: 程式碼分析
* 函式透過第一個 `while` 迴圈,可一次比對字串中的八個字元是否為 `ASCII charcater` ,其原理是透過位元運算達成。
* 標準的 `ASCII charcater` 之範圍如下
```verilog
Dec: 8'd0 to 8'd127
Bin: 8'b00000000 to 8'b01111111
```
* 因此,我們可透過 `bitwise and` 位元運算過濾非標準的 `ASCII character`
* 不大於 $127$ 的 `char` 值輸出全零。
* 反之,則輸出非零。
* 因 $127$ 恰等於 $2^{(8 - 1)} - 1$,故大於 $127$ 的數,其 `最高位元 (MSB)` 之值必為 $1$ (以 `char` 型別而言)。
* 因此我們將任意 `char` 與 `0x80` 做 `bitwise and` 運算,再檢查其值是否非零,即可確認該字元是否為標準 `ASCII character`
* 上述的運作原理只針對一個 `char`,在此處我們將其擴充成一次針對 8 個 `char` 操作,因此 `uint64_t` 的 `bitwise and` 操作,常數部分即替換成 `0x8080_8080_8080_8080_8080_8080_8080_8080` 進行操作,並透過 `memcpy()` 呼叫將字串中的 8 個字元內容複製至區域變數內。
* 第二個 while 迴圈則是針對長度非恰為 8 的倍數之字串檢查,其執行次數為 `char length` mod $8$。
## :pencil: 改寫: 應用於檢知已知長度字串是否包含英文大小寫字母
* TODO
---
# :hammer_and_wrench: 測驗二
```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;
}
```
## :building_construction: 程式碼分析
* `letter` 變數的主要目的在於指出引數 `in` 是否為字母,考量 return 的值加上了一個 `and mask`,其值為 `0x0F`,根據 ASCII 數字表示法的特性,我們不須再將回傳值加上 `shift` 偏移值,即可代表 數字 `0` ~ `9`。
* 根據以上分析,此 case 中的 `shift` 值應為 0
* 因此,變數 `in` 的 `and mask` 應選擇 `0x40`,在輸入非字母的情況下,使 `shift` 值為 0。
* 根據上述的分析,在 `in` 值輸入字母(不分大小寫)時,`letter` 的值為 `0x40`
* 而 `shift` 變數的主要作用在於 `in` 為字母輸入時加上一個 `offset`,使 return `(in + shfit)` 的最低四個位元對應到`0x00` ~ `0x0F`,並透過 `and mask` 值 `0x0F` 令最高四位元的值為 0。
* 因此,`in` 輸入為字母時,`shift` 值應為 9。
* 因為 `A` / `a` 對應 `0x41` / `0x61`,故 `in + 9` 後即為 `0x4A` / `0x6A`,加上 `and mask` 值 `0x0F`,即得正確回傳值 `10 (decimal)`
* 其餘 `B` / `b` 至 `F` / `f` 之對應亦同。
* 參考下列的數值對應,可理解 `shift` 初值指派的右移值原理。
```
8'b01000000 <= letter
------------------------------
8'b00001000 <= letter >> 3
| 8'b00000001 <= letter >> 6
------------------------------
8'b00001001 <= shift
```
## :pencil: 擴充: 十六進制字串輸入與轉換
* TODO
---
# :hammer_and_wrench: 測驗三、四
### :penguin: 測驗三程式碼與分析
```cpp=
const uint32_t D = 3;
#define M ((uint64_t)(UINT64_C(0xFFFF_FFFF_FFFF_FFFF) / (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 位元的整數,並結合定點數的概念,將低 64 位元視為小數,高 64 位元視為整數。
* 除法原理公式: $n = q \cdot d + m$
* 移項除法原理公式: $\frac{n}{d} = q + \frac{m}{d}$
* 根據公式 $\frac{n}{d}=n\cdot\frac{2^{64}}{d}\cdot\frac{1}{2^{64}} = q + \frac{m}{d}$
* 移項得 $q = n\cdot\frac{2^{64}}{d}\cdot\frac{1}{2^{64}} - \frac{m}{d}$
* 因 $m < d$,且在,故上述公式在二進位計算下可簡化成 $q = n\cdot\frac{2^{64}}{d}\cdot\frac{1}{2^{64}}$
* 我們接著計算 $\frac{2^{64}}{d}$ ,也就是 Macro `M` 的部份。
* 因數值 $2 ^ {64}$ 需要 65 位元才能表示,且因 $2 ^ {64}$ 與 $2 ^ {64} - 1$ 除以 3 皆有**精確度不足**的狀況,因此我們使用 $\frac{2 ^ {64} - 1}{d}$ 代替。
* 也因為精確度不足的問題,不論是使用 $2 ^ {64}$ 與 $2 ^ {64} - 1$ ,在輸入數字恰好整除時,`((__uint128_t) M * n) >> 64` 所計算的 `quotient` 將會與實際值恰好差 1。(參考下述分析)
* 因此,我們在 Macro `M` 中的 $\frac{2 ^ {64} - 1}{d}$ 項後面再加上 1 補償誤差,即可解決。(參考下述分析)
* 變數 `quotient` 已重現了上述 $\frac{n}{d}=n\cdot\frac{2^{64}}{d}\cdot\frac{1}{2^{64}}$ 的較快版本,$n$ 乘上 Macro `M` 所計算的常數後,右移 64 位元,即可得到商數。
* 最後,透過除法原理公式的移項,回傳 n 的餘數。
* 即 $m = n - q \cdot d$
:::info
這方面的描述不太確定數值會恰好差 1 的原因,希望老師可以在指點方向修改成正確且精準的描述,謝謝老師~
:::
:::warning
考慮到 underflow 了嗎?
:notes: jserv
:::
>謝謝老師指點,經過一番思考後終於找出原因,但在研讀 [IEEE 754 Standard](https://people.eecs.berkeley.edu/~wkahan/ieee754status/IEEE754.PDF) 的 `underflow` 描述後,個人認為使用 `定點數精確度不足` 描述此一狀況較為精準,再麻煩老師分析,謝謝老師~
### :game_die: 分析:定點數精確度不足與補償
* 我們在上述使用 $\frac{2 ^ {64} - 1}{d}$ 時,與原式表示值會有誤差,這是因為定點數可能會遇到無法精確表示小數的狀況 (像是$\dfrac{1}{3}$),考量下列的算式:
* 理想:$\dfrac{1}{3} = \dfrac{1}{4} + \dfrac{1}{16} + \dfrac{1}{256} + ...= \sum_{i = 1}^{\infty}\dfrac{1}{4^i}$
* 實際:$\dfrac{1}{3} \approx \sum_{i = 1}^{N/2}\dfrac{1}{4^i}$,其中 $N$ 為定點數中小數部份的表示位元數
* 上述實際狀況的算式在左右兩邊乘以三之後,使用二進位表示之值明顯會小於 1,在只保留定點數中整數部份的情況下,即造成了 `quotient` 與實際值相差 1 之情形。
* 我們假設使用 16 位元表示一個定點數,整數與小數部份各佔用 8 個位元,參考下列 $\frac{1}{3}\cdot3$ 的操作:
```verilog
origin: 16'h0055 => 16'h00FF
```
* 因此,我們在小數點末位 +1 補償,即可得到正確的結果, 且不影響不可被 3 整除的數字輸出之商。
```verilog
add_1: 16'h0056 => 16'h0100
```
### :penguin: 測驗四程式碼與分析
```cpp=
const uint32_t D = 3;
#define M ((uint64_t)(UINT64_C(0xFFFF_FFFF_FFFF_FFFF) / (D) + 1))
bool divisible(uint32_t n)
{
return n * M <= M - 1;
}
```
* 根據上述精度的分析, 我們代入數個 `n` 值解析輸出結果:
```verilog
"1 * M" : 64'h5555_5555_5555_5556
"2 * M" : 64'hAAAA_AAAA_AAAA_AAAC
"3 * M" : 64'h0000_0000_0000_0002
_
"M" : 64'h5555_5555_5555_5556
"M - 1" : 64'h5555_5555_5555_5555
_
"4 * M" : 64'h5555_5555_5555_5558
"4 * M" : 64'hAAAA_AAAA_AAAA_AAAE
"6 * M" : 64'h0000_0000_0000_0004
```
* 根據上述的演示,當輸入的 n 可整除時,因定點數的整數部份須 128 位元才能表示,因此此處 64 位元即為定點數的小數部份,且因 n 整除時其定點數的小數部份趨近於 0 (此處僅考慮以二進位表示),其值必定小於 `M`, 故可以此作為判斷是否整除的依據。
* 再考量輸入值為 1 的狀況,此時因比較運算子為 `<=` ,故代入 `M` 會造成錯誤的輸出,故我們須採用 `M - 1` 表示,才能正確判斷是否整除。
---
# :hammer_and_wrench: 測驗五
```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);
}
```
## :building_construction: 程式碼分析
* 大寫轉成小寫的位元運算方法為:`char ^ 0x40`,因此當輸入值須轉換成小寫時,`mask` 之值應為 `0x40` ;否則為 `0x00` ,即保留原始字元之值。
* `mask` 初始值指派時,前面的運算結果右移了兩個位元,綜合上述的想法,`VV4` 之值應為 `0x80`。
* 根據原題目已給定的程式碼,`(A ^ Z)` 為判斷輸入字元是否在 `A` 到 `Z` 的範圍內。
* `XOR` 運算的特性為 `同0異1`, 即兩輸入位元相同則輸出0,兩輸入位元相異則輸出1。
* 根據這樣的特性, 我們期望透過 `XOR` 運算,在輸入為大寫英文字母時輸出 1,否則輸出 0 ;以符合上述的轉換規則。
* 再接著看到變數 `A` 與 `Z` 的初始值指派,我們透過下列表格搭配位元運算特性,推論 `VV2` 與 `VV3` 之值:
| 輸入字元 | 變數 `A` 之值 | 變數 `Z` 之值 | `mask` 之 MSB |
| -------- | ------------- | ------------- | --- |
| (0x40) | 127 + `VV2` | 102 + `VV3` | 0 |
| A(0x41) | 128 + `VV2` | 103 + `VV3` | 1 |
| Z(0x5A) | 153 + `VV2` | 128 + `VV3` | 1 |
| (0x5B) | 154 + `VV2` | 129 + `VV3` | 0 |
| a(0x61) | 160 + `VV2` | 135 + `VV3` | 0 |
| z(0x7A) | 185 + `VV2` | 160 + `VV3` | 0 |
* 參考 `0x40` 的輸入,其 `mask` 之 MSB 值應為 0,故 *`VV2` 必非正值*。
* 參考 `A` 的輸入,其 `mask` 之 MSB 值應為 1,因此 *`VV2` 必不可能為負值*,**故 `VV2` 必為 0**。
* 參考 `Z` 的輸入,其 `mask` 之 MSB 值應為 1,此時僅有令 `VV3` -1,才能同時令輸入 `Z` 時為 1,且輸入 `0x5B` 時輸出為 0;**故 `VV3` 必為 -1**。
* 參考測驗一中 `Standard ASCII` 之位元運算判定方法,`VV1` 之值應為 `0x80`。
## :dagger_knife: String Literal 議題探討
* `char *str = "xxxx"` 的宣告方式會將字串視為 `String Literal` 並儲存在 code section,對此 `String Literal` 進行寫入操作是 `未定義行為 (Undefined Behavior)`,進而造成 `Segmentation Fault`。
* 因此在宣告的時候須使用陣列配置或是動態記憶體配置的方法,才能順利將字串中的大寫字元轉換成小寫。
* Ref: [你所不知道的C語言:指標篇 - 重新探討「字串」](https://hackmd.io/@sysprog/c-pointer?type=view#%E9%87%8D%E6%96%B0%E6%8E%A2%E8%A8%8E%E3%80%8C%E5%AD%97%E4%B8%B2%E3%80%8D)
---
# :hammer_and_wrench: 測驗六
```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;
}
```
## :building_construction: 程式碼分析
* TODO