# c-review
contributed by <`afcidk`>
## [第四週測驗題(一)](https://hackmd.io/s/SyK-WApKM#%E6%B8%AC%E9%A9%97-1)
先觀察 struct node,發現有 data, prev, next 成員,可以猜測資料結構是 linked list。
* #### `FunA` 的作用
看到 `if (!*start)` 區塊,這個區塊會配置新空間給 `*start`,再將 value 放進去,next/prev pointer 指向自己,代表在一個 node 的時候會是雙向的 circular linked list。
再往下看,這個部份屬於 `*start != NULL` 。從
```
new_node->next = *start;
(*start)->prev = new_node;
new_node->prev = last;
```
可以知道新的 node 會插在 `*start` 前面,但是 `*start` 並沒有被更改,所以可以看成在 linked list 最後面插入數字。
因此 `FunA` 的作用是在 linked list 最後面插入 node ,如果 `*start 為 NULL`,則配置空間給他。
* #### `FunB` 的作用
和 `FunA` 很像,但是是插入在 `*start` 前面,最後再將 `*start` 指派給新配的 node ,所以可以看成在 linked list 最前面插入 node。
沒有處理 `*start==NULL` 的情形,改善方法參考 `FunA`。
* #### `FunC` 的作用
會先把 `value1` 放到 new_node 裏面,再從 linked list 裏面找到 value 為 `value2` 的 node,在後面插入。但是 `FunC` 並沒有處理到找不到 `value2` 的狀況,若發生會進入無窮迴圈
改善方法:
```
struct node *start_addr = *start; // record the first address
while (temp->data != value2) {
temp = temp->next;
if (temp == start_addr){ // break if met previous address
puts("Cannot find value, FunC failed and did not insert number.");
break;
}
}
```
:::info
發現 malloc 呼叫完不用轉型也可以 assign 給變數,編譯參數加上 `-Wall` 也不會跳出警告,但是如果加上 `-Wc++compat` 就會警告我們必須要轉型,因為在 C+\+ 中這是不允許的。為什麼在 C 裡面允許的方式 C\++ 要禁止?或是說為什麼 C 可以允許這種寫法?
:::
::: warning
或許是因為 C 假設寫程式碼的人都知道自己在做什麼,所以限制比較小?這樣可以增進效能或是有什麼優點嗎?
C 和 C++ 是不同的程式語言,去比較兩者對 type 的定義差別 :notes: jserv
:::
* #### Z1 ~ Z10
由上面對 `Fun[ABC]` 的分析,再一步一步跑 main 裡面的程式
* 插入 51, 現在是 51(s)
* 插入 48, 現在是 48(s), 51
* 插入 72, 現在是 48(s), 51, 72
* 插入 86, 現在是 48(s), 51, 72, 86
* 在 51 之後插入 63,現在是 48(s), 51, 63, 72, 86
觀察 `display()` , traverse in forward direction 就是一直往後面找,當指標等於 `*start` 的時候就停止,也就是依序印出內容。
traverse in reverse direction 的迴圈會往前找,因為是雙向的 linked list,所以不會有指標為 `NULL` 的情形發生
## [第四週測驗題(二)](https://hackmd.io/s/SyK-WApKM#%E6%B8%AC%E9%A9%97-2)
* #### `FunX` 的作用
`FunX` 會順著 linked list 走下去。當發現是 circular 或是走到底的時候就會停下來,最後再回傳 node-head,可以用來判斷是不是 circular linked list,如果是會回傳 0 ,否則回傳非 0 數值。
在 for 迴圈裡面的 `data++` 沒有任何用途,猜測原本的用途應該是要在 `FunX` 裏面修改 `data` 的數值,程式碼應該要修改成 `(*data)++`,並且在函數一開始讓 `*data = 0`
* K1~K5 和 count 的數字
這段程式碼先建立基本的 linked list
```
struct node *head = node_new(0); // A(head)
head->next = node_new(1); // A->B
head->next->next = node_new(2); // A->B->C
head->next->next->next = node_new(3); // A->B->C->D
head->next->next->next->next = node_new(4); // A->B->C->D->E
```
然後檢查是不是 circular linked list,回傳非 0 ,輸出 YES
`head->next->next->next->next = head;`
現在結構變成 `A->B->C->D->A (circular)`,輸出 NO
`head->next->next->next->next->next = head->next;`
結構變成 `A->B->C->D->A (circular)`,輸出 NO
`head->next = head->next->next->next->next->next->next->next->next`
結構變成 `A->A (circular)`,輸出 NO
最後要輸出 `head->next->data`,這是 `A->data = 0`
因為剛才在 `FunX` 提到的缺陷,讓 count 無法更改數值,所以一直維持 0。
## [第五週測驗題(上1)](https://hackmd.io/s/SynmEVIqG)
考慮以下程式碼,推敲其作用
```clike
uint8_t Func32(uint32_t x) {
return x ? Func32(x >> 1) - 1 : 32;
}
```
先檢查 x 是不是 0 ,再將 x 向右位移 1 bit繼續遞迴下去。
直接從遞迴的最尾端往上想,最後一次遞迴一定是回傳 32,然後每次往回跑的時候都會遞減 1 ,因此可以判斷這個函式是用來 count leading zero。
將 `Func32` 改寫為以下功能等價的程式碼:
```
uint8_t FuncI(uint32_t x)
{
if (!x) return 32;
int n = 1;
if (!(x >> 16)) { n += 16; x <<= 16; }
if (!(x >> M1)) { n += 8; x <<= 8; }
if (!(x >> M2)) { n += 4; x <<= 4; }
if (!(x >> M3)) { n += 2; x <<= 2; }
n = n - (x >> 31);
return n;
}
```
知道這個函式在算 clz 之後,再看就很容易了。
4 個 for 迴圈使用二分搜尋法來查看前 n bit 是不是 0 (有沒有 1 ),第一次找 16 bit ,第二次 8 bit... 依序這樣下去,可以推得 M1=24(16+8), M2=28(16+8+4), M3=30(16+8+4+2)。
如果沒有找到區間內存在 1 的話,就把範圍縮小(`x <<= n` 那部份)
最後面的 `n = n - (x >> 31);` 也可以按照上面四個判斷式的寫法寫成 `if (!(x >> 31)) {...}`,只是少了判斷式可以讓程式快一點而已,並且一開始 n 需要初始化為 0 。
### 在 x86_64/Aarch32/Aarch64 ISA 中找出對應於 `Func32` 的指令,隨後在 GitHub 專案中找到 3 個應用案例並解說
### 回顧 Week1 到 Week5 的教材和作業,提出可用對應於 `Func32` (或下方 `Func64`) 的指令加速的案例
## [第五週測驗題(上2)](https://hackmd.io/s/SynmEVIqG#%E6%B8%AC%E9%A9%97-2)
先了解各個函式和資料結構的功能或實現的原理
* `union u_word`: 用來儲存把 64-bit 數字拆成兩個 32-bit 的 struct 和用 64-bit 儲存的 union,兩者記憶體空間共享(大小一樣)。
```clike
union u_qword {
struct {
uint32_t low;
uint32_t high;
} dwords;
uint64_t qword;
};
```
* `struct udiv_result`: 儲存運算完的結果,分成商和餘數。
```clike
struct udiv_result {
union u_qword q;
union u_qword r;
};
```
* `int Func64()`: `Func32()` 的擴展版本,先把數字拆成兩個 32-bit 資料,再利用原本的 `Func32()` 找出 leading zero。
```clike
static inline int Func64(uint64_t val)
{
uint32_t lo = (uint32_t) val;
uint32_t hi = (uint32_t) (val >> 32);
return hi ? Func32(hi) : 32 + Func32(lo);
}
```
* `int do_udiv32()`: 計算 32-bit 無號除法
首先,mask 會計算 divisor 和 dividend 的 leading zero 相差多少,計算 mask 的用意是知道商會有幾位數,後面我們可以利用到 mask 來判斷是否不能再繼續算下去。(mask 每次都要右移 1 bit,並決定是否要紀錄到商數,有紀錄就是 1 ,沒有就是 0)
二進位除法可以免除掉計算倍數的問題(一定是 0 或是 1 倍),只要用減的就可以完成除法。
在註解裡面的 align xxx 意思是把被除數 align 成我們計算的形式。回想長除法,除數是從 MSB 除到 LSB,所以要先把 divisor 往左"推",讓 divisor 和 dividend 的 MSB 是對齊的。
如果當前 dividend 大於 divisor,代表可以除一次,就利用 bitwise or 把 mask 的當前位數 set 上去,不能除的話商就是 0 ,只要把 mask 右移一次就好。
每次做完 divisor 都要左移一次,可以看成是往下一位繼續作除法。
#### 寫出來長這樣:
```
dividend: 0101010, divisor: 0101
| 代表對齊的地方
[] 代表右移後補上的 0
第一次
0|101010 / 0101[000]
第一次
01|01010 / 00101[00]
第一次
010|1010 / 000101[0]
第一次
0101|010 / 0000101
```
```clike
static int do_udiv32(uint32_t dividend,
uint32_t divisor,
struct udiv_result *res)
{
/* Ensure dividend is always greater than or equal to the divisor. */
uint32_t mask = Func32(divisor) - Func32(dividend);
divisor <<= mask; /* align divisor */
mask = 1U << mask; /* align dividend */
do {
if (dividend >= divisor) {
dividend -= divisor;
res->q.dwords.low |= mask;
}
divisor >>= 1;
} while ((P1) && dividend);
res->r.dwords.low = dividend;
return 0;
}
```
* `int udiv64()`: 計算 64-bit 無號除法,裏面有使用到 `do_udiv32()`。
注意到 `divisor == 0` 那邊有一個數字 `0xfff.....full`,那不是什麼很潮的寫法,只是在說有一個數字 `0xffffffffffffffff`,型態是 unsigned long long 而已。
基本上 64-bit 的版本和 32-bit 版本差不多,但是 64-bit 多了很多判斷的條件,包括:除以 0 ,除數被除數相等,除數大於被除數,只需要使用 32-bit 版本就可以實作...等,為什麼 32-bit 沒有考慮到這些問題呢?
如果我們會在 `udiv64()` 以外(像是 `main()` )呼叫到 `do_udiv32()`,就必須也加入 除以 0 等特殊情況的判斷式,但是現在我們很確定不會發生這種事情,因為這些特殊情況在呼叫前都被前面的判斷式過濾掉了。
和 32-bit 不同的地方在於 64-bit 多了一個變數 bits ,將原本 mask 的工作用兩個變數來完成。
bits 負責計算還有多少位數還沒除,每次會減 1 。
mask 負責當作 set bit 時用的 mask,每次都會左移一位元。
這樣就可以知道 bits 等於 0 的時候要離開迴圈。
:::info
感覺這樣做才是正確的,為什麼 32-bit 的版本可以讓 mask>>=1 來替代 bits 的工作呢?
:::
> 32-bit 版本的實作並不完整,其作用只是 helper function,用來實作 64-bit 版本,實際上也只有後者真的被使用
> [name=jserv]
```clike
int udiv64(uint64_t dividend, uint64_t divisor, struct udiv_result *res)
{
uint64_t mask;
uint64_t bits;
res->q.qword = res->r.qword = 0;
if (divisor == 0) { /* division by 0 ? */
res->q.qword = 0xffffffffffffffffull;
return -1;
}
if (divisor == dividend) {
res->q.qword = 1;
return 0;
}
if (divisor > dividend) {
res->r.qword = dividend;
return 0;
}
/* only 32 bit operands that the preconditions are fulfilled. */
if (!(divisor >> 32) && !(dividend >> 32))
return do_udiv32((uint32_t) dividend, (uint32_t) divisor, res);
bits = Func64(divisor) - Func64(dividend);
divisor <<= bits; /* align divisor and dividend */
mask = 1ULL << bits;
/* division loop */
do {
if (dividend >= divisor) {
dividend -= divisor;
res->q.qword |= mask;
}
divisor >>= 1;
mask >>= 1;
} while ((P2) && dividend);
res->r.qword = dividend;
return 0;
}
```
P3 和 P5 是用來改最後輸出的結果的,因為在函式裏面我們都是用數字來表示,想要印出字元的話就要將原本的數字加上 48 (ascii: '0')。
P4 那部份是用十進位來處理數字,所以答案是 10。
>可是 ASCII 0 好像是 48?
[name=e94046165][color=green]
> > 已更正,把解法寫出來就直接寫答案上去了Orz,下次不再犯QQ[name=afcidk]
### 將 10 進位輸出改為 16 進位
### 上方程式碼的 `0xCAFEBABE` 為 [magic number](https://en.wikipedia.org/wiki/Magic_number_(programming)),舉出在真實世界中程式的實際作用 (提示: 如 malloc)
## [第五週測驗題(中)](https://hackmd.io/s/HkQjgqI5G)
先理解為什麼下面程式碼輸出會是 `jserv++C`
```clike
#include <stdio.h>
int main() {
puts((char *) &(double []){ 3823806048287157.0, 96 });
}
```
把 `m` 轉成二進位形式表現,得到 `01000011 00101011 00101011 01110110 01110010 01100101 01110011 01101010`,再將每個位元組轉成字元表示,得到 `C++vresj`。
但是因為我們機器的儲存方式是 little-endian,因此顯示的會是相反過來的 `jserv++C`。
再看到真正的程式碼,可以看到原本的數字 m[0] 會乘上 $2^{96}$,我們將 exponent 的部份加上 96 ,得到 `0[1001001 0010]1011 00101011 01110110 01110010 01100101 01110011 01101010`,再轉換成字元表示,得到 `jserv++I`
### 修改程式,允許輸出 Github 帳號名稱
修改程式碼輸出 Github 帳號名稱
先把想要修改的字元寫出來,轉成二進位,用 0 把 bits 補齊,但是不能夠直接用 `00000000` 補,因為 0 是 termination character。
```clike
#include <stdio.h>
double m[] = {2.09703464477855301570627215629e209};
int main() { puts((char*)m); return 0; }
// output: afcidk
```
### 修改 gen() 函式,將 m[] 的內容變為英文姓氏
我先將二進位表示分成三個部份: sign bit, exponent, mantissa
(之所以不直接減他們的差是為了減少誤差導致的錯誤。)
原本 v.s. 修改後
`0[1101011 0110][0100 01101001 01100011 01100110 01100001 00000001 00000001]`
`0[1100111 0110][1110 01110101 01001000 00000001 00000001 00000001 00000001]`
* #### sign bit
沒有更動
* #### exponent
11010110110 -> 11001110110 ,差了 64 倍。
* #### mantissa
數字可以額外寫一個程式,利用 bitwise operator 來 clear/set bit ,再直接印出兩者的差
算出數字 `1.03214314438870169274345588995E209`
需要注意到的是如果先更改 exponent,mantissa 也要跟著乘上更改的數字。避免處理麻煩,我先加上 mantissa 才更動 exponent。
```clike
#include <stdio.h>
double m[] = { 2.09703464477855301570627215629E209, 64};
void gen() {
// original output: afcidk
m[0] += 1.03214314438870169274345588995E209;
while (m[1]--) {
m[0] /= 2;
}
puts((char *) m); // output:Hung
}
int main() { gen(); return 0; }
```
### 在 big-endian 的環境中,要如何改寫
在 little-endian 中,每 8 byte 為單位,最高位元會放在 LSB。而在 big-endian 中,最高位元會放在 MSB。
要改寫到 big-endian 環境使用,需要注意以 8 byte 為單位反向放資料。
舉例來說:`0x01234567 0x9abcdefa`
在 little-endian 要放 `67 45 23 01 fa de bc 9a`
而在 big-endian 要放 `01 23 45 67 9a bc de fa`
## [第五週測驗題(下)](https://hackmd.io/s/Sk30MXDqM)