# 2020q3 Homework6 (quiz6)
contributed by < `sammer1107` >
###### tags: `進階電腦系統理論與實作`
==[測驗題目](https://hackmd.io/@sysprog/2020-quiz6)==
# 測驗 1
```c=
float fp32tobf16(float x) {
float y = x;
int *py = (int *) &y;
unsigned int exp, man;
exp = *py & 0x7F800000u;
man = *py & 0x007FFFFFu;
if (!exp && !man) /* zero */
return x;
if (exp == 0x7F800000u) /* infinity or NaN */
return x;
/* Normalized number. round to nearest */
float r = x;
int *pr = (int *) &r;
*pr &= 0xff800000;
r /= 256;
y = x + r;
*py &= 0xffff0000;
return y;
}
```
+ ==3==: 在 c 語言中,其實是不能夠對 floating point 作bitwise 運算的。所以我們必須先把他強制轉型成 int 。才能直接對浮點數作位元操作。
+ ==5~10==: 這裡我們分別把 exponent 與 mantissa 取出來,來判斷是不是特別的情況
+ 如果都為0,那麼數值肯定是 $\pm 0$,這種情況下最低的 16 位已經沒用到,所以可以直結回傳。
+ 如果 exp 為全 1,則為 infinity(`man == 0`) 或 NaN(`man != 0`)。infinity 的話也可以直接回傳。**但 NaN 的部份應該更加小心,因為 mantissa 只要不為 0 就算 NaN**
+ 如果要縮成 16 bit,應該也要做 `& 0xffff0000` 來去除低位的 bit 才對。
+ 但得要確保 bfloat16 的 mantissa 範圍內仍有 1 才行,否則單純去除低位 bit 有可能會讓 NaN 變成 bfloat16 的 infinity
故這部份應該改成:
```c=
if (exp == 0x7F800000u) { /* infinity or NaN */
// make sure the mantissa is non-zero if NaN
*py |= ((man != 0) << 16);
*py &= 0xFFFF0000;
return y;
}
```
+ ==12~17==: 這一部份要剩下 normalized 與 denormalized number 要處理
+ 做完 15 行後相當於把原本 `x` 的 mantissa 歸 0 變成 `r`。e.g. $x = 2^{23} \cdot 1.10011011... \to r=2^{23} \cdot 1.00000000...$
+ 再來除以 $256 = 2^8$ 次方的動作是為了作 rounding,如果原本 mantissa 中的第 16 bit 為 1 (對應 `0x000080000` 這個位置,也就是 bfloat16 範圍外的第一個 bit),加上 `r` 之後就會進位。**要注意這裡 `r` 是浮點數型態,所以除以 `256` 不是單純向右移,而是把 exponent 減 8 。** e.g.
\begin{align}
r/256 &= 2^{23-8} = 2^{15}\\
x+r/256 &= 2^{23} \cdot (1.10011011 + 0.00000001)\\
&= 2^{23} \cdot 1.10011100
\end{align}
如此完成進位。
+ denormalized 所獲得的 r 為 0,所以不會變化,相當於直接 truncate。
+ ==19~20==: 這裡再把低位的 16 個 bit 去掉就行了。
# 測驗 2
```c=
#define NEXT_START_INDEX(BUF) \
(((BUF)->start != (BUF)->size) ? ((BUF)->start + RB1) : 0)
#define NEXT_END_INDEX(BUF) (((BUF)->end != (BUF)->size) ? ((BUF)->end + RB2) : 0)
```
+ 此 ring buffer 的實作是利用一個大小 size+1 的 array。當 ring buffer 為空時,end 與 start 指向同一個地方。每當放入新元素,就是放在 end 的位置,然後移動 end 到下一個位置。讀取的時候則是從 start 讀取,然後移動 start ㄉ當滿了的時候則是 end 在 start前一個位置,這時 end 沒放東西,所以最大只能放 size 個元素。
+ `NEXT_START_INDEX` 的功用在於找到 `start` 的下一個位置,這其實就是 `start+1`,除了當 `start`在最後一格時,要繞回來 0 。`NEXT_END_INDEX` 同理。所以兩個答案都為 1。
## do {...} while(0)
+ 當我們要寫一個由多個 statement 構成的 macro function 時,我們可以利用 `do {...} while(0)` 來把這些 statement 包起來,來讓整個東西變成一個 regular statement。
+ 在 c 的標準中, **do ==statement== while (==expression==);** 放在 **iteration statement** 的定義當中,這個東西特別的地方他可以夾帶一個 compound statement (`{...}`)而且他是以 **`;`** 作為結尾,這讓他跟 **expression statement** 很類似,而一般的函式呼叫就是屬於 expression statement。
+ c 的標準中似乎沒有 regular statement 的定義,但我想 [stackoverflow 的回答](https://stackoverflow.com/questions/1067226/c-multi-line-macro-do-while0-vs-scope-block) 是要表達上述的意思。
:::warning
TODO: 用 dangling else 的處置來回覆本題
:notes: jserv
:::
# 測驗 3
這題為一個 singly-linked list 加上對應的 insertion sort 的實作。
+ 先看 `llist` 與 `cons(x,y)` 的定義:
```c=
#define cons(x, y) (struct llist[]){{y, x}}
struct llist {
int val;
struct llist *next;
};
```
首先這題用到 compound literal 的技巧,根據 c standard:
> A postfix expression that consists of a parenthesized type name followed by a brace-
enclosed list of initializers is a compound literal. It provides an unnamed object whose value is given by the initializer list.
也就是長這樣的東西:==( **type-name** ) { **initializer-list** }==
+ 在這裡 **type-name** 就是 `struct llist[]`,**initializer-list** 就是 `{y,x}`,用來初始化這個 array of struct llist 的第0個元素(其中 `y` 用來初始化 `val`,`x` 初始化 `next` ),而整個 array 也就只有一個元素。
+ 整個 `cons(x,y)` 會成為一個 lvalue,array 會被轉成 pointer,可以再被傳入下一層 `cons` 作為初始值。
+ 弄懂這個原理後,其實也可以把他寫成這樣。雖然比較醜,但是可以確認我對這東西的理解是正確的:
```c=
#define cons(x, y) (struct llist*){&(struct llist){y, x}}
```
我們利用 `(struct llist){y, x}` 來創造一個 `struct llist`,對他取址,再用這個地址初始化一個 `struct llist*`。如此同樣可以達成一樣的效果。
+ ==void sort(struct llist **head)==
```c=
void sort(struct llist **head)
{
struct llist *sorted = NULL;
for (struct llist *current = *head; current;) {
struct llist *next = current->next;
sorted_insert(&sorted, current);
current = next;
}
*head = sorted;
}
```
這裡為 insertion sort 的主體,迴圈做的事就是逐點走訪 linked list,把每個節點利用 `sorted_insert()` 來把這個節點加到已排序好的 list。
+ ==void sorted_insert(struct llist **head, struct llist *node)==
```c=
void sorted_insert(struct llist **head, struct llist *node)
{
if (!*head || (*head)->val >= node->val) {
node->next = *head;
*head = node;
return;
}
struct llist *current = *head;
while (current->next && current->next->val < node->val)
current = current->next;
node->next = current->next;
current->next = node;
}
```
+ 第 ==3== 行檢查新的節點是不是最小的,如果是就把頭換成新的節點。所以答案如上。
+ ==8~12==:如果不是,就逐漸走訪排序好的 list,直到走到合適的地方再插入。
# 測驗 4
## 原理
+ 這題的原理與之前 [quiz2-測驗6](https://hackmd.io/@sammer1107/ryHs1sESP) 的概念有點類似。一樣都是分開數每個 bit 出現的次數。
+ 陣列有 $n$ 個數值,數值的範圍是 $1,2,\ldots,n−1$,只有 $n-1$ 種,所以根據鴿籠原理一定會有至少一個數字重複,而根據題目,只會有一個數字重複,且出現次數不定。
+ 我們先看一個例子。假設 $n=5$,且每個數字都會出現,也就是重複的數字只會出現兩次的情況。
| 數字 | binary |
|:-------------------:|:------:|
| 1 | 001 |
| 2 | 010 |
| 3 | 011 |
| 4 | 100 |
| 各位置 1 出現的次數 | 122 |
+ 現在如果我們選擇讓 1 重複,則最後的總和會變成 **123**,藉由比對 $2<3$ 可以輕易得知多出來的是 1。
+ 同理若是其他數字重複,我們只要看哪個位置比起全部只出現一次的總和多,就知道多出來的那個數字的二進位表示法。
+ **假如並沒有每個數字都出現過呢?**
+ 這種情況下,重複的數字可能出現了更多次。延續上面的例子,假如 1 重複了兩次,我們可以知道是答案是 1 。但當 1 重複了 3 次,我們要選一個數字被 1 取代,這個數字將不會出現在陣列當中。
+ 若我們選擇讓 2 被 1 取代,則總和變為 **114**。選擇 3 被取代,則結果變為 **113**。
+ 我們可以整理出一個規律:當重複的數字重複更多次時,在這個數字為 1 的位置,總和只可能會增加,不會變少。在這個數字為 0 的位置,總和只會減少不會增加。 e.g. 我們讓 1 重複更多次時,最低位的總和只會變更多或是不變,不管怎麼樣都會大於原本的 $2$。其他兩個位置的總和只會減少,不管怎麼樣都不會大於原本的總和。
## 程式碼解說
```c=
int findDuplicate(int *nums, int numsSize)
{
int res = 0;
const size_t log_2 = 8 * sizeof(int) - __builtin_clz(numsSize);
for (size_t i = 0; i < log_2; i++) {
int bit = 1 << i;
int c1 = 0, c2 = 0;
for (size_t k = 0; k < numsSize; k++) {
if (k & bit)
++c1;
if (nums[k] & bit)
++c2;
}
if (c1 < c2)
res += bit;
}
return res;
}
```
+ ==4==: 這裡我們根據 n 計算最多需要計算多少 bit。只要在 n 的 leading zero 區域,更小的數字都不可能會用到這些 bit。但我們實際會出現的數字範圍只有到 $n-1$,所以這裡可以這樣改成 `numSize-1`:
```c
const size_t log_2 = 8 * sizeof(int) - __builtin_clz(numsSize-1);
```
+ ==外層迴圈==:這裡我們走過每個需要看的 bit 位置。
+ ==內能迴圈==:這裡我們走過 $0\sim n-1$,一邊計算 $1\sim n-1$ 當中這個 bit 1 出現的次數 `c1`,一邊計算陣列中的數字裡 1 出現的次數 `c2`。最後比較 `c1 < c2` 就知道重複的數字在這個位置上是不是 1 了。如果是,就在 res 把對應的 bit 設為 1。
+ 走完所有 bit 位置後,就知道重複的數字長怎樣了。
$$
\newcommand{\floor}[1]{\lfloor{#1}\rfloor}
$$
## 實作改善
### 分析
+ 現有實作的缺點就是一定得走訪整個 `nums` $\log_2(n-1)$ 次,時間複雜度為 $O(n\log n)$,換來的則是 $O(1)$ 的空間複雜度。
+ Leetcode 上最快的實作是直接用一個長度 $n$ 的陣列紀錄數字是否出現過,如此通常不必走完整個陣列就完成任務了。缺點是 $O(n)$ 的儲存空間。
### 改善
+ 現有的實作可透過增加一個長度 $\log_2(n)$ 的陣列來累計每個位置的 `c2`,如此只要完整走訪整個陣列一次,在每個數字則利用 gcc extension 加速。如此用 $O(\log n)$ 的儲存空間換取到更短的時間。
+ 另外一點是,`c1` 事實上可以直接計算而不必實際走訪 $1\sim n-1$ 來累計。觀察以下二進位表,就可以發現規律。
| Decimal | Binary |
| ------- | ------ |
| 0 | 000 |
| 1 | 001 |
| 2 | 010 |
| 3 | 011 |
| 4 | 100 |
| 5 | 101 |
| 6 | 110 |
| 7 | 111 |
首先 $0\sim n-1$ 和 $1\sim n-1$ 的累計是一樣的。再來第 0 個 bit 是 2 個數字作為一個周期變化,每兩個數字中有 1 個為 1 。第 1 個 bit 則是 4 個數字成為一個周期,每 4 個數字中有 2 個為 1 。
如此根據 n ,我們可以計算出 bit $i$ 的累積結果:
\begin{align}
\floor{\frac{n}{2^{i+1}}} 2^i + \max(n\bmod{2^{i+1}},2^i)-2^i
\end{align}
+ Code
```c=
int findDuplicate(int *nums, int n)
{
int res = 0;
const size_t log_2 = 8 * sizeof(int) - __builtin_clz(n-1);
int *c2 = calloc(log_2, sizeof(int));
for (size_t i = 0; i < n; i++) {
while(nums[i]) {
++c2[__builtin_ctz(nums[i])];
nums[i] &= nums[i] - 1;
}
}
for (size_t i = 0; i < log_2; i++) {
int c1, r, bit = 1 << i;
r = n & bit ? n & (bit-1): 0;
c1 = ((n >> (i+1)) << i) + r;
res |= (c1 < c2[i]) << i;
}
free(c2);
return res;
}
```
+ ==7~12==: 這裡走訪整個 `nums` 來統計各 bit 的累計次數。其中用到與 [quiz3-Bitmap](https://hackmd.io/xfEJ36T_QWyZPe2IAvB9dQ#%E6%B8%AC%E9%A9%97-5) 一樣的技巧,利用 ctz 來避免檢查每一個 bit 位置。
+ ==14~19==: 這裡檢查每一個 bit 位置,直接計算 `c1` 後,比較 `c1` 與 `c2` 來設定 `res`
+ 比較結果
+ 在 LeetCode 實測過後偶爾可以達到 4ms,原本則都是 8ms。
+ 以下為在我電腦上實測的結果,x 軸為 $n$,y 軸為執行時間。
![](https://i.imgur.com/CGU9EfB.png)
+ 測試程式
:::warning
這裡測資的產生並不夠隨機,所以比較結果可能不公平,畢竟新方法會受到 set bit 數量而影響速度。
但平均來講新方法還是會比較快才對。
:::
:::spoiler
```c=
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#define MAX_N 20000
int findDuplicate_improve(int *nums, int n)
{
int res = 0;
const size_t log_2 = 8 * sizeof(int) - __builtin_clz(n-1);
int *c2 = calloc(log_2, sizeof(int));
for (size_t i = 0; i < n; i++) {
while(nums[i]) {
++c2[__builtin_ctz(nums[i])];
nums[i] &= nums[i] - 1;
}
}
for (size_t i = 0; i < log_2; i++) {
int c1, r, bit = 1 << i;
r = n & bit ? n & (bit-1): 0;
c1 = ((n >> (i+1)) << i) + r;
res |= (c1 < c2[i]) << i;
}
free(c2);
return res;
}
int findDuplicate(int *nums, int numsSize)
{
int res = 0;
const size_t log_2 = 8 * sizeof(int) - __builtin_clz(numsSize);
for (size_t i = 0; i < log_2; i++) {
int bit = 1 << i;
int c1 = 0, c2 = 0;
for (size_t k = 0; k < numsSize; k++) {
if (k & bit)
++c1;
if (nums[k] & bit)
++c2;
}
if (c1 < c2)
res += bit;
}
return res;
}
int main(void)
{
int *nums = malloc(sizeof(int) * MAX_N);
nums[0] = 1;
double time1=0, time2=0;
for (int i=2; i < MAX_N; ++i) {
nums[i-1] = i;
nums[i] = 1;
clock_t t1,t2;
t1 = clock();
findDuplicate(nums, i + 1);
time1 += t1 = clock() - t1;
t2 = clock();
findDuplicate_improve(nums, i + 1);
time2 += t2 = clock() - t2;
printf("%d, %.8f, %.8f\n", i + 1,
(double)t1 / CLOCKS_PER_SEC, (double)t2 / CLOCKS_PER_SEC); // in seconds
}
printf("%.8f, %.8f\n", time1 / CLOCKS_PER_SEC, time2 / CLOCKS_PER_SEC); // in seconds
return 0;
}
```
:::