# 2025q1 Homework2 (quiz1+2)
contributed by < `cmosinverter` >
## 開發環境
```shell
$ gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 4
On-line CPU(s) list: 0-3
Vendor ID: GenuineIntel
Model name: Intel(R) N100
CPU family: 6
Model: 190
Thread(s) per core: 1
Core(s) per socket: 4
Socket(s): 1
Stepping: 0
CPU(s) scaling MHz: 87%
CPU max MHz: 3400.0000
CPU min MHz: 700.0000
BogoMIPS: 1612.80
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat p
se36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdp
e1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl
xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqd
q dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm p
cid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave
avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb cat_l2 cd
p_l2 ssbd ibrs ibpb stibp ibrs_enhanced tpr_shadow flexpriority ept
vpid ept_ad fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid r
dt_a rdseed adx smap clflushopt clwb intel_pt sha_ni xsaveopt xsave
c xgetbv1 xsaves split_lock_detect user_shstk avx_vnni dtherm ida a
rat pln pts hwp hwp_notify hwp_act_window hwp_epp hwp_pkg_req vnmi
umip pku ospke waitpkg gfni vaes vpclmulqdq rdpid movdiri movdir64b
fsrm md_clear serialize arch_lbr ibt flush_l1d arch_capabilities
Virtualization features:
Virtualization: VT-x
Caches (sum of all):
L1d: 128 KiB (4 instances)
L1i: 256 KiB (4 instances)
L2: 2 MiB (1 instance)
L3: 6 MiB (1 instance)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0-3
Vulnerabilities:
Gather data sampling: Not affected
Itlb multihit: Not affected
L1tf: Not affected
Mds: Not affected
Meltdown: Not affected
Mmio stale data: Not affected
Reg file data sampling: Mitigation; Clear Register File
Retbleed: Not affected
Spec rstack overflow: Not affected
Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl
Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitizatio
n
Spectre v2: Mitigation; Enhanced / Automatic IBRS; IBPB conditional; RSB fillin
g; PBRSB-eIBRS Not affected; BHI BHI_DIS_S
Srbds: Not affected
Tsx async abort: Not affected
```
## 第一週 Qiuz 1
#### 延伸問題 1
> 解釋上方程式碼運作原理
根據 `list_insert_before` prototype 的描述,我們可以透過這個函式完成以下條件:
1. 將新的 item 加到 before 之前
2. 如果 before 是 head of list,那麼新的 item 會被加到最前面
3. 如果 before 為 NULL,那麼新的 item 會被加到最尾端
```c
static inline void list_insert_before(list_t *l, list_item_t *before, list_item_t *item)
{
list_item_t **p;
for (p = AAAA; *p != BBBB; p = CCCC)
;
*p = item;
DDDD = before;
}
```
考慮一般的情況 (如下圖):
```graphviz
digraph G {
rankdir=LR;
node [shape=record];
a [label="{ <data> head | <ref> }"];
b [label="{ <data> a | <ref> }"];
c [label="{ <data> b | <ref> }"];
d [label="{ <data> NULL}"];
a:ref:c -> b:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false];
b:ref:c -> c:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false, label="before"];
c:ref:c -> d:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false];
}
```
* p 的初始情況為一個指向 指向 head 的指標 的指標,因此 AAAA 為 `&l->head`
* p 是一個指標的指標,他會逐一走訪每一個節點的指標,只要 `*p != Before`,他就會繼續走訪,因此 BBBB 為 `*p != Before`
* 如果要移動 p 這個間接指標,那麼要先對他解指標 `*p` 然後取出該指標指向的節點所指向的下一個節點的指標 `(*p)->next`,然後再用 address-of operator 取出這個指標的位置: `&(*p)->next`,因此 CCCC 為 `&(*p)->next`
* 當 `*p == Before` 時,可以直接對 p 解指標然後指派 item 給 `*p`,此時情況如下圖所示:
```graphviz
digraph G {
rankdir=LR;
node [shape=record];
head [label="{ <data> head | <ref> }"];
a [label="{ <data> a | <ref> }"];
b [label="{ <data> b | <ref> }"];
item [label="{ <data> item | <ref> }"];
n [label="{ <data> NULL}"];
before [label="{ <data> &before | <ref> }"];
head:ref:c -> a:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false];
a:ref:c -> item:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false];
b:ref:c -> n:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false];
before:ref:c -> b:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false];
}
```
然後把 item 的 next 指派為 before,也就是目前指向 B 的指標:
```graphviz
digraph G {
rankdir=LR;
node [shape=record];
head [label="{ <data> head | <ref> }"];
a [label="{ <data> a | <ref> }"];
b [label="{ <data> b | <ref> }"];
item [label="{ <data> item | <ref> }"];
n [label="{ <data> NULL}"];
head:ref:c -> a:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false];
a:ref:c -> item:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false];
b:ref:c -> n:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false];
item:ref:c -> b:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false];
}
```
成功插入 item 至 linked-list 中。
那麼 `如果 before 是 head of list 呢` ? 如下圖所示:
```mermaid
graph LR
subgraph linked list
head==>node1==>node2
end
subgraph pointer
before==>head
end
```
根據剛剛完成的程式碼:
* 先假設程式碼是對的
```C=
static inline void list_insert_before(list_t *l, list_item_t *before, list_item_t *item)
{
list_item_t **p;
for (p = &l->head; *p != before; p = &(*p)->next)
;
*p = item;
item->next = before;
}
```
從程式碼中 for-loop 結束後的狀態可以得知,indirect pointer `p` 指向的指標 `*p`,正好跟 pointer `Before`一模一樣,也就是 `*p == Before`,如果此時照著程式碼 (6) 的邏輯繼續執行,如下圖:
```mermaid
graph LR
subgraph linked list
direction LR
head==>node1==>node2
end
subgraph linked list
direction LR
item
end
subgraph pointer to pointer
direction LR
p ==> item
end
subgraph pointer
direction LR
before==>head
end
```
`*p`,會改為指向一個新的 item,然後繼續執行 (7):
```mermaid
graph LR
subgraph pointer to pointer
direction LR
p
end
subgraph linked list
direction LR
p ==> item ==> head==>node1==>node2
end
```
此結構符合一開始的規定,也就是若 before 是 head of list,那麼新的 item 會被加到最前面。
最後,`如果 before 為 NULL,那麼 item 會被加進 tail of list 嗎` ?
從程式碼中 for-loop 結束後的狀態可以得知,p 會在迴圈結束後指向指向 NULL 的 pointer,所以綜合前面得到的經驗, item 會被加到 list 的最後。
#### 延伸問題 2
> 在現有的程式碼基礎上,撰寫合併排序操作
## 第一週 Qiuz 2
#### 延伸問題 1.1
> 解釋程式碼運作的原理,並嘗試補完程式碼
首先看到管理記憶體區塊的結構表示:
```C
typedef struct block {
size_t size;
struct block *l, *r;
} block_t;
```
因為所要管理的記憶體是樹狀結構,而且這個樹狀結構是一個 Binary Search Tree ,也就是任何一個節點他的 right subtree 內的任何一個節點的記憶體區塊大小都比較大,left subtree 內的任何一個節點的記憶體區塊大小都比較小,遵循這個規則,可以在 log(n) 時間複雜度找到目標大小的記憶體區塊。
閱讀完了程式碼和對應的註解後,了解到 remove_free_tree 這個函式想要達成的目的如下:
* 可以移除目標記憶體區塊
* 如果該區塊左右都有子節點,就需要找到 size 小於該區塊的最大記憶體區塊 `in-order predecessor`
* 如果該區塊只有一個子節點,用該子節點做替代
* 沒有子節點,直接移除該節點
#### 延伸問題 1.2
> 使其可獨立執行,應提供相關的測試程式碼
[Code](https://github.com/cmosinverter/linux2025-w1-q2)
測試程式碼構想:
* 先產生一個大小為 n 的 list,裡面每一個 element 都是 size_t
* 透過這個 list 建立一個 binary search tree
* 從原本的 list 中隨機 remove memory block k 次, k 小於等於 n
首先,為了讓程式碼可以獨立的執行,所以必須先完成 `find_free_tree` 和 `find_predecessor_free_tree` 對應的實作。
**[find_free_tree](https://github.com/cmosinverter/linux2025-w1-q2/blob/main/block.c#L8)**
* 用遞迴版本 Binary Search 求出 target 的指標
**[find_predecessor_free_tree](https://github.com/cmosinverter/linux2025-w1-q2/blob/main/block.c#L29)**
* 這邊的 predecessor 是指 in-order predecessor,所以該節點為左子節點的最右邊子節點
在測試樹狀記憶體結構之前,還要再補上一些樹操作的實作如下:
**[init_tree](https://github.com/cmosinverter/linux2025-w1-q2/blob/main/block.c#L49)**
* 藉由動態分配記憶體初始化 **root** 間接指標,並將 ***root** 指向 NULL
**[insert_free_tree_iterative](https://github.com/cmosinverter/linux2025-w1-q2/blob/main/block.c#L142)**
* 利用迭代法插入記憶體區塊
**[inorder](https://github.com/cmosinverter/linux2025-w1-q2/blob/main/block.c#L159)**
* In-order traversal 的遞迴版本,預期可以由小到大印出所有剩餘記憶體的大小
實際測試流程:
**[test.c](https://github.com/cmosinverter/linux2025-w1-q2/blob/main/test.c)**
1. 初始化根節點
2. 生成各種大小的記憶體區塊
3. 將前一步生成的記憶體區塊插入樹結構中
4. 移除所有記憶體區塊
5. 釋放記憶體
測試結果:
```shell
15
5 15
5 10 15
5 10 15 25
5 10 15 20 25
5 10 15 25
5 10 15
5 15
15
```
所有函式功能皆正常,最後使用 valgrind 檢查 memory leak:
```shell
$ valgrind ./test
==938828== Memcheck, a memory error detector
==938828== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==938828== Using Valgrind-3.22.0 and LibVEX; rerun with -h for copyright info
==938828== Command: ./test
==938828==
15
5 15
5 10 15
5 10 15 25
5 10 15 20 25
5 10 15 25
5 10 15
5 15
15
==938828==
==938828== HEAP SUMMARY:
==938828== in use at exit: 0 bytes in 0 blocks
==938828== total heap usage: 7 allocs, 7 frees, 1,152 bytes allocated
==938828==
==938828== All heap blocks were freed -- no leaks are possible
==938828==
==938828== For lists of detected and suppressed errors, rerun with: -s
==938828== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
```
valgrind 沒有發現記憶體洩漏。
#### 延伸問題 2
> 參閱 [memory-allocators](https://github.com/mtrebi/memory-allocators),針對你補完 (及後續改進) 的記憶體配置器,進行效能評比,並解讀其效能表現
## 第一週 Quiz 3
#### 延伸問題 1
> 解釋程式碼運作原理
以下程式碼為 quick_sort 的變數宣告:
```C
int n = list_length(list);
int value;
int i = 0;
int max_level = 2 * n;
struct list_head *begin[max_level];
struct list_head *result = NULL, *left = NULL, *right = NULL;
begin[0] = list->next;
list->prev->next = NULL;
```
max_level: 定義為 2 x n ,此情況模擬了遞迴實作下的最大遞迴深度。
begin: 本實作方法為 iterative 而不是 recursive,因此需要模擬一個 stack 結構。
result: 排序後的暫存指標
對於鏈結串列的 quick sort 而言,每一輪的排序(對於最外層的 while 迴圈而言)如下 :
* 若分割有大於一個節點:
* 將所有小於 pivot 的節點改接到 left,所有大於 pivot 的節點改接到 right
* 依照 left,pivot,right 的順序推到 stack 中
* 若分割沒有或只有一個節點:
* 若有一個節點的話,將該節點插入 result 的頭部
* 執行 stack pop `i--`
#### 延伸問題 2
> 研讀〈[A comparative study of linked list sorting algorithms](https://dl.acm.org/doi/pdf/10.1145/225890.225893)〉,分析針對雙向鏈結串列的排序演算法考量,並運用 Linux 核心風格的 List API 予以實作
## 第二週 Quiz 1
#### 延伸問題 1
> 解釋程式碼運作原理,改進 random_shuffle_array 也探討 list_for_each_entry_safe 建構的迴圈中,若將 list_move_tail 換為 list_move,為何會無法滿足 stable sorting
list_quicksort 為遞迴版本的 quick sort,其詳細步驟如下:
* 取出串列的第一個元素作為 pivot,並且從串列中移除 pivot
* 依序比對串列中各個元素與 pivot 的大小關係,小於的接到 list_less 尾端,大於的接到 list_greater 尾端
* 遞迴的對 list_less 和 list_greater 做排序
* 最後依照 list_less、pivot、list_greater 的順序組建最終排序串列
stable sorting 之 [Wikipedia](https://simple.wikipedia.org/wiki/Stable_sorting_algorithm) 定義:
> A sorting algorithm is called stable if it preserves the order of elements with the same sorting key.
假如將 list_move_tail 換成 list_move,那麼相同鍵值的相對順序將會反轉,導致 stable sorting 的狀態不成立,list_move_tail 因為遵循比較晚被比較的鍵值 (比較後面的鍵值) 會被放在後面的性質,因此 stable sorting 的狀態依然成立。
在改進 random_shuffle_array 之前,先探討題目是如何實作的。
```C=
static inline uint8_t getnum(void)
{
static uint16_t s1 = UINT16_C(2);
static uint16_t s2 = UINT16_C(1);
static uint16_t s3 = UINT16_C(1);
s1 *= UINT16_C(171);
s1 %= UINT16_C(30269);
s2 *= UINT16_C(172);
s2 %= UINT16_C(30307);
s3 *= UINT16_C(170);
s3 %= UINT16_C(30323);
return s1 ^ s2 ^ s3;
}
static uint16_t get_unsigned16(void)
{
uint16_t x = 0;
size_t i;
for (i = 0; i < sizeof(x); i++) {
x <<= 8;
x |= getnum();
}
return x;
}
```
本程式碼用到了一個叫做 Linear congruential generator (LCG) 的 PRNG,s1、s2、s3 分別代表一個 LCG,且由於 s1、s2、s3 已經被宣告為 static variable,因此他們只會在第一次 getnum 的 function call 被初始化為 2、1、1,並且在整個程式的生命週期內每一次 getnum function call 之後產生隨機數。
之所以使用三個 LCG 而不是單一個的好處就是可以增加隨機性,並且延長 PRNG 的週期至 30268、30306、30322 的最小公倍數。
將 random_shuffle_array 的問題:
1. 不能對任意 array 洗牌
2. 只能產生 0, 1, 2, ... length(array) 的隨機陣列
將 random_shuffle_array 改成 Fisher–Yates shuffle:
```C
static inline void random_shuffle_array(uint16_t *operations, uint16_t len)
{
for (uint16_t i = len-1; i > 0; i--) {
uint16_t j = get_unsigned16() % (i + 1);
uint16_t temp = operations[i];
operations[i] = operations[j];
operations[j] = temp;
}
}
```
同時在 main 裡面加入 init_array 來初始化 values[256],變成一個 0, 1, 2, 3, ..., 255 的陣列。
Reference
* [Wichmann–Hill](https://en.wikipedia.org/wiki/Wichmann%E2%80%93Hill)
* [Linear congruential generator](https://en.wikipedia.org/wiki/Linear_congruential_generator)
#### 延伸問題 2
> 將程式碼整合進 lab0 提及的 qtest,並探討環狀雙向鏈結串列的排序效能,並善用 perf 一類的效能分析工具,探討包含 Linux 核心 list_sort 在內排序實作的效能表現並充分解釋
## [第二週 Qiuz 2](https://hackmd.io/@sysprog/linux2025-quiz2#%E6%B8%AC%E9%A9%97-2)
#### 延伸問題 1
> 解釋上述程式碼,並探討擴充為 ⌈根號(x)⌉ (向上取整數) 實作,該如何調整並保持只用整數加減及位元操作
為了要好的理解 digit-by-digit method 的實作方法,我打算先實做一個自己看得懂的版本,~~因為我看了一個月還是看不懂 Quiz 裡面的寫法~~。
以下是我實作的版本:
```C=
uint64_t sqrti_simple(uint64_t x)
{
uint64_t y = 0;
if (x <= 1)
return x;
int total_bits = 64;
int shift = (total_bits - 1 - clz64(x)) & (~1UL);
uint64_t p = 0;
for(int i = (shift / 2); i >= 0; i--) {
uint64_t b = (p << (i + 1)) + (1 << (2 * i));
if (x >= b) {
x -= b;
p += (1 << i);
}
}
y = p;
return y;
}
```
`Line 5`: 若 x 小於 1,直接返回 (0, 1 平方根與自己相同)
`Line 8`: uint64_t 的 bit 個數
`Line 9`: 從 LSB 數過來最高的 set bit (最高的為 1 的 bit) 的 index
`Line 11`: 目前找到所有 bit 的和
`Line 12`:
假設 shift 是 k,那麼根據平方根的不等式定義:
$$ 2^k <= x <= 2^{k+1} $$
因此對 x 開根號之後:
$$ 2^{k/2} <= sqrt(x) <= 2^{(k+1)/2} $$
因為 quiz 裡面要找的平方根定義是要找到 $floor(x)$ 的 binary representation,所以 $sqrt(x)$ 的最高的 set bit 會是 $floor(k/2)$,這也是為什麼 for 迴圈裡面要從 `i = (shift / 2)` 開始尋找。
`Line 12~18`:
假設以下式子,我們想要找到 x 的平方根:
$$x=N^2\approx(b_n2^0 + b_{n-1}2^1 + ... +b_02^n)^2$$
其中,
$$b_i \in \{0, 1\}$$
且定義數列 $P$ 如下所示:
$$P_m=P_{m+1} + a_m$$
遞迴式子不太好懂,所以我先從 $m=0$ 把式子展開:
$$P_0=b_n2^0 + b_{n-1}2^1 + ... +b_02^n=P_{1} + a_m$$
其中,
$$a_m=b_m2^0$$
其實 $P_0$ 就是我們所要找的最終解答,在每一個 for 迴圈裡面,我們都要嘗試決定 $a_m$ 裡面的 $b_m$ 到底是 1 還是 0。
也就是說,每一次在進到迴圈之前都已經有了 $P_{m+1}$,我們想要看看如果 $(P_m)^2 = (P_{m+1}+2^m)^2$ 會不會超過 $x$,如果超過了代表 $a_m=b_m=0$,$P_m=P_{m+1}$,第 $m$ 個 bit 被設為 0,反之沒超過的話就設為 1。
可是要怎麼知道 $(P_m)^2$ 的值是多少?回到數列 $P$ 的定義用平方公式把他展開:
$$(P_{m+1} + a_m)^2 = P_{m+1}^2 + P_{m+1}2^{m+1} + 2^{2m}$$
可以發現 $(P_m)^2$ 其實就是 $P_{m+1}^2$ 加上 $P_{m+1}2^{m+1} + 2^{2m}$,而我們定義了誤差值的關係式:
$$X_m = N^2 - P_m^2$$
因為我們想要決定以下不等式是否成立:
$$(P_{m+1}+2^m)^2 \leq N^2$$
將以下式子帶入:
$$X_{m+1} = N^2 - P_{m+1}^2$$
所以等同檢查下面不等式是否成立:
$$X_{m+1} \geq P_{m+1}2^{m+1}+2^{2m}$$
這就是 `Line 12、13` 在做的事情。
`Line 15、16`: 把 $x$ 扣掉 $P_{m+1}2^{m+1}+2^{2m}$ (縮小誤差),然後將最終答案加上 $2^
i$
至於如何改進?參考[陳麒升](https://hackmd.io/@rota1001/linux2025-homework2?stext=19320%3A10%3A0%3A1746545653%3A_OW8m9)大神的解釋:
我們可以令 $y_i = P_i*2^{i}$,代入 `Line 13`
```C
uint64_t b = (p << (i + 1)) + (1 << (2 * i));
```
變成:
```C
uint64_t b = y + (1 << (2 * i));
```
`(p << (i + 1))` 實際上就是 $y_{i+1} = P_{i+1}*2^{i+1}$
令 $y_{i+1} = P_{i+1}*2^{i+1}$,代入 `Line 16`:
```C
p += (1 << i);
```
上面的數學是其實就是 $P_i = P_{i+1} + 2^i$
$$P_{i} = 2^{-i}*y_i$$
$$P_{i+1} = 2^{-(i+1)}*y_{i+1}$$
帶入 $P_i = P_{i+1} + 2^i$ 之後變成:
$$2^{-i}*y_i = 2^{-(i+1)}*y_{i+1} + 2^i$$
左右同時乘上 $2^{-i}$:
$$y_i = 2^{-1}*y_{i+1} + 2^{2i}$$
對應到程式碼的話,就是 `line 14、17`:
```C=
uint64_t sqrti_simple(uint64_t x)
{
uint64_t y = 0;
if (x <= 1)
return x;
int total_bits = 64;
int shift = (total_bits - 1 - clz64(x)) & (~1UL);
uint64_t p = 0;
for(int i = (shift / 2); i >= 0; i--) {
uint64_t b = y + (1 << (2 * i));
y >> 1;
if (x >= b) {
x -= b;
y += 1 << (2 * i)
}
}
return y;
}
```
最後看到 `Line 13、17` 都有出現的部分:
```C
(1 << (2 * i))
```
因為 for 迴圈裡面 的 i 是從 shift/2 開始,所以初始狀態的 i:
$$2i = 2*(shift/2) = shift$$
每一次的迭代 i 都會 -1,所以:
$$2(i-1) = 2*(shift/2-1) = shift-2$$
意思就是我們可以令 `m = 1 << shift`,然後每次在迴圈結束的時候把 `m` 往右位移兩個 bit,當 `m` 為 0 的時候剛好迴圈也結束了,所以可以順便把 `m` 當作 while loop 的條件。
修改之後的程式碼:
```C=
uint64_t sqrti_simple(uint64_t x)
{
uint64_t m, y = 0;
if (x <= 1)
return x;
int total_bits = 64;
int shift = (total_bits - 1 - clz64(x)) & (~1UL);
m = 1ULL << shift;
while (m) {
uint64_t b = y + m;
y >>= 1;
if (x >= b) {
x -= b;
y += m;
}
m >>= 2;
}
return y;
}
```
與 `uint64_t sqrti(uint64_t x)` 一樣,修改完畢。
至於向上取整數的話,先看到下面原本向下取整數的不等式:
$$(P_{m+1}+2^m)^2 \leq N^2$$
意思就是最終狀態
$$(P_{0})^2 \leq N^2$$
如果我們想要向上取整數,假設向上取整的最終結果是 $P_0'$,不等式要修改成下面的樣子
$$(P_{0}'-1)^2 < N^2$$
也就是 `if (x >= b)` 要改成 `if (x > b)`,
改完了之後再把扣掉的 1 加回去 `return y + 1`。
#### 延伸問題 2
> 參照計算機結構測驗題 C 及其注釋,以 C 語言 (搭配 GNU extension) 撰寫不依賴除法指令的 sqrtf,確保符合 IEEE 754 規格。對照 sqrtf, rsqrt, reciprocal implementations in Arm assembly
#### 延伸問題 3
> 參照從 √2 的存在談開平方根的快速運算提到的「藉由查表的平方根運算」,以 C 語言實作,並比較不同手法的整數開平方根效能一旦發現效能改進的機會,可準備提交改進 int_sqrt 程式碼到 Linux 核心
## 第二週 Qiuz 3