# 2022q1 Homework3 (fibdrv)
contributed by < `Korin777` >
## Linux 效能分析
### 限定 CPU 給特定的程式使用
1. 在 `/etc/default/grub` 中新增 `isolcpus` 參數
```
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=1"
```
2. 更新 `/boot/grub/grub.cfg`
```shell
$ sudo update-grub
```
3. 重新開機
4. 檢查是否生效
* 查看 `/proc/cmdline` 裡是否有 `isolcpu` 參數
> korin@korin-VivoBook-15-ASUS-Laptop-X542UF:~$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-5.13.0-30-generic root=UUID=0316da8c-1a94-4511-a7fc-a5133651768b ro quiet splash isolcpus=1 vt.handoff=7
* 最後用 `taskset` 查看 process 的 cpu affinity , 可以發現它已經不能在第一個 cpu 執行了
> korin@korin-VivoBook-15-ASUS-Laptop-X542UF:~$ taskset -p 1
pid 1's current affinity mask: fd
### 排除干擾效能分析的因素
* 抑制 [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
* `cpufreq` 是一個動態調整 cpu 頻率的模組,系統啟動時生成一個資料夾 `/sys/devices/system/cpu/cpu0/cpufreq/`,裡面有幾個檔案,`scalin_governor` 代表 cpu 頻率調整模式,用它來控制 CPU 頻率 , 設為 `performance` 表示將 CPU 頻率固定工作在其支援的最高執行頻率上,而不動態調節
```shell
for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
do
sudo echo performance > ${i}
done
```
* 針對 Intel 處理器,關閉 turbo mode
* Intel CPU 有個 [Turbo Boost](https://zh.wikipedia.org/wiki/%E8%8B%B1%E7%89%B9%E5%B0%94%E7%9D%BF%E9%A2%91%E5%8A%A0%E9%80%9F) 功能,當應用程式需要較高的運算效能時,可以以高於額定頻率執行 , 我們要避免上述情況
```shell
$ sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo"
```
* 經過以上處理後 , 比較計算費式數列對時間的作圖 , 可以發現執行時間比較沒有突然衝很高的情況 , 可以看作每次測量執行時間都比較沒有被外在因素影響
* Before: 沒有排除效能分析干擾 , 沒有限定 CPU 只給程式用
![](https://i.imgur.com/lTu0nYI.png)
* After: 排除效能分析干擾 , 限定 CPU 只給程式用
![](https://i.imgur.com/tNDOkkb.png)
## 費氏數列
* 費氏數列 $F(n$) 可透過 $F(n/2)$ 及 $F(n/2+1)$ 求得 , 而在 $n$ 為奇數及偶數的算法分別如下
* $F(2k)=F(k)[2F(k+1)-F(k)]$
* $F(2k+1)=F(k+1)^2+F(k)^2$
* 當我們有$(a,b) = (F(n/2),F(n/2)+1)$ ,根據上面兩個式子即可算出 $(c,d)=(2 * a * b - a * a,a * a + b * b)$ , 考慮 $n$ 以下情況
* $n$ 為奇數 => $(c,d)=(F(n-1),F(n))$
* $n$ 為偶數=> $(c,d)=(F(n),F(n+1))$
* 可以發現當 $n$ 為奇數 , 我們會得到 $F(n-1)$ 及 $F(n)$ , 因此若要在算出 $F(2n)$ , 我們得再算出 $F(n+1)$ , 而 $n$ 為奇數則變為如下
* $n$ 為奇數 => $(d,c+d)=(F(n),F(n+1))$
* 透過上面式子 , 假設要計算 $F(11)$ , 我們只要透過 $F(5)、F(6) => F(2)、F(3) => F(1)、F(2)$
* 對照 $F(11)$ , 11~10~ = 1011~2~
| i | start | 4 | 3 |2|1| result
| -------- | -------- | -------- | -------- |-------- |-------- |-------- |
| n | - | ==1==011 | 1==0==11 | 10==1==1 | 101==1== | - |
| F(n)| F(0) | F(0*2+1)| F(1*2) |F(2*2+1)|F(5*2+1) |F(11)
| F(n) = a | F(0)=0 |1 |1|5|89|89
| F(n+1) = b | F(1)=1 |1|2|8|144
|F(2n),F(2n+1)|-|奇數(d,c+d)|偶數(c,d)|奇數(d,c+d)|奇數(d,c+d)
* 透過以上公式 , 可以寫出以下程式來計算費伯納西數 `n`
* 可以透過 `63 - __builtin_clzll(n)` 來代替迴圈求出 `n` 要除以 2 多少次才等於 0
```c
unsigned long long fib(unsigned long long n) {
int start = 63 - __builtin_clzll(n);
unsigned long long int bit = 1ULL << (start), a = 0, b = 1,c,d;
while(bit) {
c = 2*a*b - a*a, d = a*a + b*b;
if(n & bit) {
a = d, b = c + d;
}
else {
a = c, b = d;
}
bit >>= 1;
}
return a;
}
```
## 減少乘法運算的成本
* 計算費式數列的過程中 , 可以發現要求出 `c,d` 需要大量的乘法運算
```c
c = 2*a*b - a*a, d = a*a + b*b;
```
* 運用位移的方式將乘法運算改為加法 , 可以實作出一個只需要加減及為移的 `fib` 函式
```c
c = (mul(a,b) << 1ULL) - sqare(a), d = sqare(a) + sqare(b);
```
```c
unsigned long long mul(unsigned long long a, unsigned long long b) {
unsigned long long ans = 0ULL;
for(int i = 0 ; i < 64; ++i) {
if(b & (1ULL << i))
ans += a << i;
}
return ans;
}
unsigned long long sqare(unsigned long long x) {
unsigned long long ans = 0ULL;
for(int i = 0 ; i < 64; ++i) {
if(x & (1ULL << i))
ans += x << i;
}
return ans;
}
```
## 大數運算
### 正確計算 $F(93)$ 以及其後的值
在計算 $F(93)$ 時 , 因為結果超過 64 位元所能表示的數 , 得定義新的資料結構來儲存 , 並改寫 `fib_sequence`
```c
struct BigN{
unsigned long long lower, higher;
};
static struct BigN fib_sequence(long long k)
{
struct BigN f[k + 2];
f[0].lower = f[0].higher = f[1].higher = 0;
f[1].lower = 1;
for (int i = 2; i <= k; i++) {
f[i].lower = f[i - 1].lower + f[i - 2].lower;
f[i].higher = f[i - 1].higher + f[i - 2].higher + (f[i].lower < f[i - 1].lower || f[i].lower < f[i - 2].lower);
}
return f[k];
}
```
* 參考 [KYG-yaya573142 的報告](https://hackmd.io/@KYWeng/rkGdultSU#%E5%A4%A7%E6%95%B8%E9%81%8B%E7%AE%97) , 透過字串的方式 , 將大數以 10 進位的方式印出來
* 將大數以 2 進位的觀點轉成 10 進位 , 假設有一 10 進位數 `num` 初始為 0 , 將大數從高位元到低位元 , 每次將 `num` 乘以 2 且若遇到 1 就將 `num` 加 1 , 最後即可將此數轉為 10 進位
* 對照 40~10~ = 101000~2~
| i | start = 6 | 5 | 4 | 3 | 2 |1|result
| -------- | -------- | --------| -------- | -------- | -------- |-------- |-------- |
| n(2進位)|==1==01000 | 1==0==1000 | 10==1==000 | 101==0==00 | 1010==0==0 |10100==0== | - |
| n(10進位)| 0 | 1 | 2 | 5| 10 | 20 | 40
| 下一個數 | 0*2+1| 1*2|2*2+1|5*2|10*2 |20*2|-
* 前兩層 for 迴圈即是將大數從 2 進位轉成 10 進位
* 最後一層 for 迴圈用來找出 10 進位數的開頭
* `kmalloc` 定義於 `linux/slab.h` , 用來配置核心的記憶體空間
```c
char* displayBigN(struct BigN b) {
char *num = kmalloc(41,GFP_KERNEL);
memset(num,'0',40);
num[40] = '\0';
int carry;
for(unsigned long long i = 1ULL << 63; i; i >>= 1) {
carry = (b.higher & i) > 0;
for(int j = 39; j >= 0; j--) {
num[j] += ((num[j] - '0') + carry);
if(num[j] > '9') {
num[j] -= 10;
carry = 1;
}
else
carry = 0;
}
}
for(unsigned long long i = 1ULL << 63; i; i >>= 1) {
carry = (b.lower & i) > 0;
for(int j = 39; j >= 0; j--) {
num[j] += ((num[j] - '0') + carry);
if(num[j] > '9') {
num[j] -= 10;
carry = 1;
}
else
carry = 0;
}
}
int i = 0;
for(;i < 39 && num[i] == '0';++i);
return &num[i];
}
```
如果我們要用這個資料結構 , 運用 fast doubling 的手法計算費伯那西數 , 我們得另外實做用於此資列結構的 `+`,`-`,`*`,`<< 1`
1. `+`
* 先將 `lower` 相加 , 在將 `higher` 相加並加上 `lower` 的進位
```c
struct BigN* add(struct BigN a, struct BigN b) {
struct BigN *c = kmalloc(sizeof(struct BigN),GFP_KERNEL);
c->lower = a.lower + b.lower;
c->higher = a.higher + b.higher + (c->lower < a.lower || c->lower < b.lower);
return c;
}
```
2. `-`
* 將減數取 2 補數再與被減數相加
```c
struct BigN* sub(struct BigN a, struct BigN b) {
struct BigN *c;
b.lower = ~b.lower+1;
b.higher = ~b.higher + (b.lower == 0);
c = add(a,b);
return c;
}
```
3. `*`
* 先將 `lower` 轉型成 `unsigned __int128` 並相乘 , 避免 overflow , 結果的較低 64 位元即是新的 `lower` , 較高 64 位元進位到 `higher`
* 分別將一數的 `higher` 乘以另一數的 `lower` 並相加 , 在將上之前的進位 , 即為新的 `higher`
* `higher` 乘以 `higher` 因無法儲存於此資料結構 , 暫時不處理
```c
struct BigN* mul(struct BigN a, struct BigN b) {
struct BigN *c = kmalloc(sizeof(struct BigN),GFP_KERNEL);
unsigned __int128 tmp = (unsigned __int128)a.lower * (unsigned __int128)b.lower;
c->lower = (unsigned long long)tmp;
c->higher = a.lower * b.higher + a.higher * b.lower + (unsigned long long)(tmp >> 64);
return c;
}
```
4. `<< 1`
* 先將 `higher` 左移並加上 `lower` 的最高位元 , 在將 `lower` 左移
```c
void lshfit(struct BigN *a) {
a->higher = (a->higher << 1) + (a->lower >> 63 & 1ULL);
a->lower = a->lower << 1;
}
```
* 修改實作 fast doubling 的 `fib`
```c
struct BigN fib(unsigned long long n) {
int start = 63 - __builtin_clzll(n);
unsigned long long int bit = 1ULL << (start);
struct BigN a, b;
struct BigN *v1, *v2, *v3,*c,*d;
a.lower = a.higher = b.higher = 0;
b.lower = 1;
while(bit) {
v1 = mul(a,b);
lshfit(v1);
v2 = mul(a,a);
v3 = mul(b,b);
c = sub(*v1,*v2);
d = add(*v2,*v3);
kfree(v1);
kfree(v2);
kfree(v3);
if(n & bit) {
a = *d, b = *add(*c,*d);
}
else {
a = *c, b = *d;
}
bit >>= 1;
}
return a;
}
```
* 透過這個資料結構 , 我們可以表示 $0$ ~ $2^{128}-1$ , 就可以正確計算到 $F(186) = 332825110087067562321196029789634457848$
### 改寫 BigN 來計算更大的費伯納西數
為了計算出更大的費伯納西數 , 將 `BigN` 的 `higher` 及 `lower` 更改為 `*digits` , 可以根據我們想儲存的數字大小 , 動態配置記憶體 , `size` 則用來表示數字真正用到的 記憶體大小(幾個 unsigned long long)
```c
struct BigN{
unsigned long long *digits;
unsigned long long size;
};
```
* 改寫 `fib_sequence`
* 這裡將 `f[k + 2]` 改為 `f1`,`f2` 及 `tmp` , `f1`,`f2` 用來保留前兩個費伯那西數 , `tmp` 則用來更換新的 `f1`,`f2`
```c
static struct BigN* fib_sequence(unsigned long long k)
{
struct BigN *f1, *f2, *tmp;
f1 = malloc(sizeof(struct BigN));
f2 = malloc(sizeof(struct BigN));
f1->digits = malloc(sizeof(unsigned long long));
f2->digits = malloc(sizeof(unsigned long long));
f1->digits[0] = 0;
f1->size = f2->size = f2->digits[0] = 1;
unsigned long long overflow;
for (int i = 2; i <= k; i++) {
int carry = 0;
for(int j = 0; j < f1->size; j++) {
overflow = f1->digits[j];
f1->digits[j] += f2->digits[j] + carry;
carry = f1->digits[j] < overflow;
}
unsigned long long f = 0;
int inc = 1;
if(f1->size != f2->size) {
overflow = f = f2->digits[f2->size-1];
f += carry;
carry = f < overflow;
++inc;
}
if(carry) {
f1->digits = realloc(f1->digits,sizeof(unsigned long long) *(f1->size+inc));
f1->size += inc;
f1->digits[f1->size-1] = 1;
if(f)
f1->digits[f1->size-2] = f;
}
if(!carry && f) {
f1->digits = realloc(f1->digits,sizeof(unsigned long long) *(f1->size+1));
f1->size += 1;
f1->digits[f1->size-1] = f;
}
tmp = f2;
f2 = f1;
f1 = tmp;
}
if(k==0) {
tmp = f2;
f2 = f1;
f1 = tmp;
}
free(f1->digits);
free(f1);
return f2;
}
```
* 改寫 `displayBigN`
```c
char *displayBigN(struct BigN *b)
{
char *num = kmalloc(sizeof(char) * (b->size * 20) + 1, GFP_KERNEL);
unsigned long long len = sizeof(char) * (b->size * 20) + 1;
memset(num, '0', len);
num[len - 1] = '\0';
int carry;
// convert number from base-2 to base-10
for (int k = b->size - 1; k >= 0; --k) {
for (unsigned long long i = 1ULL << 63; i; i >>= 1) {
carry = (b->digits[k] & i) > 0;
for (int j = len - 2; j >= 0; j--) { // update num
num[j] += ((num[j] - '0') + carry);
if (num[j] > '9') {
num[j] -= 10;
carry = 1;
} else
carry = 0;
}
}
}
// skip leading 0
int i = 0;
for (; i < len - 2 && num[i] == '0'; ++i)
;
char *retnum = kmalloc(sizeof(char) * (len - i), GFP_KERNEL);
memcpy(retnum, &num[i], sizeof(char) * (len - i));
kfree(num);
return retnum;
}
```
1. `+`
```c
struct BigN* add(struct BigN *f1, struct BigN *f2) {
struct BigN *c = malloc(sizeof(struct BigN)), *large, *small;
int carry = 0, j;
if(f1->size > f2->size)
large = f1, small = f2;
else
large = f2, small = f1;
c->digits = malloc(sizeof(unsigned long long)*(large->size+1));
for(j = 0; j < small->size; j++) {
c->digits[j] = small->digits[j] + large->digits[j] + carry;
carry = c->digits[j] < small->digits[j];
}
for(; j < large->size; j++) {
c->digits[j] = large->digits[j] + carry;
carry = c->digits[j] < large->digits[j];
}
c->digits[j] = carry;
c->size = large->size + carry;
return c;
}
```
* 透過 `add` 函式改寫 `fib_sequence`
```c
static struct BigN* fib_sequence(unsigned long long k)
{
struct BigN *f1, *f2, *tmp;
f1 = malloc(sizeof(struct BigN)), f2 = malloc(sizeof(struct BigN));
f1->digits = malloc(sizeof(unsigned long long)), f2->digits = malloc(sizeof(unsigned long long));
f1->digits[0] = 0, f1->size = f2->size = f2->digits[0] = 1;
for(int i = 2; i <= k; i++) {
tmp = add(f1,f2);
free(f1->digits), free(f1);
f1 = f2, f2 = tmp;
}
if(k == 0)
tmp = f2, f2 = f1, f1 = tmp;
free(f1->digits), free(f1);
return f2;
}
```
2. `-`
* 保證 a,b 皆為正數
```c
struct BigN* sub(struct BigN *a, struct BigN *b) {
struct BigN *c, *tmp = malloc(sizeof(struct BigN)), *large, *small;
if(a->size > b->size)
large = a, small = b;
else
large = b, small = a;
tmp->size = large->size, tmp->digits = malloc(sizeof(unsigned long long) * large->size);
// change b to its 2's complement
tmp->digits[0] = ~b->digits[0] + 1;
int i = 1;
for(; i < b->size; ++i)
tmp->digits[i] = ~b->digits[i] + (tmp->digits[i-1] == 0);
for(; i < large->size; ++i)
tmp->digits[i] = ~0ULL;
c = add(a,tmp);
i = large->size - 1;
c->size = 1;
for(; i > 0; --i) {
if(c->digits[i]) {
c->size = i+1;
break;
}
}
free(tmp->digits);
free(tmp);
return c;
}
```
3. `*`
```c
struct BigN* mul(struct BigN *a, struct BigN *b) {
struct BigN *c = malloc(sizeof(struct BigN));
int size = a->size + b->size;
c->digits = malloc(sizeof(unsigned long long)*size);
memset(c->digits,0,sizeof(unsigned long long)*size);
int *carry = malloc(sizeof(int)*size);
memset(carry,0,sizeof(int)*size);
unsigned long long overflow;
for(int i = 0; i < a->size; ++i) {
for(int j = 0; j < b->size; ++j) {
unsigned __int128 tmp = (unsigned __int128)a->digits[i] * (unsigned __int128)b->digits[j];
int index = i+j;
overflow = c->digits[index];
c->digits[index] += (unsigned long long)tmp;
carry[index] += c->digits[index] < overflow;
overflow = c->digits[index+1];
c->digits[index+1] += (unsigned long long)(tmp >> 64);
carry[index+1] += c->digits[index+1] < overflow;
}
}
for(int i = 0; i < size-1; i++) {
overflow = c->digits[i+1];
c->digits[i+1] += carry[i];
carry[i+1] += c->digits[i+1] < overflow;
}
if(c->digits[size-1])
c->size = size;
else
c->size = size -1;
free(carry);
return c;
}
```
4. `<<1`
```c
void lshfit(struct BigN *a) {
int i = a->size - 1;
if(a->digits[i] >> 63 & 1ULL) {
a->size += 1;
a->digits = realloc(a->digits,sizeof(unsigned long long) * (a->size));
a->digits[a->size-1] = 1;
}
for(; i > 0; --i)
a->digits[i] = (a->digits[i] << 1) + (a->digits[i-1] >> 63 & 1ULL);
a->digits[0] <<= 1;
}
```
* 改寫實作 fast doubling 的 `fib`
```c
struct BigN* fib(unsigned long long n) {
struct BigN *a = malloc(sizeof(struct BigN)), *b = malloc(sizeof(struct BigN)), *v1, *v2, *v3, *c, *d;
a->digits = malloc(sizeof(unsigned long long)) , b->digits = malloc(sizeof(unsigned long long));
a->digits[0] = 0;
a->size = b->size = b->digits[0] = 1;
if(n == 0) {
free(b->digits), free(b);
return a;
}
int start = 63 - __builtin_clzll(n);
unsigned long long int bit = 1ULL << start;
while(bit) {
v1 = mul(a,b);
lshfit(v1);
v2 = mul(a,a);
v3 = mul(b,b);
c = sub(v1,v2);
d = add(v2,v3);
free(v1->digits), free(v1), free(v2->digits), free(v2), free(v3->digits), free(v3),
free(a->digits), free(a), free(b->digits), free(b);
if(n & bit)
a = d, b = add(c,d);
else
a = c, b = d;
bit >>= 1;
}
free(b->digits), free(b);
return a;
}
```
* 根據 [Other Built-in Functions Provided by GCC](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) , `__builtin_clzll` 傳 0 進去為 undefined behavior 所以得特別判斷 `n` 是否為 0
透過這個改寫過的 `BigN` , `fib_sequence` 及 `fib` 至少能正確計算到 $F(31200)$
### fast doubling 及 sequence 執行時間
* 在 kernel space 測量時間
* 透過 gnuplot 觀察到 sequence 手法執行時間隨著 n 的增大線性成長 , 而 fast doubling 手法為非線性緩慢上升
![](https://i.imgur.com/wbJxQRF.png)
### 與 [bignum](https://github.com/0xff07/bignum/tree/fibdrv) 實做比較
* 在 user space 進行 fast doubling 的時間測量
* 在用 gnuplot 繪圖時 , 發現還是有許多突然衝高的點 , 後來透過每個 $F(n)$ 都測量 100 次 , 並取其中最小值後畫出來的圖比較沒有上述情況了
* `bignum` 的實做比我的實做還要快 3 ~ 4 倍
![](https://i.imgur.com/YHtWNBs.png)
## 回傳核心計算的值給使用者
在費伯那西數大於 `ssize_t` 的情況下 , 使用者就無法透過 `read` 的回傳值取得它 , 因此在進行大數運算時,我們得透過 `copy_to_user` 的方式 , 將大數傳給使用者
* `unsigned long copy_to_user(void *to, const void *from, unsigned long n)`
* `to`:目標地址(使用者空間)
* `from`:源地址(核心空間)
* `n`:將要拷貝資料的位元組數
```c
static ssize_t fib_read(struct file *file,char *buf,size_t size,loff_t *offset)
{
int ret = 0;
struct BigN *n = fib(*offset);
char *num = displayBigN(n);
ret = copy_to_user(buf,num,strlen(num)+1);
kfree(n), kfree(num);
return (ssize_t) ret;
}
```
## 檢查費氏數列正確性
透過 `make check` 我們能檢查我們所求的費氏數列是否正確 , 並將發生錯誤的地方給印出來
1. 將 MakeFile `@diff -u out scripts/expected.txt && $(call pass)` 註解 , `expected.txt` 裡的 $fib(93)$ ~ $fib(100)$ 是錯誤的並非我們想要的值
2. 跟據想計算到費伯那西數修改 `fibdrc.c` 的 `MAX_LENG` 、 `client.c` 的 `offset` 及 `verify.py` 的 `range(2, 101)`
```
korin@korin-VivoBook-15-ASUS-Laptop-X542UF:~/Desktop/linux2022/fibdrv$ make check
make -C /lib/modules/5.13.0-30-generic/build M=/home/korin/Desktop/linux2022/fibdrv modules
make[1]: Entering directory '/usr/src/linux-headers-5.13.0-30-generic'
make[1]: Leaving directory '/usr/src/linux-headers-5.13.0-30-generic'
make unload
make[1]: Entering directory '/home/korin/Desktop/linux2022/fibdrv'
sudo rmmod fibdrv || true >/dev/null
rmmod: ERROR: Module fibdrv is not currently loaded
make[1]: Leaving directory '/home/korin/Desktop/linux2022/fibdrv'
make load
make[1]: Entering directory '/home/korin/Desktop/linux2022/fibdrv'
sudo insmod fibdrv.ko
make[1]: Leaving directory '/home/korin/Desktop/linux2022/fibdrv'
sudo ./client > out
make unload
make[1]: Entering directory '/home/korin/Desktop/linux2022/fibdrv'
sudo rmmod fibdrv || true >/dev/null
make[1]: Leaving directory '/home/korin/Desktop/linux2022/fibdrv'
# @diff -u out scripts/expected.txt && env printf "\e[32;01m Passed [-]\e[0m\n"
f(187) fail
input: 198239973509362327032045173661212819077
expected: 538522340430300790495419781092981030533
```
#### 參考資料
[CPU 優化建議使用 cpupower 設定 CPU Performance 模式](https://www.gushiciku.cn/pl/2iRh/zh-tw)
[isolcpu參數 隔離cpu使其不被自動調度(linux 修改boot參數)](https://www.twblogs.net/a/5d3f74ccbd9eee5174229c27)