# 2023q1 Homework3 (fibdrv)
contributed by < `paulpeng-popo` >
> Github: [paulpeng-popo](https://github.com/paulpeng-popo/fibdrv)
## 開發環境
```shell
$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 39 bits physical, 48 bits virtual
CPU(s): 12
On-line CPU(s) list: 0-11
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 158
Model name: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
Stepping: 10
CPU max MHz: 4600.0000
CPU min MHz: 800.0000
BogoMIPS: 6399.96
Virtualization: VT-x
L1d: 192 KiB (6 instances)
L1i: 192 KiB (6 instances)
L2: 1.5 MiB (6 instances)
L3: 12 MiB (1 instance)
NUMA node0 CPU(s): 0-11
```
## 開發紀錄
### 頭腦暖身操
:::spoiler
#### 費氏數列推導
假設成年兔子為 `a`,幼年兔子為 `b`,且兔子不死亡
$a_{n+1} = a_n + b_n$
$b_{n+1} = a_n$
==一般遞迴==
$a_{n+1} = a_{n-1} + a_n, n \ge 1$
$a_0 = 0, a_1 = 1$
==矩陣運算 Q-matrix==
$\begin{bmatrix}
a_{n+1} \\ b_{n+1}
\end{bmatrix} = \begin{bmatrix}
1 & 1 \\
1 & 0
\end{bmatrix}
\begin{bmatrix}
a_n \\ b_n
\end{bmatrix}$
又可改寫成
$\begin{bmatrix}
a_{n+1} \\ a_n
\end{bmatrix} = \begin{bmatrix}
1 & 1 \\
1 & 0
\end{bmatrix}
\begin{bmatrix}
a_n \\ a_{n-1}
\end{bmatrix}$
$\begin{bmatrix}
a_{n+1} & a_n \\ a_n & a_{n-1}
\end{bmatrix} = \begin{bmatrix}
1 & 1 \\
1 & 0
\end{bmatrix}^n$
==Fast Doubling==
$F(0) = a_0 = 0$
$F(1) = a_1 = 1$
$$
\begin{split}
\begin{bmatrix}
F(2n+1) \\ F(2n)
\end{bmatrix} &=
\begin{bmatrix}
1 & 1 \\
1 & 0
\end{bmatrix}^{2n}
\begin{bmatrix}
F(1) \\ F(0)
\end{bmatrix}\\ \\ &=
\begin{bmatrix}
1 & 1 \\
1 & 0
\end{bmatrix}^n
\begin{bmatrix}
1 & 1 \\
1 & 0
\end{bmatrix}^n
\begin{bmatrix}
F(1) \\ F(0)
\end{bmatrix}\\ \\ &=
\begin{bmatrix}
F(n+1) & F(n) \\
F(n) & F(n-1)
\end{bmatrix}
\begin{bmatrix}
F(n+1) & F(n) \\
F(n) & F(n-1)
\end{bmatrix}
\begin{bmatrix}
1 \\ 0
\end{bmatrix}\\ \\ &=
\begin{bmatrix}
F(n+1)^2 + F(n)^2\\
F(n)F(n+1) + F(n-1)F(n)
\end{bmatrix}
\end{split}
$$
其中 $F(n-1) = F(n+1) - F(n)$
最後得到
$F(2k) = F(k)[ 2F(k+1)-F(k) ]$
$F(2k+1) = F(k+1)^2 + F(k)^2$
:::
### Fast Doubling
$F(2k) = F(k)[ 2F(k+1)-F(k) ]$
$F(2k+1) = F(k+1)^2 + F(k)^2$
作業說明中的虛擬碼
```clike
Fast_Fib(n)
a = 0; b = 1; // m = 0
for i = (number of binary digit in n) to 1
t1 = a*(2*b - a);
t2 = b^2 + a^2;
a = t1; b = t2; // m *= 2
if (current binary digit == 1)
t1 = a + b; // m++
a = b; b = t1;
return a;
```
### 原理
這裡 Fast Doubling 利用 **比 $n$ 兩倍小的 $n'$** 來算出 $F(n)$,遞迴的深度則被限制在 $O(\log{n})$ 內,這可以大幅減少重複的計算
參考:[Calculating Fibonacci Numbers by Fast Doubling](https://chunminchang.github.io/blog/post/calculating-fibonacci-numbers-by-fast-doubling)
```c
/* FILE: fibdrv.c */
static long long fib_fastd(long long n)
{
if (n < 2)
return n;
long long a = 0;
long long b = 1;
for (int j = 63 - 1; j >= 0; j--) {
long long c = a * ((b << 1) - a);
long long d = a * a + b * b;
a = c;
b = d;
if ((n >> j) & 1LL) {
a = d;
b = c + d;
}
}
return a;
}
```
n 小於 2,直接回傳,不會經過迴圈
從 n 的 MSB 開始,依序計算該數的費氏數,直到 n 的 LSB 或剩下之 bit 皆為 0,則提前跳出迴圈
先用 `h` 記錄實際的數字共有幾個 bit (不含 leading 0s),接著從 MSB 開始計算首項費氏數,再藉由迴圈依次增加 `n >> j` 的 bit 數,以計算後續的費氏數
**複雜度分析**
- 每次迭代使用 3 個乘法跟 2 個加法,時間複雜度為 $O(1)$
- 根據輸入的 n 大小決定迭代次數,即最多做 $O(log n)$ 次遞迴
- 總共時間複雜度為 $O(1) * O(log n) = O(log n)$
解釋 fast doubling ==為什麼用 2k 跟 2k + 1==
例如:**長度為 n 個 bit 的數字 $\alpha$** 與 **長度為 n+1 個 bit 的數字 $\beta$** 具有以下關係:
- if $\beta$ 要為偶數,則 $\beta = 2\alpha$,也就是 $\beta$ = $\alpha$ << 1
- if $\beta$ 要為奇數,則 $\beta = 2\alpha + 1$,也就是 $\beta$ = $\alpha$ << 1 + 1
剛好就是 binary number 的進位規則
使用 `__builtin_clzll` function 做硬體上的計算加速
`clz` (Count Leading Zero) 可以計算數字 `n` 前面有幾個 0,加速取得 `h` 的速度
```diff
- for (int j = 63 - 1; j >= 0; j--) {
+ long long mask = 1LL << (63 - __builtin_clzll(n));
+ for (; mask; mask >>= 1) {
- if ((n >> j) & 1LL) {
+ if (mask & n) {
```
### 實作大數運算前的效能比較
#### VLA 改進
Linux kernel 不支援 VLA,因此使用 Dynamic Programming 與空間重複利用的技巧,避免動態陣列的使用,同時將 space complexity 從 $O(n)$ 降到 $O(1)$
> Linux 核心限制使用 VLA 的因素
> 在 [The Linux Kernel is now VLA-Free](https://www.phoronix.com/news/Linux-Kills-The-VLA) 有提到 Linux kernel 不再允許 VLA 的使用
>
> C99 具有 VLA (Variable-Length Array) 的特性,允許執行時期再決定陣列佔用的空間大小,但可能會造成 stack overflow 等安全疑慮
根據教材 [你所不知道的 C 語言:函式呼叫篇](https://hackmd.io/@sysprog/c-function#stack-based-buffer-overflow),攻擊者可以利用 stack-based-buffer-overflow 來執行惡意程式碼,從而對系統造成威脅
>
> 此外,Linus Torvalds 也說過:『[USING VLA'S IS ACTIVELY STUPID! It generates much more code, and much _slower_ code (and more fragile code), than just using a fixed key size would have done.](https://lkml.org/lkml/2018/3/7/621)』
```c
/* FILE: fibdrv.c */
static long long fib_sequence(long long k)
{
long long f[2] = {0, 1};
if (k < 2)
return f[k];
for (int i = 2; i <= k; i++) {
f[i & 1] = f[0] + f[1];
}
return f[k & 1];
}
```
#### 時間測量功能
> 參考 [時間測量與效能分析](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-c) 的說明與 [qwe661234](https://hackmd.io/@qwe661234/linux2022q1-homework3#%E6%95%88%E8%83%BD%E5%88%86%E6%9E%90) 的解釋
> 1. [High resolution timers and dynamic ticks design notes](https://docs.kernel.org/timers/highres.html)
> 2. [hrtimers - subsystem for high-resolution kernel timers](https://docs.kernel.org/timers/hrtimers.html)
在 Linux kernel 中具有兩種計時方式
==jiffies== 是 Linux kernel 較早期存在的計時方法,使用作業系統**從開機以來的計時器中斷發生的次數**作為計時的依據,此全域變數稱作 `ticks` 數。計時器中斷為週期性的中斷事件,以固定的時間間隔觸發,利用間隔固定的特性便能達到測量時間的目的。
通常情況下,jiffies 的精度為毫秒級別,具體的精度取決於 tick rate 的大小。例如 1000Hz 表示每秒觸發 1000 次的 timer interrupt,因此 jiffies 的增加速度為每毫秒一次。
jiffies 轉換為**秒**的公式為
```c
(unsigned long long)(jiffies - INITIAL_JIFFIES) / HZ;
```
由以上程式碼可知, 假如系統原先設定的 `tick rate (HZ)` 不大於 $10^9$,其時間精度是無法達到 ns 等級的。
==hrtimer== 是在 2.6.16 開始有的新的計時機制,裡面使用了 `ktime_t` 這個資料結構來進行計時。書中提到,原本基於 `jiffies` 的計時機制 `timer wheel` 在許多層面上有許多缺失,並幾乎無法再優化/整合了,包括:
> 1. The forced handling of low-resolution and high-resolution timers in the same way leads to a lot of compromises, macro magic and #ifdef mess.
> 2. The unpredictable [O(N)] overhead of cascading leads to delays which necessitate a more complex handling of high resolution timers, which in turn decreases robustness.
> 3. The timer wheel data structure is too rigid for high-res timers.
因此實作 `hrtimer` 的設計考量包括
- simplicity
- data structure not bound to jiffies or any other granularity. All the kernel logic works at 64-bit nanoseconds resolution - no compromises
- simplification of existing, timing related kernel code
ktime_t 結構定義如下
```c
union ktime {
s64 tv64;
};
typedef union ktime ktime_t;
```
其結構有一型態為 64 位元的有號整數 tv64,單位為奈秒 (ns)
透過調用 [Linux kernel API](https://elixir.bootlin.com/linux/v4.4/source/kernel/time/timekeeping.c#L665) `ktime_get()`,可以取得**相對於系統啟動時間的偏移量**,因此藉由計算出兩次 `ktime_get()` 的時間差異,便可知其之間的程式碼執行時間花費
```c
/* FILE: fibdrv.c */
static ktime_t kt;
static long long fib_time_proxy(long long k)
{
kt = ktime_get();
long long result = fib_sequence(k);
kt = ktime_sub(ktime_get(), kt);
return result;
}
static ssize_t fib_read(struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
return (ssize_t) fib_time_proxy(*offset);
}
static ssize_t fib_write(struct file *file,
const char *buf,
size_t size,
loff_t *offset)
{
return ktime_to_ns(kt);
}
```
在 read 的時候,計算 fib,並同時將所花時間儲存在 `kt` 中
接著 write 的時候,將 `kt` 的值轉換成 nanoseconds 再回傳給 client
`client` 中順序調用 `read` 和 `write`,即可取得所花費的 kernel time
```c
/* FILE: client.c */
for (int i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
sz = read(fd, buf, 1);
kt = write(fd, write_buf, 1);
}
```
執行 `sudo ./client` 後,成功輸出計算費氏數時間,輸出如下
```
Reading from /dev/fibonacci at offset 0, returned the sequence 0.
Time: 331 (ns).
Reading from /dev/fibonacci at offset 1, returned the sequence 1.
Time: 197 (ns).
Reading from /dev/fibonacci at offset 2, returned the sequence 1.
Time: 183 (ns).
Reading from /dev/fibonacci at offset 3, returned the sequence 2.
Time: 113 (ns).
Reading from /dev/fibonacci at offset 4, returned the sequence 3.
Time: 123 (ns).
Reading from /dev/fibonacci at offset 5, returned the sequence 5.
Time: 120 (ns).
Reading from /dev/fibonacci at offset 6, returned the sequence 8.
Time: 81 (ns).
Reading from /dev/fibonacci at offset 7, returned the sequence 13.
Time: 78 (ns).
Reading from /dev/fibonacci at offset 8, returned the sequence 21.
...
```
#### 選擇演算法
> 參考 [yanjiew1](https://hackmd.io/@yanjiew/linux2023q1-fibdrv#%E5%A4%9A%E6%BC%94%E7%AE%97%E6%B3%95%E9%81%B8%E6%93%87) 的做法,利用 `read` 傳入 `size` 作為使用不同演算法的條件
```c
/* FILE: fibdrv.c */
switch (size) {
case 1:
result = fib_sequence(k);
break;
case 2:
result = fib_fastd(k);
break;
case 3:
result = fib_fastd_clz(k);
break;
default:
return -EINVAL;
}
```
使用 gnuplot 製圖,確認 fast doubling 確實加速了費氏數的計算,同時發現 __builtin_clzll 大幅降低了時間開銷

NOTE: 此處時間為 kernel mode 中計算的時間,還未加入 user space 的時間開銷比較
#### 排除效能干擾因素
使用 `taskset` 預留 cpu 給效能測試使用
1. `$ sudo vim /etc/default/grub`
2. 修改此行 `GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=5"`
3. `$ sudo update-grub`
4. `$ sudo init 6`
將作業說明提到的方法寫成一個 script
```sh
#!/bin/sh
CPUID=5
MASK=$((1 << $CPUID))
MASK=$((0xfff & ~$MASK))
MASK=`printf "%x" $MASK`
ORIG_ASLR=`cat /proc/sys/kernel/randomize_va_space`
ORIG_GOV=`cat /sys/devices/system/cpu/cpu$CPUID/cpufreq/scaling_governor`
ORIG_TURBO=`cat /sys/devices/system/cpu/intel_pstate/no_turbo`
ORIG_IRQ=`cat /proc/irq/$CPUID/smp_affinity`
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo"
sudo sh -c "echo performance > /sys/devices/system/cpu/cpu$CPUID/cpufreq/scaling_governor"
for file in /proc/irq/*/smp_affinity; do
var=0x$(cat "$file")
var=$((var & 0xfdf))
var=`printf "%x" $var`
sudo sh -c "echo $var > $file" 2> /dev/null
done
sudo sh -c "echo $MASK > /proc/irq/$CPUID/smp_affinity"
# Load the module and run the client
make unload
make load
python3 scripts/statisic_plot.py -cpu=$CPUID
gnuplot -e "filename='scripts/data.txt'" scripts/draw.gp
make unload
# Restore original settings
sudo sh -c "echo $ORIG_ASLR > /proc/sys/kernel/randomize_va_space"
sudo sh -c "echo $ORIG_GOV > /sys/devices/system/cpu/cpu$CPUID/cpufreq/scaling_governor"
sudo sh -c "echo $ORIG_TURBO > /sys/devices/system/cpu/intel_pstate/no_turbo"
sudo sh -c "echo $ORIG_IRQ > /proc/irq/$CPUID/smp_affinity"
```
最後使用 `taskset -c 5 ./client` 指定 client 使用 cpu 5 進行費氏數計算
#### 去除極端值
參考 [時間測量與效能分析](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-c)
計算 50 次費氏數的平均,並將離散值做處理,用以得到較平滑的效能比較圖
程式碼 [4b6a40c](https://github.com/paulpeng-popo/fibdrv/commit/4b6a40cd4785434a5250e6ef80409fb5e2ea2561#diff-310bc6068100c1b144bacada83ff9be0125cea94160c62aea2ca0b15c3e566fa)