---
tags: linux2023
---
# 2023q1 Homework1 (fibdrv)
contributed by < [Jerejere0808](https://github.com/Jerejere0808) >
新增以下資料結構使 fibonnaci 可以進行大數運算,其中 capacity 是用於分配多餘的記憶體空間,這樣就可以避免多次的 resize (realloc) , 只有在 capacity 不足時才會需要重新分配記憶體空間。
```c
typedef struct _bn {
unsigned int *number;
unsigned int size;
int sign;
int capacity;
} bn;
```
將計算 fibonnaci 的方法改成 fast doubling,使時間複雜度可以達到 log(n)。
:::warning
注意就算使用大數運算仍會有運算數字大小的限制。因為 kmalloc 會有分配空間的限制,分配超過某個 size 會導致錯誤的情況,而分配空間的限制跟每台電腦的系統配置有關,如果假設最大的分配空間為 4MB , 那麼根據 Binet 公式 , 當 n 為足夠大的正整數時可以推算需要使用的位元數: − 1.160964 + n × 0.694242
當 n = 5761680 , f(n) 就會需要 4MB 來表示,所以當 n > 5761680 就會產生錯誤。如果是以 struct _bn 裡的 unsigned int 陣列來看,Bytes 數量就必須除以 32 加 + 1 ,也就是 unsigned int *number 最多可以分配 125000 個 unsigned int。
:::
```c
void bn_fib_fdoubling(bn *dest, unsigned int n)
{
bn_resize(dest, 1);
if (n < 2) { // Fib(0) = 0, Fib(1) = 1
dest->number[0] = n;
return;
}
bn *f1 = dest; /* F(k) */
bn *f2 = bn_alloc(1); /* F(k+1) */
f1->number[0] = 0;
f2->number[0] = 1;
bn *k1 = bn_alloc(1);
bn *k2 = bn_alloc(1);
/* walk through the digit of n */
for (unsigned int i = 1U << 31; i; i >>= 1) {
/* F(2k) = F(k) * [ 2 * F(k+1) – F(k) ] */
bn_cpy(k1, f2);
bn_lshift(k1, 1);
bn_sub(k1, f1, k1);
bn_mult(k1, f1, k1);
/* F(2k+1) = F(k)^2 + F(k+1)^2 */
bn_mult(f1, f1, f1);
bn_mult(f2, f2, f2);
bn_cpy(k2, f1);
bn_add(k2, f2, k2);
if (n & i) {
bn_cpy(f1, k2);
bn_cpy(f2, k1);
bn_add(f2, k2, f2);
} else {
bn_cpy(f1, k1);
bn_cpy(f2, k2);
}
}
// return f[0]
bn_free(f2);
bn_free(k1);
bn_free(k2);
}
```
可以用以下方法來測量在 kernel 所需多少計算時間
```c
kt = ktime_get();
bn_fib_fdoubling(result, *offset);
kt = ktime_sub(ktime_get(), kt);
```
參考 [colinyoyo26](https://github.com/colinyoyo26/fibdrv) 的方法用 clock_gettime 在 client 取得 user 所需的時間
```c
static inline long long get_nanotime()
{
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return ts.tv_sec * 1e9 + ts.tv_nsec;
}
```
兩者相減可以得到 kernel to user 的時間,如下圖:
![](https://i.imgur.com/x2mliMO.png)
不過我發現所需的時間比預期 log(n) 還多出許多,fib(300) 就需要 50000 ns 經過檢查,發現我把 bn_to_string ,也就是轉化成字串的部分算進去 kernel time 裡面了 ,把這部分的計算時間去掉之後,時間有明顯變短,如下圖所示,可見把 bn 轉成字串是非常耗時間的。
![](https://i.imgur.com/lP8UqIA.png)
再持續修改,原本的 bn_fib_fdoubling 有 bn_cpy 也就是需要複製資料,這樣會需要額外成本,因此透過修改 bn_lshift 使 src 可以直接把左移的結果給 dest 就不用先複製之後又再往左位移。
ex: bn_cpy(k1, f2); bn_lshift(k1, 1); 可以改成 bn_lshift2(f2, 1, k1)
bn_cpy 和 舊的 bn_lshift 如下
```c
int bn_cpy(bn *dest, bn *src)
{
if (bn_resize(dest, src->size) < 0)
return -1;
dest->sign = src->sign;
memcpy(dest->number, src->number, src->size * sizeof(bn_data));
return 0;
}
```
```c
void bn_lshift(bn *src, size_t shift)
{
size_t z = bn_clz(src);
shift %= 64; // only handle shift within 32 bits atm
if (!shift)
return;
if (shift > z)
bn_resize(src, src->size + 1);
for (int i = src->size - 1; i > 0; i--)
src->number[i] =
src->number[i] << shift | src->number[i - 1] >> (64 - shift);
src->number[0] <<= shift;
}
```
新的 bn_lshift 和 fast doubling 實作如下:
```c
void bn_lshift(const bn *src, size_t shift, bn *dest)
{
size_t z = bn_clz(src);
shift %= 32; // only handle shift within 32 bits atm
if (!shift)
return;
if (shift > z) {
bn_resize(dest, src->size + 1);
bn_resize(src, src->size + 1);
} else {
bn_resize(dest, src->size);
}
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] = src->number[0] << shift;
}
```
```c
void bn_fib_fdoubling_nocpy(bn *dest, unsigned int n)
{
bn_resize(dest, 1);
if (n < 2) { // Fib(0) = 0, Fib(1) = 1
dest->number[0] = n;
return;
}
bn *f1 = dest; /* F(k) */
bn *f2 = bn_alloc(1); /* F(k+1) */
f1->number[0] = 0;
f2->number[0] = 1;
bn *k1 = bn_alloc(1);
bn *k2 = bn_alloc(1);
/* walk through the digit of n */
for (unsigned int i = 1U << (31 - __builtin_clz(n)); i; i >>= 1) {
/* F(2k) = F(k) * [ 2 * F(k+1) – F(k) ] */
/* F(2k+1) = F(k)^2 + F(k+1)^2 */
bn_lshift(f2, 1, k1);// k1 = 2 * F(k+1)
bn_sub(k1, f1, k1); // k1 = 2 * F(k+1) – F(k)
bn_mult(k1, f1, k2); // k2 = k1 * f1 = F(2k)
bn_mult(f1, f1, k1); // k1 = F(k)^2
bn_swap(f1, k2); // f1 <-> k2, f1 = F(2k) now
bn_mult(f2, f2, k2); // k2 = F(k+1)^2
bn_add(k1, k2, f2); // f2 = f1^2 + f2^2 = F(2k+1) now
if (n & i) {
bn_swap(f1, f2); // f1 = F(2k+1)
bn_add(f1, f2, f2); // f2 = F(2k+2)
}
}
// return f[0]
bn_free(f2);
bn_free(k1);
bn_free(k2);
}
```
沒有使用 bn_cpy 的執行速度如下圖:
![](https://i.imgur.com/s3pI41A.png)
:::info
在作業的說明中 bn_lshift 原本沒有
```c
bn_resize(src, src->size + 1);
```
但是我發現這樣算出來的f(92)的結果會是錯誤的,然後我取到 f(1000) 發現後面某些值也是錯的,所以單獨檢查 f(92)。
![](https://i.imgur.com/5tafB7E.png)
上圖為當計算到92的最後一個bit時, bn_lshift2(f2, 1, k1) 計算結果是錯的,也就是 printk編號 0 到 1 的時候 k1 應該要變成 f2 的兩倍(2971215073 * 2 = 5,942,430,146) , 但卻變為 1647462850,導致後面一系列的錯誤,所以問題出在bn_lshift 。 shift > z 時 src 也要resize, 不然會導致當 shift > z 為真時,bn_resize(dest, src->size + 1) 之後 dest 比 src 多出 32 bit,也就是一個 number 然後 for (int i = src->size - 1; i > 0; i--) i 是從 src->size - 1 開始,也就是 dest 最後面那個 number 沒有被賦予值,最後結果錯誤。
:::
下面是用 fdoubling 計算 fb(1) 到 fb(1000) 並且用 perf 去做效能量測的結果。
```
7353,7380 cycles
1,1866,8345 instructions # 1.61 insn per cycle
9,4991 cache-references
3,3307 cache-misses # 35.063 % of all cache refs
0.043825063 seconds time elapsed
0.011958000 seconds user
0.031888000 seconds sys
```
下面是用 fdoubling 且避免使用bn_ cpy 計算 fb(1) 到 fb(1000) 並且用 perf 去做效能量測的結果。
```
1745,6179 cycles
2678,2969 instructions # 1.53 insn per cycle
6,4014 cache-references
2,4079 cache-misses # 37.615 % of all cache refs
0.020755088 seconds time elapsed
0.000000000 seconds user
0.020713000 seconds sys
```
根據作業提示再進一步調整將原本的fast doubling 公式
$$
\begin{split}
F(2k) &= F(k)[2F(k+1) - F(k)] \\
F(2k+1) &= F(k)^2+F(k+1)^2
\end{split}
$$
利用Q-Matrix
$$
\begin{split}
F(2k-1) &= F(k)^2+F(k-1)^2 \\
F(2k) &= F(k)[2F(k-1) + F(k)]
\end{split}
$$
:::info
還在研究為何換個公式就可以減少執行時間...
:::
```c
void bn_fib_fdoubling_Q_Matrix(bn *dest, unsigned int n)
{
bn_resize(dest, 1);
if (n < 2) { // Fib(0) = 0, Fib(1) = 1
dest->number[0] = n;
return;
}
bn *f1 = bn_alloc(1); // f1 = F(k-1)
bn *f2 = dest; // f2 = F(k) = dest
f1->number[0] = 0;
f2->number[0] = 1;
bn *k1 = bn_alloc(1);
bn *k2 = bn_alloc(1);
for (unsigned int i = 1U << (30 - __builtin_clz(n)); i; i >>= 1) {
/* F(2k-1) = F(k)^2 + F(k-1)^2 */
/* F(2k) = F(k) * [ 2 * F(k-1) + F(k) ] */
bn_lshift2(f1, 1, k1); // k1 = 2 * F(k-1)
bn_add(k1, f2, k1); // k1 = 2 * F(k-1) + F(k)
bn_mult(k1, f2, k2); // k2 = k1 * f2 = F(2k)
bn_mult(f2, f2, k1); // k1 = F(k)^2
bn_swap(f2, k2); // f2 <-> k2, f2 = F(2k) now
bn_mult(f1, f1, k2); // k2 = F(k-1)^2
bn_add(k2, k1, f1); // f1 = k1 + k2 = F(2k-1) now
if (n & i) {
bn_swap(f1, f2); // f1 = F(2k+1)
bn_add(f1, f2, f2); // f2 = F(2k+2)
}
}
bn_free(f1);
bn_free(k1);
bn_free(k2);
}
```
再用perf測量一次 時間有下降一些
```
1649,2759 cycles
2510,1465 instructions # 1.52 insn per cycle
6,2615 cache-references
2,7358 cache-misses # 43.692 % of all cache refs
0.006684335 seconds time elapsed
0.000000000 seconds user
0.006707000 seconds sys
```
下圖為有 bn_cpy 和 沒有 bn_cpy 的fast doubling 執行時間比較圖,可以看到後者的表現好非常多
![](https://i.imgur.com/qCgEKlO.png)
下圖為沒有 bn_cpy 的 fast doubling 和利用Q-Matrix 且沒有 bn_cpy 的 fast doubling 執行時間比較圖,可以看到後者的時間相對較少。
![](https://i.imgur.com/QG2r6r2.png)
```
7361,9839 cycles
1,1744,5106 instructions # 1.60 insn per cycle
8,7895 cache-references
4,0258 cache-misses # 45.802 % of all cache refs
0.043562180 seconds time elapsed
0.003964000 seconds user
0.039644000 seconds sys
```
```
1564,6279 cycles
2160,7539 instructions # 1.38 insn per cycle
7,3739 cache-references
3,0741 cache-misses # 41.689 % of all cache refs
0.010380267 seconds time elapsed
0.000000000 seconds user
0.010386000 seconds sys
```
```
1421,4606 cycles
1933,1207 instructions # 1.36 insn per cycle
6,7948 cache-references
2,8622 cache-misses # 42.123 % of all cache refs
0.006762957 seconds time elapsed
0.000114000 seconds user
0.006370000 seconds sys
```