# 2023q1 Homework3 (fibdrv)
contributed by < [`ericlai1021`](https://github.com/ericlai1021/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
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz
CPU family: 6
Model: 142
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
Stepping: 10
CPU max MHz: 3400.0000
CPU min MHz: 400.0000
BogoMIPS: 3600.00
Virtualization features:
Virtualization: VT-x
Caches (sum of all):
L1d: 128 KiB (4 instances)
L1i: 128 KiB (4 instances)
L2: 1 MiB (4 instances)
L3: 6 MiB (1 instance)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0-7
$ uname -r
5.19.0-35-generic
```
## 前期準備
* 取得[原始程式碼](https://github.com/ericlai1021/fibdrv)後,進行編譯及測試
* 觀察產生的 `fibdrv.ko` 核心模組
```shell
$ modinfo fibdrv.ko
```
得到以下輸出
```shell
filename: /home/eric/linux2023/fibdrv/fibdrv.ko
version: 0.1
description: Fibonacci engine driver
author: National Cheng Kung University, Taiwan
license: Dual MIT/GPL
srcversion: 9A01E3671A116ADA9F2BB0A
depends:
retpoline: Y
name: fibdrv
vermagic: 5.19.0-35-generic SMP preempt mod_unload modversions
```
* 觀察 `fibdrv.ko` 核心模組在 Linux 核心掛載後的行為
模組載入核心:
```shell
$ sudo insmod fibdrv.ko
```
* 查看 `/dev` 目錄下可以發現的確有一個名為 `fibonacci` 的 character device
```shell
$ ls -l /dev/fibonacci
crw------- 1 root root 509, 0 三 16 13:44 /dev/fibonacci
```
繼續觀察
```shell
$ cat /sys/class/fibonacci/fibonacci/dev
509:0
```
可以注意到 `509` 這個數字,不同電腦設備會產生不同數字,試著對照 [`fibdrv.c`](https://github.com/ericlai1021/fibdrv/blob/master/fibdrv.c) ,發現關鍵在 alloc_chrdev_region() , 此函式會向 linux kernel 申請設備號。
```shell
$ cat /sys/module/fibdrv/version
0.1
```
這和 [`fibdrv.c`](https://github.com/ericlai1021/fibdrv/blob/master/fibdrv.c) 透過 `MODULE_VERSION` 所指定的版本號碼相同。
```shell
$ lsmod | grep fibdrv
fibdrv 16384 0
$ cat /sys/module/fibdrv/refcnt
0
```
這兩道命令的輸出都是 0,意味著目前的 [reference counting](https://en.wikipedia.org/wiki/Reference_counting)。
* 掛載 `fibdrv.ko` 核心模組後,執行 `client` 並觀察輸出
```shell
$ sudo ./cilent
Reading from /dev/fibonacci at offset 0, returned the sequence 0.
Reading from /dev/fibonacci at offset 1, returned the sequence 1.
Reading from /dev/fibonacci at offset 2, returned the sequence 1.
...
Reading from /dev/fibonacci at offset 92, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 93, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 94, returned the sequence 7540113804746346429.
```
可以發現在 offset 大於 92 之後都會得到同樣的值,猜測是因為 overflow ,於是先去查看 [`client.c`](https://github.com/ericlai1021/fibdrv/blob/master/client.c) ,基本上該檔案只是對 `/dev/fibonacci` 此裝置做開檔、讀檔、寫檔的動作,所以 fibonacci sequence 的計算都在 [`fibdrv.c`](https://github.com/ericlai1021/fibdrv/blob/master/fibdrv.c) 檔案內,馬上去查看,發現以下程式碼
```c
/* MAX_LENGTH is set to 92 because
* ssize_t can't fit the number > 92
*/
#define MAX_LENGTH 92
```
再往下看到 `fib_device_lseek()` ,此函式中會判斷欲找尋的位置是否大於 `MAX_LENGTH` ,若大於則回傳 `MAX_LENGTH` ,因此要讀取大於 F(92) 的數值都會讀到 F(92)。
## Linux 效能分析
參考 [作業說明 (三)](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-c) 當中的設定
### 限定 CPU 給特定的程式使用
使用 `isolcpus` 這個 Linux 核心起始參數,修改 `/etc/default/grub` 設定檔,在 `GRUB_CMDLINE_LINUX_DEFAULT` 欄位加入 `isolcpus=0` 使開機時保留第 0 個 CPU 核給那些被 [taskset](https://man7.org/linux/man-pages/man1/taskset.1.html) 特別指定的行程使用。
```shell
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=0"
```
修改完輸入 `sudo update-grub` 以更新設定,重新開機後確認設定是否生效。
```shell
$ cat /sys/devices/system/cpu/isolated
0
$ taskset -cp 1
pid 1's current affinity list: 1-7
```
可以發現第 0 個 CPU 確實被保留下來,再使用 `taskset` 查看 PID=1 的行程的處理器親和性,此時已經不包含第 0 個 CPU。
### 時間量測
使用 [ktimer 相關的 API](https://www.kernel.org/doc/html/latest/core-api/timekeeping.html) 來量測時間。 改寫 `fibdrv.c` 當中的 `fib_read()` 和 `fib_write()` 兩個函式。
首先宣告一個 `ktime_t` 的變數:
```c
static ktime_t kt;
```
針對 `fib_read()` 的修改如下:
```c
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;
}
/* calculate the fibonacci number at given offset */
static ssize_t fib_read(struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
return (ssize_t) fib_time_proxy(*offset);
}
```
針對 `fib_write()` 的修改如下:
```c
/* write operation is skipped */
static ssize_t fib_write(struct file *file,
const char *buf,
size_t size,
loff_t *offset)
{
return ktime_to_ns(kt);
}
```
我們可發現到 write 在此核心模組暫時沒作用,所以可以用來輸出上一次執行 fib_sequence() 的時間。
在 `client.c` 中先用 lseek(fd, i, SEEK_SET) 將檔案的讀寫位置指向特定位置 (即第 i 個位置),接著再用 read() 取得 Fib(i) 的數值,最後再使用 write() 取得計算 Fib(i) 的時間,相對應的程式碼如下:
```c
for (int i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
sz = read(fd, buf, 1);
sz = write(fd, write_buf, strlen(write_buf));
}
```
## 改進費氏數列
### Fast Doubling
參考作業說明 [(一)](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-a#-%E8%B2%BB%E6%B0%8F%E6%95%B8%E5%88%97) 、 [(四)](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-d) 以迭代方式實作 bottom-up Fast Doubling。
使用 Fast Doubling 可以得出以下公式
$$
\begin{split}
F(2k) &= F(k)[2F(k+1) - F(k)] \\
F(2k+1) &= F(k+1)^2+F(k)^2
\end{split}
$$
對應的虛擬碼如下:
```python
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;
```
搭配 bitwise 操作求得 `number of binary digit in n` 以及判斷 `current binary digit` 是否為 1。
* 首先討論如何判斷當前位元是否為 1,想法很簡單,就是將 1 往左位移 i 個位元再跟 n 做 AND 運算即可求得 n 的第 i 位元是否為 1。
對應的程式碼如下:
```c
...
if (n & (1 << i)) {
...
}
...
```
* 接著來討論如何求得 n 的二進制位數,這邊可以選擇是否搭配 [clz / ctz](https://en.wikipedia.org/wiki/Find_first_set) 一類的指令來實作。
* 使用 clz 指令取得 n 的 leading 0-bits 數量
因為 n 的型態為 `long long` ,所以首先將 `sizeof(long long)` 向左位移 3 個位元即可求得 `long long` 所需的位元數,接著再減去 n 的 leading 0-bits 數量就可以求得 n 的二進制位數。
對應的程式碼如下:
```c
/* calculate the number of binary digits in k using clz */
int nbits = (sizeof(n) << 3) - __builtin_clzll(n);
```
* 相反的,如果不使用 clz 指令的話,我的作法是簡單的透過一個迴圈產生跟 clz 指令同樣的結果
想法是從 n 的 MSB 往下逐一判斷並搭配變數 count 表示 n 的 leading 0-bits 數,若當前位元為 1 則跳出迴圈,否則讓 `count++` , 最後 `(sizeof(long long) << 3) - count` 即為 n 的 leading 0-bits 數量。
對應的程式碼如下:
```c
/* calculate the number of binary digits in k without using clz */
int nbits = 0;
int llsize = sizeof(n) << 3;
for (int i = llsize - 1; i >= 0; i--) {
if (n >> i)
break;
nbits++;
}
nbits = llsize - nbits;
```
### 實驗結果 (尚未排除干擾效能分析的因素)
* 比較使用遞迴方法與 Fast Doubling (搭配 clz 指令)計算費氏數列的時間
![](https://i.imgur.com/s3MmBTz.png)
可以發現 Fast Doubling 在計算 Fib(0) 時會花費大量時間,猜測是跟使用 clz 指令有關,目前正在做實驗研究,若我們將 y 軸範圍縮小到 2500 ns 會得到以下結果
![](https://i.imgur.com/UV7AQ6d.png)
即便尚未排除干擾效能的因素,仍可以看到 Fast Doubling 明顯優於遞迴方式
* 目前對於 Fast Doubling 計算 Fib(0) 花費大量時間的解法為在 `fib_sequence()` 的一開始加上提前中止的條件處理
```c
if (k <= 0)
return k;
```
### 排除干擾效能分析的因素
* 抑制 [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
```
之後再用 `$ sudo sh performance.sh` 執行
* 針對 Intel 處理器,關閉 turbo mode
```shell
$ sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo"
```
* 關閉 [SMT](https://en.wikipedia.org/wiki/Simultaneous_multithreading) (Intel 稱它為 [Hyper-Threading](https://en.wikipedia.org/wiki/Hyper-threading))
```shell
$ sudo sh -c "echo off > /sys/devices/system/cpu/smt/control"
```
### 實驗結果 (排除干擾效能分析的因素)
* 比較使用遞迴方法與 Fast Doubling (搭配 clz 指令)計算費氏數列的時間
![](https://i.imgur.com/4iO9zMM.png)
可以看出來兩種方式的曲線都明顯平滑許多
* 比較在 Fast Doubling 上有無使用 clz 指令的差別
![](https://i.imgur.com/52OBe8J.png)
可以看到使用 clz 指令對計算費氏數列有顯著的改善。
## 計算 Fib(92) 之後的 Fibonacci 數
> 參考作業說明 [(四)](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-d#%E5%88%9D%E6%AD%A5%E6%94%AF%E6%8F%B4%E5%A4%A7%E6%95%B8%E9%81%8B%E7%AE%97) 初步支援大數運算
### 支援大數運算結構體
為了能正確計算 92 項以後的費氏數列,引入長度可變動的數值表示法,藉由動態配置不同大小記憶體來呈現更大範圍的整數,結構體的定義如下
值得注意的地方是,我透過觀察發現即便是使用 [fast doubling](https://hackmd.io/@ericlai1021/linux2023q1-fibdrv#Fast-Doubling) 在計算費氏數列的過程中,所有會用到自定義結構體 `bn` 宣告的變數 (e.g., `a`, `b`, `t1`, `t2` ) 皆為非負整數,所以我就沒有定義 `sign` 來表示數值的正負。
```c
/*
* number[size - 1] = msb, number[0] = lsb
* sign = 1 for negative number
*/
typedef struct _bn {
unsigned int *number;
unsigned int size;
} bn;
```
* `number` : 指向無號整數陣列的開頭
* `size` : 配置給 number array 的大小,單位為 `sizeof(unsigned int)`
### iterative 實作方法
初步嘗試使用大數運算來實作迭代作法的費氏數列計算,首先會需要加入以下功能
1. `bn_to_string` ,將 `bn` 型態變數轉換成字串
2. `bn_add` ,將兩個 `bn` 型態變數相加
3. `bn_cpy` ,將一個 `bn` 型態變數複製到另一個 `bn` 型態變數(即做 assign 的動作)
在講解以上功能之前要先介紹支援 `bn` 型態的記憶體配置方法以及釋放方法,這邊為了能更加彈性的使用 `bn` 結構體,因此皆採用動態配置記憶體的方式來使用 `bn` 結構體
### bn_alloc
先配置空間給 `bn` 結構體,再配置 `sizeof(unsigned int) * size` 的大小給 `number` 指標變數,最後將所有數值初始化為 0
參考教材 [kmalloc vs. vmalloc](https://hackmd.io/@sysprog/linux-memory#kmalloc-vs-vmalloc) 及網路材料 [difference between kmalloc and vmalloc](https://www.emblogic.com/blog/10/difference-between-kmalloc-and-vmalloc/) 後選擇使用 kmalloc 來配置記憶體是因為 kmalloc 會配置實體連續的記憶體空間,接著就可以簡單的使用 `memset` 來初始化此空間,但當沒有足夠的實體記憶體空間時 kmalloc 就會回傳錯誤,通常是回傳 `NULL`
```c
/*
* allocate a bn structure with the given size
* the value is initialized to 0
*/
bn *bn_alloc(size_t size)
{
bn *new = (bn *) kmalloc(sizeof(bn), GFP_KERNEL);
new->number =
(unsigned int *) kmalloc(sizeof(unsigned int) * size, GFP_KERNEL);
// QUESTION? physical or virtual contiguous memory allocation
memset(new->number, 0, sizeof(unsigned int) * size);
new->size = size;
return new;
}
```
### bn_free
依序將 `number` 以及 `bn` 結構體釋放
```c
/*
* free entire bn data structure
* return 0 on success, -1 on error
*/
int bn_free(bn *src)
{
if (src == NULL)
return -1;
kfree(src->number);
kfree(src);
return 0;
}
```
### bn_to_string
由於大數沒辦法直接以數值的形式列出,這裡改用字串來呈現,轉換的概念很簡單,就是將大數除以 10 取餘數取得個位數,接著再將大數更新為商數,不斷重複此步驟直到大數為 0
```c
memset(s, '0', len - 1);
s[len - 1] = '\0';
int cur = len - 2;
while (!bn_is_zero(src)) {
unsigned long long remainder = 0U;
// src->number[size - 1] is msb
for (int i = src->size - 1; i >= 0; i--) {
unsigned int quotient = 0U;
// walk through every bit of bn
for (unsigned int d = 1U << 31; d > 0; d >>= 1) {
if (src->number[i] & d) {
remainder = (remainder << 1) | 1U;
} else {
remainder = (remainder << 1) | 0U;
}
quotient = (quotient << 1) | (remainder / 10U);
remainder %= 10;
}
src->number[i] = quotient;
}
s[cur] += remainder;
cur--;
}
```
### bn_add
實作加法的概念很直觀,就是從 lsb 開始相加,將進位的部份加到下一輪的相加結果
```c
/* c = a + b */
void bn_add(const bn *a, const bn *b, bn *c)
{
bn_resize(c, MAX(a->size, b->size) + 1);
unsigned long long carry = 0;
for (int i = 0; i < c->size; i++) {
unsigned int tmp1 = (i < a->size) ? a->number[i] : 0;
unsigned int tmp2 = (i < b->size) ? b->number[i] : 0;
carry += (unsigned long long) tmp1 + tmp2;
c->number[i] = carry & 0x00000000FFFFFFFF;
carry >>= 32;
}
if (!c->number[c->size - 1] && c->size > 1)
bn_resize(c, c->size - 1);
}
```
### bn_cpy
使用 `memcpy` 將一個 `bn` 結構體的數值部份複製到另一個 `bn` 結構體的數值部份
```c
/*
* copy the value from src to dest
* return 0 on success, -1 on error
*/
int bn_cpy(bn *dest, const bn *src)
{
if (bn_resize(dest, src->size) < 0)
return -1;
memcpy(dest->number, src->number, src->size * sizeof(unsigned int));
return 0;
}
```
### fast doubling 實作方法
使用大數運算實作 fast doubling 會需要額外實作以下功能
1. `bn_sub` ,將兩個 `bn` 型態變數相減
2. `bn_lshift` ,將 `bn` 型態變數向左位移
3. `bn_mult` ,將兩個 `bn` 型態變數相乘
### bn_sub
實作減法的概念類似加法,一樣從 lsb 開始相減,若相減結果為負數,則向下一位借位
```c
/* c = a - b */
void bn_sub(const bn *a, const bn *b, bn *c)
{
bn_resize(c, MAX(a->size, b->size));
long long borrow = 0;
for (int i = 0; i < c->size; i++) {
unsigned int tmp1 = (i < a->size) ? a->number[i] : 0;
unsigned int tmp2 = (i < b->size) ? b->number[i] : 0;
borrow = (long long) tmp1 - tmp2 - borrow;
if (borrow < 0) {
c->number[i] = borrow + (1LL << 32);
borrow = 1;
} else {
c->number[i] = borrow;
borrow = 0;
}
}
int clz_ints = bn_clz(c) / 32;
if (clz_ints == c->size)
--clz_ints;
bn_resize(c, c->size - clz_ints);
}
```
### bn_shift
對 `bn` 結構體左移 `shift` 個 bit ,因為左移超過 32 bits 的處理相對複雜,因此就先做出比較簡單的版本
```c
/* dest = src << shift */
void bn_lshift(bn *dest, bn *src, size_t shift)
{
size_t z = bn_clz(src);
shift %= 32; // only handle shift within 32 bits
if (!shift)
return;
if (shift > z)
bn_resize(src, src->size + 1);
bn_cpy(dest, src);
/* bit shift */
for (int i = src->size - 1; i > 0; i--)
dest->number[i] =
src->number[i] << shift | src->number[i - 1] >> (32 - shift);
dest->number[0] <<= shift;
}
```
### bn_mult
實作大數乘法的概念類似硬體乘法器 (如下圖),從 `multiplier` 的 lsb 開始判斷,若當前 bit 為 1 ,則將 `multiplicand` 加至 `product` ,再將 `product` 往左位移 1 bit ,重複上述直到 `multiplier` 的判斷到達 msb 為止
![](https://hackmd.io/_uploads/ryma_4AN3.png)
```c
/* c = a * b */
bn *tmp1 = bn_alloc(1);
bn_cpy(tmp1, a);
if (b->number[0] & 1U)
bn_add(c, tmp1, c);
for (int i = 1; i < bn_digit(b); i++) {
bn_lshift(tmp1, tmp1, 1);
if (b->number[i / 32] & (1U << (i % 32)))
bn_add(c, tmp1, c);
}
bn_free(tmp1);
int clz_ints = bn_clz(c) / 32;
if (clz_ints == c->size)
--clz_ints;
bn_resize(c, c->size - clz_ints);
```
### 正確計算 $F(92)$ 以後的數值
使用以上實作出來的大數 API 來正確計算 92 項以後的費氏數列,程式邏輯皆參照原始的實作,首先展示迭代演算法
```c
void bn_fib(bn *dest, unsigned int n)
{
bn_resize(dest, 1);
if (n < 2) { // Fib(0) = 0, Fib(1) = 1
dest->number[0] = n;
return;
}
bn *a = bn_alloc(1);
bn *b = bn_alloc(1);
dest->number[0] = 1;
for (unsigned int i = 1; i < n; i++) {
bn_cpy(b, dest);
bn_add(dest, a, dest);
bn_cpy(a, b);
}
bn_free(a);
bn_free(b);
}
```
接著是 fast doubling 演算法的實作
```c
void bn_fib_fast(bn *dest, unsigned int n)
{
bn_resize(dest, 1);
if (n < 2) { // Fib(0) = 0, Fib(1) = 1
dest->number[0] = n;
return;
}
int nbits = (sizeof(n) << 3) - __builtin_clz(n);
bn *a = bn_alloc(1);
a->number[0] = 0U;
bn *b = bn_alloc(1);
b->number[0] = 1U;
for (int i = nbits - 1; i >= 0; i--) {
bn *t1 = bn_alloc(1);
bn_lshift(t1, b, 1);
bn_sub(t1, a, t1);
bn_mult(a, t1, t1);
bn *t2 = bn_alloc(1);
bn_mult(b, b, b);
bn_mult(a, a, a);
bn_add(a, b, t2);
bn_cpy(a, t1);
bn_cpy(b, t2);
if (n & (1U << i)) {
bn_add(a, b, t1);
bn_cpy(a, b);
bn_cpy(b, t1);
}
bn_free(t1);
bn_free(t2);
}
bn_cpy(dest, a);
bn_free(a);
bn_free(b);
}
```
使用 [/scripts/verify.py](https://github.com/ericlai1021/fibdrv/blob/master/scripts/verify.py) 來驗證,至少能夠正確計算至第 3000 項
## 改善大數運算
後續改進皆更新在 [2023期末專題](https://hackmd.io/@sysprog/HyyMNz7S3)