---
tags: linux2022
---
# 2022q1 Homework3 (fibdrv)
contributed by < `hsuedw` >
* [作業繳交](https://hackmd.io/@sysprog/linux2022-homework3)
* [作業需求](https://hackmd.io/@sysprog/linux2022-fibdrv)
* Due Date: Mar 21, 2022
---
# 預備知識
* [Linux 核心模組運作原理](https://hackmd.io/@sysprog/linux-kernel-module)
# 工作環境
* 作業系統
* Ubuntu 20.04.4 LTS (Focal Fossa)
* Linux kernel: 5.13.0-30-generic
:::spoiler details
```
$ uname -a
Linux edward-ubuntu 5.13.0-30-generic #33~20.04.1-Ubuntu SMP Mon Feb 7 14:25:10 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
```
:::
* 硬體資源
* CPU: Intel(R) Core(TM) i5-7400 CPU @ 3.00GHz, 4 cores
:::spoiler CPU details
```
$ 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): 4
On-line CPU(s) list: 0-3
Thread(s) per core: 1
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 158
Model name: Intel(R) Core(TM) i5-7400 CPU @ 3.00GHz
Stepping: 9
CPU MHz: 3000.000
CPU max MHz: 3500.0000
CPU min MHz: 800.0000
BogoMIPS: 6000.00
Virtualization: VT-x
L1d cache: 128 KiB
L1i cache: 128 KiB
L2 cache: 1 MiB
L3 cache: 6 MiB
NUMA node0 CPU(s): 0-3
```
:::
* RAM: 8 GB
* 編譯器
```
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
```
---
# Fix VLA not allowed in kernel issue
編譯 `fibdrv` 時,發現下列警告訊息。雖然 `fibdrv` 仍可安裝並計算 Fibonacci number。不過,既然 Linux kernel 不允許使用 variable length array (VLA),那就把先把這個警告訊息處理掉。
目前採用的解法是用動態規劃 (Dynamic Programming) 裡的狀態壓縮技巧處理。原本的 `fib_sequence()` 裡有個 `f` 陣列,用來計算 `f[k]`。這樣做的時間與空間複雜度都是 O(k)。不過,仔細想想,以 buttom-up DP 實作的演算法只需要 `f[k - 1]` 與 `f[k - 2]` 就可以計算出 `f[k]`。所以不需要一個長度為 `k` 的陣列。所以改用下列實作,既可避開禁止使用 VLA 的警告訊息,又可降低空間複雜度到 O(1)。
:::spoiler Fix VLA not allowed in kernel issue (commit: dd77cd1)
```c
static long long fib_sequence(long long k)
{
if (k < 2)
return k;
long long fk = -1, fk_2 = 0, fk_1 = 1;
for (int i = 2; i <= k; i++) {
fk = fk_2 + fk_1;
fk_2 = fk_1;
fk_1 = fk;
}
return fk;
}
```
:::
# 使用 sysfs 介面
`fibdrv` 遇到的第一個要解決的問題就是因為 C 語言內建的 `long long` 資料型態所能表示的最大值為 2^63^ - 1 = 9,223,372,036,854,775,807。導致在計算 F~93~ 出現溢位 (overflow)。
> F~93~ = 15080227609492692858
就算是改用 `unsigned long long` 最大值也只能是 2^64^ - 1 = 18,446,744,073,709,551,615。勉強可以用來計算 F~93~。但是作業的要求是至少要計算到 F~100~,甚至是更後面的 Fibonacci number。所以必須要使用自訂資料型態實作 big number。不過,若是以目前 `fibdrv` 所採用的 VFS,實作 `open`、`close`、`read`、`write` 等 system call 的方式將 `fibdrv` 計算出來的 Fibonacci number (用自訂資料型態實作 big number 表示) 困擾。在用 VFS 的實作前提下,可能的解決方案有兩種。
1. 使用 `ioctl` 這個 system call。這樣可以一次把自訂 big number 型態的 object 讀到 user space 中。不過,這樣做的話, `fibdrv` 必須向 `client` 揭露 big number 資料型態定義與其他相關實作細節。
2. 按照目前 `client` 所提示的實作方式,用 `lseek` 與 `read` 兩個 system call 搭配操作。先用 `lseek` 設定準備要讀取的資料的位置,再用 `read` 把指定的資料讀出來。一次讀一個 `long long` 的資料,直到把自訂 big number 資料結構中記錄 Fibonacci number 的所有 `long long` 讀完為止。
這兩種做法對 `client` 而言,都不是很友善。因為 `client` 必須重組資料才能得到完整的 Fibonacci number。而且必須揭露許多 `fibdrv` 的實作細節給 `client`。會加深 `fibdrv` 與 `client` 間 coupling 的程度。
> [Coupling (computer programming)](https://en.wikipedia.org/wiki/Coupling_(computer_programming))
比較好的做法應該是 `client` 能夠直接讀到計算完成的 Fibonacci number。並且不要揭露任何關於 `fibdrv` 內部的實作細節給 `client`。所以我打算使用 sysfs 介面。
目前打算在 sysfs 下產生三個檔案。藉以向 `client` 揭露 `fibdrv` 的計算結果。
1. `/sys/kernel/fibonacci/input`: `client` 在此檔案中寫入想要計算的 Fibonacci number 的項數。例如,寫入 10 表示想要計算 F~10~。
3. `/sys/kernel/fibonacci/output`: `fibdrv` 計算的 Fibonacci number。例如,F~10~ = 55。
4. `/sys/kernel/fibonacci/time`: `fibdrv` 計算 Fibonacci number 所花費的時間。
:::spoiler 測試目前實作的 sysfs interface (commit: 6d04220)
```
$ make clean all
make -C /lib/modules/5.13.0-30-generic/build M=/home/edward/workspace/linux_2022/homework3/fibdrv clean
make[1]: Entering directory '/usr/src/linux-headers-5.13.0-30-generic'
CLEAN /home/edward/workspace/linux_2022/homework3/fibdrv/Module.symvers
make[1]: Leaving directory '/usr/src/linux-headers-5.13.0-30-generic'
rm -f client out
cc -o client client.c
make -C /lib/modules/5.13.0-30-generic/build M=/home/edward/workspace/linux_2022/homework3/fibdrv modules
make[1]: Entering directory '/usr/src/linux-headers-5.13.0-30-generic'
CC [M] /home/edward/workspace/linux_2022/homework3/fibdrv/fibdrv.o
MODPOST /home/edward/workspace/linux_2022/homework3/fibdrv/Module.symvers
CC [M] /home/edward/workspace/linux_2022/homework3/fibdrv/fibdrv.mod.o
LD [M] /home/edward/workspace/linux_2022/homework3/fibdrv/fibdrv.ko
BTF [M] /home/edward/workspace/linux_2022/homework3/fibdrv/fibdrv.ko
Skipping BTF generation for /home/edward/workspace/linux_2022/homework3/fibdrv/fibdrv.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-5.13.0-30-generic'
$
$ sudo insmod fibdrv.ko
$
$ lsmod | grep fibdrv
fibdrv 16384 0
$
$ sudo su
[sudo] password for edward:
#
# whoami
root
#
# cat /sys/kernel/fibonacci/input
0
#
# cat /sys/kernel/fibonacci/output
0
#
# echo 10 > /sys/kernel/fibonacci/input
#
# cat /sys/kernel/fibonacci/input
10
#
# cat /sys/kernel/fibonacci/output
55
#
# echo 92 > /sys/kernel/fibonacci/input
# cat /sys/kernel/fibonacci/input
92
#
# cat /sys/kernel/fibonacci/output
7540113804746346429
```
:::
* 參考資料:
[Brief sysfs Tutorial](https://www.cs.swarthmore.edu/~newhall/sysfstutorial.pdf)
[Documentation/filesystems/sysfs.rst](https://github.com/torvalds/linux/blob/master/Documentation/filesystems/sysfs.rst)
[Sample kobject implementation](https://elixir.bootlin.com/linux/latest/source/samples/kobject/kobject-example.c) (注意: 切換到對應的 Linux 核心版本)
---
# 實作 big number
* 本階段目標
- [x] 實作 big number 資料結構。
- [x] 實作 big number 加法運算。使 `fibdrv` 可用原本的迭代演算計算 Fibonacci number (F~92~ 以上)。
- [x] 實作 big number 轉字串。在字串中以十六進位表示 Fibonacci number。
- [x] 為 Fast Doubling 計算 Fibonacci number 做準備。
- [x] 實作 big number bitwise left shift
- [x] 實作 big number 減法運算。
- [x] 實作 big number 乘法運算。
* big number 資料結構
```c
/*
* bignum data structure
* number[0] contains least significant bits
* number[size - 1] contains most significant bits
* sign = 1 for negative number
*/
typedef struct _bignum {
NUM_TYPE *num;
size_t sz;
int sign;
} bignum;
```
* 目前只有用到 `num` 與 `sz` 兩個欄位。
* `num` 是指向一個 `kmalloc()` 動態宣告的記憶體空間。其大小為 `BIGNUM_SZ * sizeof(NUM_TYPE)`。目前 `BIGNUM_SZ` 設定為 `500`。`NUM_TYPE` 定義為 `uint32_t`。以目前這樣的實作可以算到 F~23000~。
* big number 加法運算
```c=
// s = a + b
void bignum_add(bignum *s, const bignum *a, const bignum *b)
{
memset(s->num, 0, s->sz * sizeof(NUM_TYPE));
int carry = 0;
for (int i = 0; i < s->sz; ++i) {
TMP_TYPE tmp = (TMP_TYPE) a->num[i] + b->num[i] + carry;
carry = tmp > MAX_VAL;
s->num[i] = tmp & MAX_VAL;
}
return;
}
```
* `TMP_TYPE` 被定義為 `uint64_t`。`NUM_TYPE` 被定義為 `uint32_t`。而 `a->num[i]` 的型態為 `NUM_TYPE` 也就是 `uint32_t`。
* 所以,在第 8 行執行加法運算時,被加數 (`a->num[i]`) 與加數 (`b->num[i]`) 均被轉型為 `uint64_t`。然後在第 9 行判斷變數 `tmp` 是否大於 `uint32_t` 所能表示的最大值決定是否有發生進位。
* big number 轉字串。在字串中以十六進位表示 Fibonacci number
```c
ssize_t bignum_to_string(bignum *bn, char *buf)
{
ssize_t s_len = BIGNUM_SZ * sizeof(NUM_TYPE) * 2 + 1;
ssize_t j = 0, blk_sz = sizeof(NUM_TYPE) * 2 + 1;
char *s = (char *) kmalloc(s_len, GFP_KERNEL);
for (int i = bn->sz - 1; i >= 0; --i)
j += snprintf(s + j, blk_sz, "%08X", bn->num[i]);
j = snprintf(buf, s_len + 1, "%s\n", s);
kfree(s);
return j;
}
```
* 目前這個將 big number 轉為字串的實作是以十六進位顯示。因為 big number 是以二進位的想法在實作運算。這樣可以省去轉換為十進位的運算成本。
* 這個 big number 轉為字串的實作沒有去掉 leading zeros。應該稍後會回來實作這部分。
* big number bitwise left shift
```c
// bn = bn << shift
void bignum_lshift(bignum *bn, size_t shift)
{
shift %= 32; // only handle shift within 32 bits atm
if (!shift)
return;
for (int i = bn->sz - 1; i > 0; i--)
bn->num[i] = bn->num[i] << shift | bn->num[i - 1] >> (32 - shift);
bn->num[0] <<= shift;
}
```
* big number 的 bitwise left shift 運算是為了 fast doubling 演算法做準備。因為在計算 F(2k) 時,需要對 F(k+1) 乘 2。
* 對數字左移一個 bit,相當於對此數字執行乘 2 運算。
* big number 減法運算
```c
// d = a - b
void bignum_sub(bignum *d, const bignum *a, const bignum *b)
{
memset(d->num, 0, d->sz * sizeof(NUM_TYPE));
bignum *tmp = bignum_create(BIGNUM_SZ);
bignum_cpy(tmp, b);
// Calculate the two's complement for b.
tmp->num[0] = ~tmp->num[0];
int carry = ((TMP_TYPE) tmp->num[0] + 1) > MAX_VAL;
tmp->num[0] += 1;
for (int i = 1; i < tmp->sz; ++i) {
tmp->num[i] = ~tmp->num[i];
carry = ((TMP_TYPE) tmp->num[i] + carry) > MAX_VAL;
tmp->num[i] += carry;
}
// d = a - b = a + b(two's complement)
bignum_add(d, a, tmp);
}
```
* big number 的減法運算基本上就是先對減數取其二補數 (two's complement)。然後再利用 big number 加法運算將被減數與被減數二補數相加。
* big number 乘法運算
```c
static void bn_mult_add(bignum *p, int offset, TMP_TYPE x)
{
TMP_TYPE carry = 0;
for (int i = offset; i < p->sz; ++i) {
carry += p->num[i] + (x & MAX_VAL);
p->num[i] = carry;
carry >>= 32;
x >>= 32;
if (!x && !carry)
return;
}
}
// p = a * b
void bignum_mult(bignum *p, const bignum *a, const bignum *b)
{
memset(p->num, 0, p->sz * sizeof(NUM_TYPE));
for (int i = 0; i < a->sz; ++i)
for (int j = 0; j < b->sz; ++j) {
TMP_TYPE carry = (TMP_TYPE) a->num[i] * b->num[j];
bn_mult_add(p, i + j, carry);
}
}
```
* Debug tools:
* [fibonacci number generator](https://www.omnicalculator.com/math/fibonacci)
* [Decimal to Hexadecimal converter](https://www.rapidtables.com/convert/number/decimal-to-hex.html)
* [Online Big Number Calculator](https://defuse.ca/big-number-calculator.htm)
* [Hexadecimal to Decimal converter](https://www.rapidtables.com/convert/number/hex-to-decimal.html)
* 參考實作:
* [KYG-yaya573142](https://github.com/KYG-yaya573142/fibdrv/blob/master/bn_kernel.c)
* [sysprog21](https://github.com/sysprog21/bignum/blob/master/bignum.c)
* [bignum.c](https://www3.cs.stonybrook.edu/~skiena/algorist/book/programs/bignum.c)
* [kokke](https://github.com/kokke/tiny-bignum-c/blob/master/bn.c)
---
# 實作時間量測
在 `fibdrv.c` 中使用 `ktime` 量測每次計算 Fibonacci number 所花費的時間。單位是 nano second。我把實作 fast doubling (`fib_fast_doubling()`) 與 iteration (`fib_sequence()`) 的函式原型設計得一樣,然後利用函式指標 (function pointer) 依據不同的需求將不同的演算法送入 `fib_time_cost()` 中。
```c
static void fib_time_cost(void (*fib_algo)(NUM_TYPE), NUM_TYPE k)
{
kt = ktime_get();
fib_algo(k);
kt = ktime_sub(ktime_get(), kt);
}
```
在 `client.c` 中使用 `clock_gettime()` 量測讀取 Fibonacci number 所需花費的時間。單位是 nano second。
```c
:
struct timespec t1, t2;
size_t rd_len, out_len = 0;
// Read a Fibonacci number from linux kernel.
// The output of the Fibonacci driver is a hexdecimal number.
char fib_output[FIB_OUTPUT_LEN];
int fd_out = open(FIB_OUTPUT, O_RDONLY);
clock_gettime(CLOCK_MONOTONIC, &t1);
while (rd_len = read(fd_out, fib_output + out_len,
FIB_OUTPUT_LEN - out_len)) {
out_len += rd_len;
}
clock_gettime(CLOCK_MONOTONIC, &t2);
fib_output[FIB_OUTPUT_LEN - 1] = '\0';
close(fd_out);
:
// Record the time for reading a Fibonacci number.
uint64_t rd_time = (uint64_t)((t2.tv_sec * 1e9 + t2.tv_nsec) -
(t1.tv_sec * 1e9 + t1.tv_nsec));
snprintf(tmp_buf, TMP_BUF_LEN, "%d %lu\n", k, rd_time);
int fd_rd_time = open(rd_time_file, O_CREAT | O_APPEND | O_WRONLY,
S_IRUSR | S_IRGRP | S_IROTH);
write(fd_rd_time, tmp_buf, strlen(tmp_buf));
close(fd_rd_time);
```
---
# 簡化實驗流程
這個階段的目標是要透過下列方法簡化實驗流程。
* 在 `sysfs` 中新增 `/sys/kernel/fibonacci/algo`。利用它來動態調整計算 Fibonacci number 的演算法。
* 在 `fibdrv.c` 中,實作 iteration 與 fast doubling 的函式原型如下所示:
```c
static void fib_fast_doubling(NUM_TYPE k);
static void fib_sequence(NUM_TYPE k);
```
* 定義函式指標 `fib_algo` 動態指向上述兩個函式中的一個。`fib_algo` 預設指向 `fib_fast_doubling`。
```c
static void (*fib_algo)(NUM_TYPE);
```
* 對 `/sys/kernel/fibonacci/algo` 分別寫入 `"iteration"` 與 `"fast-doubling"` 就可以控制計算 Fibonacci number 的演算法。
```c
static ssize_t fib_algo_store(struct kobject *kobj,
struct kobj_attribute *attr,
const char *buf,
size_t count)
{
if (strncmp(buf, "iteration", 9) == 0) {
/* Set the algorithm to be iteration. */
pr_info("%s: Set the algorithm to be iteration.\n", KBUILD_MODNAME);
fib_algo = fib_sequence;
} else if (strncmp(buf, "fast-doubling", 13) == 0) {
/* Set the algorithm to be fast-doubling. */
pr_info("%s: Set the algorithm to be fast-doubling.\n", KBUILD_MODNAME);
fib_algo = fib_fast_doubling;
} else {
pr_info("%s: %s is not support.\n", KBUILD_MODNAME, buf);
pr_info("%s: Set the algorithm to be fast-doubling.\n", KBUILD_MODNAME);
fib_algo = fib_fast_doubling;
}
return strlen(buf);
}
---
# 為效能分析設定實驗環境
## 將行程固定在特定的 CPU 中執行
執行 `taskset` 並使用 `-cp` 參數再加上 process ID 查看指定行程的處理器親和性(CPU affinity)。已執行結果看來,我可以使用 CPU 0~3。
```
$ taskset -cp 1
pid 1's current affinity list: 0-3
```
接下來使用 boot loader 所提供的自訂開機參數功能,手動加入 `isolcpus=cpu_id` 參數。讓特定的 CPU 核心在開機時就被保留下來,不讓其他的行程干擾我要執行的程式。我先保留 CPU 0。首先切換目錄到 `/etc/default`,然後備份 `grub`。
```shell
$ cd /etc/default
$ sudo cp grub grub-org
```
然後修改 `/etc/default/grub` 中的 `GRUB_CMDLINE_LINUX_DEFAULT` 參數。在此參數後加上 `isolcpus=0` 也就是保留 CPU 0 給特定程式使用。
```
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=0"
```
執行下列指令更新 boot loader 的開機參數。
```shell
$ sudo update-grub
Sourcing file `/etc/default/grub'
Sourcing file `/etc/default/grub.d/init-select.cfg'
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-5.13.0-35-generic
Found initrd image: /boot/initrd.img-5.13.0-35-generic
Found linux image: /boot/vmlinuz-5.13.0-30-generic
Found initrd image: /boot/initrd.img-5.13.0-30-generic
Found memtest86+ image: /boot/memtest86+.elf
Found memtest86+ image: /boot/memtest86+.bin
done
```
先檢查 `/sys/devices/system/cpu/isolated` 的內容,應該要是空的。
```shell
$ cat /sys/devices/system/cpu/isolated
```
然後,重新開機。
```shell
$ sudo reboot
```
待重新開機完成後,在檢查 `/sys/devices/system/cpu/isolated` 的內容。應該要是 0。也就是我剛剛在 `/etc/default/grub` 中用 `isolcpus=0` 保留下來的 CPU。
```shell
$ cat /sys/devices/system/cpu/isolated
0
```
接著就可以用 `taskset` 指定 CPU 來執行程式。在這個作業中,執行 `client` 並透過 `sysfs` 介面操作 `fibdrv` 計算 Fibonacci numbers。
```shell
# taskset -c 0 ./client $PWD fd_fib.time fd_rd.time
```
將這個動作整合到 `Makefile` 中。以簡化後續的實作流程。以下是在 `Makefile` 中新增的兩個 rule。
```Makefile
it_time: client
# This rule must be run as root
echo "iteration" > /sys/kernel/fibonacci/algo
# Bind the executable to CPU 0.
taskset -c 0 ./client $(PWD) it_fib.time it_rd.time
fd_time: client
# This rule must be run as root
echo "fast-doubling" > /sys/kernel/fibonacci/algo
# Bind the executable to CPU 0.
taskset -c 0 ./client $(PWD) fd_fib.time fd_rd.time
```
## 排除干擾效能分析的因素
* 暫時抑制 [address space layout randomization](https://en.wikipedia.org/wiki/Address_space_layout_randomization) (ASLR)
```shell
$ sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
```
* 設定 scaling_governor 為 performance。準備以下 shell script,檔名為 `performance.sh`:
```shell
for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
do
echo performance > ${i}
done
```
然後再用下列執行 `performance.sh`。
```shell
$ sudo sh performance.sh
```
* 針對 Intel 處理器,關閉 turbo mode
```shell
$ sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo"
```
* 將上述這些排除干擾效能分析的因素的動作全部整合進 `Makefile` 以簡化後續實驗流程。
```Makefile
set_env:
# This rule must be run as root
# Suppress address space layout randomization (ASLR)
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
sudo sh performance.sh
# For Intel CPU, disable turbo mode
sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo"
```
* 參考資料: [排除干擾效能分析的因素](https://hackmd.io/@sysprog/linux2022-fibdrv#%E6%8E%92%E9%99%A4%E5%B9%B2%E6%93%BE%E6%95%88%E8%83%BD%E5%88%86%E6%9E%90%E7%9A%84%E5%9B%A0%E7%B4%A0)
---
# 效能分析(內建資料型別)
## 初步觀察
以下列這個 commit 的程式碼搭配本文一開始所描述的環境做實驗。
> commit: ab44371
> branch: perf.built-in-type
:::spoiler fast doubling
```c
static long long fib_fast_doubling_org(long long n)
{
if (n <= 1) {
// base case:
// F(0) = 0
// F(1) = 1
// Performanc measure strat.
kt = ktime_get();
int ret = n;
// Performanc measure stop.
kt = ktime_sub(ktime_get(), kt);
return ret;
}
// Performanc measure strat.
kt = ktime_get();
long long a = 0, b = 1;
unsigned int mask_len = sizeof(unsigned int) * 8 - 1;
for (unsigned int k = 1U << mask_len; k; k >>= 1) {
long long k1 = a * (2 * b - a); // F(2k) = F(k)[2F(k+1) - F(k)]
long long k2 = a * a + b * b; // F(2k+1) = F(k)^2 + F(k+1)^2
if (n & k) {
a = k2; // F(2k+1)
b = k1 + k2; // F(2k+2)
} else {
a = k1; // F(2k)
b = k2; // F(2k+1)
}
}
// Performanc measure stop.
kt = ktime_sub(ktime_get(), kt);
return a;
}
```
:::
執行下列指令畫出 `iteration` 與 `fast doubling` 的效能比較。
```shell
$ make clean plot_clean all
$ make unload load
$ sudo su
$ make plot
```
結果會畫在 `fibdrv_perf.png` 中。

Iteration 演算法的時間複雜度為 O(n),fast doubling 演算法的時間複雜度為 O(logn)。由上圖的實驗結果可看出這個趨勢。
## 設法改善 fast doubling 的效能
由作業說明 ([H03: fibdrv, 費氏數列](https://hackmd.io/@sysprog/linux2020-fibdrv#-%E8%B2%BB%E6%B0%8F%E6%95%B8%E5%88%97)) 中的虛擬碼與示範案例,以及[Calculating Fibonacci Numbers by Fast Doubling](https://chunminchang.github.io/blog/post/calculating-fibonacci-numbers-by-fast-doubling) 的說明可窺見,以 fast doubling 計算 Fibonacci number 可對所欲求取的項數的二進位表示法決定計算步驟。所以可以利用 `gcc` 提供的 `__builtin_clz()` 減少尋找 MSB (most significant bit) 的時間。
> commit: ab44371
> branch: perf.built-in-type
:::spoiler fast doubling with clz
```c
static long long fib_fast_doubling_clz(long long n)
{
if (n <= 1) {
// base case:
// F(0) = 0
// F(1) = 1
// Performanc measure strat.
kt = ktime_get();
int ret = n;
// Performanc measure stop.
kt = ktime_sub(ktime_get(), kt);
return ret;
}
// Performanc measure strat.
kt = ktime_get();
long long a = 0, b = 1;
unsigned int mask_len = sizeof(unsigned int) * 8 - 1 - __builtin_clz(n);
for (unsigned int k = 1U << mask_len; k; k >>= 1) {
long long k1 = a * (2 * b - a); // F(2k) = F(k)[2F(k+1) - F(k)]
long long k2 = a * a + b * b; // F(2k+1) = F(k)^2 + F(k+1)^2
if (n & k) {
a = k2; // F(2k+1)
b = k1 + k2; // F(2k+2)
} else {
a = k1; // F(2k)
b = k2; // F(2k+1)
}
}
// Performanc measure stop.
kt = ktime_sub(ktime_get(), kt);
return a;
}
```
:::

# 效能分析 (自行實作的 big number)
## 初步觀察
以下列這個 commit 的程式碼搭配本文一開始所描述的環境做實驗。
> commit: 2b8c55a
> branch: master
:::spoiler fast doubling (big number)
```c
static void fib_fast_doubling_org(NUM_TYPE k)
{
if (k <= 1) {
answer = bignum_create(BIGNUM_SZ);
// Performanc measure strat.
kt = ktime_get();
answer->num[0] = k;
// Performanc measure stop.
kt = ktime_sub(ktime_get(), kt);
return;
}
bignum *a = bignum_create(BIGNUM_SZ);
answer = a;
bignum *b = bignum_create(BIGNUM_SZ);
bignum *c = bignum_create(BIGNUM_SZ);
bignum *d = bignum_create(BIGNUM_SZ);
bignum *tmp1 = bignum_create(BIGNUM_SZ);
bignum *tmp2 = bignum_create(BIGNUM_SZ);
// Performanc measure strat.
kt = ktime_get();
unsigned int mask_len = sizeof(unsigned int) * 8 - 1;
a->num[0] = 0; // base case, F(0) = 0
b->num[0] = 1; // base case, F(1) = 1
for (unsigned int i = 1U << mask_len; i; i >>= 1) {
bignum_cpy(tmp1, b);
bignum_lshift(tmp1, 1); // tmp1 = 2 * F(k+1)
bignum_sub(tmp2, tmp1, a); // tmp2 = 2 * F(k+1) - F(k)
bignum_mult(c, a, tmp2); // F(2k) = F(k) * [2 * F(k+1) - F(k)]
bignum_mult(tmp1, a, a); // tmp1 = a * a
bignum_mult(tmp2, b, b); // tmp2 = b * b
bignum_add(d, tmp1, tmp2); // F(2k+1) = F(k) * F(k) + F(k+1) * F(k+1)
if (k & i) {
bignum_cpy(a, d);
bignum_add(b, c, d);
} else {
bignum_cpy(a, c);
bignum_cpy(b, d);
}
}
// Performanc measure stop.
kt = ktime_sub(ktime_get(), kt);
bignum_destroy(b);
bignum_destroy(c);
bignum_destroy(d);
bignum_destroy(tmp1);
bignum_destroy(tmp2);
}
```
:::
執行下列指令畫出 `iteration` 與 `fast doubling` 的效能比較。
```shell
$ make clean plot_clean all
$ make unload load
$ sudo su
$ make plot
```
結果會畫在 `fibdrv_perf.png` 中。

## 以 clz 改善以 big number 實作的 fast doubling 效能
> commit: 2b8c55a
> branch: master
:::spoiler fast doubling (big number) with clz
```c
static void fib_fast_doubling_clz(NUM_TYPE k)
{
if (k <= 1) {
answer = bignum_create(BIGNUM_SZ);
// Performanc measure strat.
kt = ktime_get();
answer->num[0] = k;
// Performanc measure stop.
kt = ktime_sub(ktime_get(), kt);
return;
}
bignum *a = bignum_create(BIGNUM_SZ);
answer = a;
bignum *b = bignum_create(BIGNUM_SZ);
bignum *c = bignum_create(BIGNUM_SZ);
bignum *d = bignum_create(BIGNUM_SZ);
bignum *tmp1 = bignum_create(BIGNUM_SZ);
bignum *tmp2 = bignum_create(BIGNUM_SZ);
// Performanc measure strat.
kt = ktime_get();
unsigned int mask_len = sizeof(unsigned int) * 8 - 1 - __builtin_clz(k);
a->num[0] = 0; // base case, F(0) = 0
b->num[0] = 1; // base case, F(1) = 1
for (unsigned int i = 1U << mask_len; i; i >>= 1) {
bignum_cpy(tmp1, b);
bignum_lshift(tmp1, 1); // tmp1 = 2 * F(k+1)
bignum_sub(tmp2, tmp1, a); // tmp2 = 2 * F(k+1) - F(k)
bignum_mult(c, a, tmp2); // F(2k) = F(k) * [2 * F(k+1) - F(k)]
bignum_mult(tmp1, a, a); // tmp1 = a * a
bignum_mult(tmp2, b, b); // tmp2 = b * b
bignum_add(d, tmp1, tmp2); // F(2k+1) = F(k) * F(k) + F(k+1) * F(k+1)
if (k & i) {
bignum_cpy(a, d);
bignum_add(b, c, d);
} else {
bignum_cpy(a, c);
bignum_cpy(b, d);
}
}
// Performanc measure stop.
kt = ktime_sub(ktime_get(), kt);
bignum_destroy(b);
bignum_destroy(c);
bignum_destroy(d);
bignum_destroy(tmp1);
bignum_destroy(tmp2);
}
```
:::

由實驗數據看來,原本的 iteration 演算法隨著項數的增長,所花費的時間以線性關係成長。而 fast doubling 演算法的曲線則是呈現類似對數 (logarithm) 的關係。雖然兩個演算法的時間複雜度與理論吻合,可是fast doubling 所花費的時間卻比 iteration 來的高。以這個趨勢來看,當項數足夠大的時候,iteration 所花費的時間會超越 fast doubling。
不過,已經算到 F~23000~ 了 fast doubling 所花費的時間仍然比 iteration 高。看來我實作的 big numger 還有很大的改善空間。
---
# 自我檢查清單
- [ ] 研讀上述 Linux 效能分析的提示 描述,在自己的實體電腦運作 GNU/Linux,做好必要的設定和準備工作 → 從中也該理解為何不希望在虛擬機器中進行實驗;
- [ ] 研讀上述費氏數列相關材料 (包含論文),摘錄關鍵手法,並思考 clz / ctz 一類的指令對 Fibonacci 數運算的幫助。請列出關鍵程式碼並解說
- [ ] 複習 C 語言 數值系統 和 bitwise operation,思考 Fibonacci 數快速計算演算法的實作中如何減少乘法運算的成本;
- [ ] 研讀 KYG-yaya573142 的報告,指出針對大數運算,有哪些加速運算和縮減記憶體操作成本的舉措?
- [ ] lsmod 的輸出結果有一欄名為 Used by,這是 “each module’s use count and a list of referring modules”,但如何實作出來呢?模組間的相依性和實際使用次數 (reference counting) 在 Linux 核心如何追蹤呢?
>搭配閱讀 The Linux driver implementer’s API guide » Driver Basics
- [ ] 注意到 fibdrv.c 存在著 DEFINE_MUTEX, mutex_trylock, mutex_init, mutex_unlock, mutex_destroy 等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題。嘗試撰寫使用 POSIX Thread 的程式碼來確認。