---
tags: linux2022
---
# sysprog21/bignum 的研究和改進
contributed by <`curlyw819` >
## 實驗環境
```
$ gcc --version
gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0
$ lscpu
架構: x86_64
CPU 作業模式: 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
每核心執行緒數: 2
每通訊端核心數: 6
Socket(s): 1
NUMA 節點: 1
供應商識別號: GenuineIntel
CPU 家族: 6
型號: 158
Model name: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
製程: 10
CPU MHz: 3200.000
CPU max MHz: 4600.0000
CPU min MHz: 800.0000
虛擬: 6399.96
Virtualization: VT-x
L1d cache: 192 KiB
L1i cache: 192 KiB
L2 cache: 1.5 MiB
L3 cache: 12 MiB
NUMA node0 CPU(s): 0-11
```
## 作業要求
對比 [`sysprog21/bignum`](https://github.com/sysprog21/bignum) 和自己撰寫的大數運算[2022q1 Homework3(fibdrv)](https://hackmd.io/T9OVVGNcRRS0o0lmfz2ZCA),同樣在求 `Fibonacci` 數,分析運算速度、記憶體開銷,和潛在的錯誤,並解釋何以有這些落差。研讀 `sysprog21/bignum`,落實原始程式碼中標注的待做事項。
## 開發進度
由於我的 `fibnacci` 運算沒有使用到 `fastdoubling` 與 `clz` 加速,原本在前提不相同的情況下是無法與使用了 `fastdoubling` 與 `clz` 加速過的 `sysprog21/bignum` 去做比對。因此這部分的研究重點會放在重新理解 `fastdoubling` 是如何加速,並還是與自己的程式去做速度上的比對。
![](https://i.imgur.com/OH2jV5o.png)
* 從計算費式數列的比較圖中可以發現,在線的趨勢上就有明顯的落差,自己的程式隨著計算量的增加,時間耗費也明顯增長。
### `sysprog21/bignum` 程式分析
* 在四則運算的部分,`sysprog21/bignum` 採用高階 `(bn)` 與低階 `(amp)` 的 API 包裝而成
* 在費式數列的計算上,原本使用 `iterative` 的方式計算費式數列 ,迴圈迭代次數與欲求費式數成正比,時間複雜度爲 **O(n)**。使用 `fast doubling` ,由於是 bit 的二進制運算,至多只要迭代 64(或32,依設定有所不同)次,實際上去除 MSB 為 0 的 bit 不用做迭代,時間複雜度為 **O(log n)**。 `sysprog21/bignum` 的 `fast doubling` 使用了不同的 `Q-matrix`(參考自[KYG-yaya573142 的報告](https://hackmd.io/@KYWeng/rkGdultSU#%E5%8E%9F%E6%9C%AC%E7%9A%84%E9%81%8B%E7%AE%97%E6%88%90%E6%9C%AC)),推導如下
$$
\begin{split}
\begin{bmatrix}
F(2n-1) \\
F(2n)
\end{bmatrix} &=
\begin{bmatrix}
0 & 1 \\
1 & 1
\end{bmatrix}^{2n}
\begin{bmatrix}
F(0) \\
F(1)
\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)^2 + F(n-1)^2\\
F(n)F(n) + F(n)F(n-1)
\end{bmatrix}
\end{split}
$$
整理後可得
$$
\begin{split}
F(2k-1) &= F(k)^2+F(k-1)^2 \\
F(2k) &= F(k)[2F(k-1) + F(k)]
\end{split}
$$
使用上述公式改寫 `fdoubling` 函式,優點是可以少掉一次迴圈的計算及避免使用減法
* 引入 `bn_sqr` (參考自[KYG-yaya573142 的報告](https://hackmd.io/@KYWeng/rkGdultSU#%E5%8E%9F%E6%9C%AC%E7%9A%84%E9%81%8B%E7%AE%97%E6%88%90%E6%9C%AC))
```
a b c
x a b c
-------------------
ac bc cc
ab bb bc
aa ab ac
```
考慮上述 $abc^2$ 的計算過程,會發現數值 $ab$ 、 $ac$ 與 $bc$ 各會重複一次,因此可先計算對角線其中一邊的數值,將數值的總和直接乘二,最終再加上對角線上的 $aa$ 、 $bb$ 與 $cc$。藉由這種方式,平方運算的成本可由本來的 $n^2$ 次乘法降為 $(n^2 - n)/2$ 次乘法
* `clz / ctz`
由於除 2 可以看作是進行 `binary code` 右移,結合 `clz / ctz` 的指令搭配 `fastdoubling` (先去除數字 MSB 起算的開頭 0 位元,因為真正的數字不包含 leading 0s),最後呈現出 [sysprog21/bignum](https://github.com/sysprog21/bignum/blob/master/fibonacci.c) 計算費式數列的實作
```c
bn *a1 = fib; /* Use output param fib as a1 */
bn_t a0, tmp, a;
bn_init_u32(a0, 0); /* a0 = 0 */
bn_set_u32(a1, 1); /* a1 = 1 */
bn_init(tmp); /* tmp = 0 */
bn_init(a);
/* Start at second-highest bit set. */
for (uint64_t k = ((uint64_t) 1) << (62 - __builtin_clzll(n)); k; k >>= 1) {
/* Both ways use two squares, two adds, one multipy and one shift. */
bn_lshift(a0, 1, a); /* a03 = a0 * 2 */
bn_add(a, a1, a); /* ... + a1 */
bn_sqr(a1, tmp); /* tmp = a1^2 */
bn_sqr(a0, a0); /* a0 = a0 * a0 */
bn_add(a0, tmp, a0); /* ... + a1 * a1 */
bn_mul(a1, a, a1); /* a1 = a1 * a */
if (k & n) {
bn_swap(a1, a0); /* a1 <-> a0 */
bn_add(a0, a1, a1); /* a1 += a0 */
}
}
/* Now a1 (alias of output parameter fib) = F[n] */
```
* 使用 `Karatsuba algorithm` 來加速乘法與平方運算
計算費式數列時,乘法與平方由於運算較複雜,占用程式執行時間的比例也最高,因此`sysprog21/bignum` 使用 `Karatsuba algorithm` 來加速乘法與平方運算。對 `sysprog21/bignum` 使用 `perf` 進行分析如下(只擷取部分重點資訊)
```
$ sudo perf report --stdio -g graph,0.5,caller
# To display the perf.data header info, please use --header/--header-only options.
#
#
# Total Lost Samples: 0
#
# Samples: 5K of event 'cycles'
# Event count (approx.): 7673070456
#
# Children Self Command Shared Object Symbol >
# ........ ........ ......... ................. ..............................>
#
75.85% 2.07% fibonacci fibonacci [.] main
|
|--73.78%--main
| |
| |--26.61%--bn_sqr
| | |
| | |--9.77%--_apm_mul_base
| | | |
| | | |--2.89%--apm_dmul
| | | |
| | | --2.13%--apm_dmul_add
| | |
| | |--3.40%--__GI___libc_realloc (inlined)
| | | |
| | | --2.85%--_int_realloc
| | | |
| | | |--0.93%--_int_free
| | | |
| | | --0.85%--_int_malloc
| | |
| | |--2.27%--__GI___libc_malloc (inlined)
| | | |
| | | --0.52%--tcache_get (inlined)
| | |
| | |--2.04%--apm_sqr
| | |
| | |--1.97%--_int_free
| | |
| | |--1.37%--__memmove_avx_unaligned_erms
| | |
| | --0.52%--__GI___libc_free (inlined)
| |
| |--19.35%--bn_mul
| | |
| | |--4.56%--_apm_mul_base
| | | |
| | | |--1.31%--apm_dmul
| | | |
| | | --0.97%--apm_dmul_add
| | |
| | |--3.90%--__GI___libc_malloc (inlined)
| | | |
| | | |--1.20%--tcache_get (inlined)
| | | |
| | | --0.56%--checked_request2size (inlin>
| | |
| | |--2.56%--_int_free
| | |
| | |--1.80%--apm_mul
| | |
| | |--1.25%--__GI___libc_realloc (inlined)
| | | |
| | | --0.96%--_int_realloc
| | |
| | |--1.12%--__memmove_avx_unaligned_erms
| | |
| | --0.68%--__GI___libc_free (inlined)
| |
| |--16.17%--bn_add
| | |
| | |--3.82%--apm_add_n
| | |
| | |--3.45%--__GI___libc_realloc (inlined)
| | | |
| | | --2.77%--_int_realloc
| | | |
| | | |--0.90%--_int_malloc
| | | |
| | | --0.60%--_int_free
| | |
| | --3.08%--apm_add
| |
| |--3.87%--bn_lshift
| | |
| | |--1.31%--apm_lshift
| | |
| | --0.77%--__memset_avx2_unaligned_erms
| |
| |--1.99%--bn_init
| | |
| | --1.54%--__libc_calloc
| | |
| | --0.71%--_int_malloc
| |
| |--1.83%--_int_free
| |
| |--1.62%--bn_swap
| |
| --1.21%--bn_init_u32
| |
| --1.08%--bn_init
| |
| --0.91%--__libc_calloc
|
|--1.30%--_start
| __libc_start_main
| main
|
--0.77%--0xffffffffffffffff
main
```
可以看到即使`sysprog21/bignum` 使用 `Karatsuba algorithm` 來加速乘法與平方運算,乘法與平方仍是程式執行時間佔比最高的運算,分別為 19.35% 與 26.61%,合計近 46% 的時間占比。
`Karatsuba algorithm` 原理的部分可以參見 [blueskyson](https://hackmd.io/@blueskyson/linux2022-fibdrv) 同學講解 `Karatsuba algorithm` 的部分,說明得清楚又好理解
### 自己的([curlyw819](https://hackmd.io/T9OVVGNcRRS0o0lmfz2ZCA)) 程式分析
#### 運算速度
首先,自己的程式在費式數列的運算次數上,使用傳統的 `iterative`,時間複雜度為 O(n),而 `sysprog21/bignum` 為 `fast doubling` 的 O(log n),撇除費式數列的計算方式不同,在 `bignum` 的實作上也有差異。
`sysprog21/bignum` 採用高階 `(bn)` 與低階 `(amp)` 的 API 包裝而成,其中 `amp` 使用二進制進行 `bitwise` 運算,為整體程式提供許多速度上的優化
apm.c
```c
/* Set u[size] = u[size] + 1, and return the borrow. */
static apm_digit apm_inc(apm_digit *u, apm_size size)
{
if (size == 0)
return 1;
for (; size--; ++u) {
if (++*u)
return 0;
}
return 1;
}
/* Set u[size] = u[size] + v and return the carry. */
apm_digit apm_daddi(apm_digit *u, apm_size size, apm_digit v)
{
if (v == 0 || size == 0)
return v;
return ((u[0] += v) < v) ? apm_inc(&u[1], size - 1) : 0;
}
/* Set w[size] = u[size] + v[size] and return the carry. */
apm_digit apm_add_n(const apm_digit *u,
const apm_digit *v,
apm_size size,
apm_digit *w)
{
ASSERT(u != NULL);
ASSERT(v != NULL);
ASSERT(w != NULL);
apm_digit cy = 0;
while (size--) {
apm_digit ud = *u++;
const apm_digit vd = *v++;
cy = (ud += cy) < cy;
cy += (*w = ud + vd) < vd;
++w;
}
return cy;
}
/* Set w[max(usize, vsize)] = u[usize] + v[vsize] and return the carry. */
apm_digit apm_add(const apm_digit *u,
apm_size usize,
const apm_digit *v,
apm_size vsize,
apm_digit *w)
{
ASSERT(u != NULL);
ASSERT(usize > 0);
ASSERT(u[usize - 1]);
ASSERT(v != NULL);
ASSERT(vsize > 0);
ASSERT(v[vsize - 1]);
if (usize < vsize) {
apm_digit cy = apm_add_n(u, v, usize, w);
if (v != w)
apm_copy(v + usize, vsize - usize, w + usize);
return cy ? apm_inc(w + usize, vsize - usize) : 0;
} else if (usize > vsize) {
apm_digit cy = apm_add_n(u, v, vsize, w);
if (u != w)
apm_copy(u + vsize, usize - vsize, w + vsize);
return cy ? apm_inc(w + vsize, usize - vsize) : 0;
}
/* usize == vsize */
return apm_add_n(u, v, usize, w);
}
/* Set u[usize] = u[usize] + v[vsize] and return the carry. */
apm_digit apm_addi(apm_digit *u,
apm_size usize,
const apm_digit *v,
apm_size vsize)
{
ASSERT(u != NULL);
ASSERT(v != NULL);
ASSERT(usize >= vsize);
apm_digit cy = apm_addi_n(u, v, vsize);
return cy ? apm_inc(u + vsize, usize - vsize) : 0;
}
```
`apm_inc` 會回傳一個 `carry` ,而 `carry` 的計算方式為 : 會有一個由 `LSB` 走往 `MSB` 的指標,直到走到相加不為 0 的位數,則回傳進位
`apm_addi` 去除不必要的資訊
`apm_add` 兩個變數的 usize、vsize 相減,就可以在計算結果把高位元多出來的部分直接移到結果上
`apm_add_n` 若 usize == vsize 則使用 apm_add_n
#### 空間使用
我的程式在計算大位數的數字時就需要開很大的陣列,如下
```c
void bn_add(bignum *x, bignum *y, bignum *dest)
{
if (x->length < y->length)
return bn_add(y, x, dest);
int carry = 0;
for (int i = 0; i < y->length; i++) {
dest->decimal[i] = y->decimal[i] + x->decimal[i] + carry - OFFSET;
if (dest->decimal[i] >= 10 + OFFSET) { // OFFSET = 48
carry = 1;
dest->decimal[i] -= 10;
} else
carry = 0;
}
for (int i = y->length; i < x->length; i++) {
dest->decimal[i] = x->decimal[i] + carry;
if (dest->decimal[i] >= 10 + OFFSET) {
carry = 1;
dest->decimal[i] -= 10;
} else
carry = 0;
}
dest->length = x->length;
dest->decimal[dest->length] = carry + OFFSET;
if (carry)
dest->length++;
dest->decimal[dest->length] = '\0';
}
```
且每個數字都需要用到 2 個 byte 來儲存,若 fib(500) 有 105 個數字,就需要用到 210 byte,記憶體開銷非常大,且有潛在的風險,當fib往上加,就可能會有溢位的問題
而 `sysprog21/bignum` 固定就是 64 bit(or 32 bit),並且在要輸出結果時才一口氣轉換成 10 進制,節省了不少時間上的開銷
### 落實原始程式碼中標注的待做事項
由於個人能力不足,在經過一陣子嘗試後仍沒有進度,因此無法完成