--- tags: linux2022 --- # 2022q1 Homework3 (fibdrv) contributed by < `sternacht` > ## 自我檢查清單 - [x] 研讀上述 ==Linux 效能分析的提示== 描述,在自己的實體電腦運作 GNU/Linux,做好必要的設定和準備工作 $\to$ 從中也該理解為何不希望在虛擬機器中進行實驗; - [x] 研讀上述費氏數列相關材料 (包含論文),摘錄關鍵手法,並思考 [clz / ctz](https://en.wikipedia.org/wiki/Find_first_set) 一類的指令對 Fibonacci 數運算的幫助。請列出關鍵程式碼並解說 - [ ] 複習 C 語言 [數值系統](https://hackmd.io/@sysprog/c-numerics) 和 [bitwise operation](https://hackmd.io/@sysprog/c-bitwise),思考 Fibonacci 數快速計算演算法的實作中如何減少乘法運算的成本; - [ ] 研讀 [KYG-yaya573142 的報告](https://hackmd.io/@KYWeng/rkGdultSU),指出針對大數運算,有哪些加速運算和縮減記憶體操作成本的舉措? - [ ] `lsmod` 的輸出結果有一欄名為 `Used by`,這是 &#34;each module&#39;s use count and a list of referring modules&#34;,但如何實作出來呢?模組間的相依性和實際使用次數 ([reference counting](https://en.wikipedia.org/wiki/Reference_counting)) 在 Linux 核心如何追蹤呢? &gt; 搭配閱讀 [The Linux driver implementer’s API guide » Driver Basics](https://www.kernel.org/doc/html/latest/driver-api/basics.html) - [ ] 注意到 `fibdrv.c` 存在著 `DEFINE_MUTEX`, `mutex_trylock`, `mutex_init`, `mutex_unlock`, `mutex_destroy` 等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題。嘗試撰寫使用 [POSIX Thread](https://en.wikipedia.org/wiki/POSIX_Threads) 的程式碼來確認。 ## [作業要求](https://hackmd.io/@sysprog/linux2022-fibdrv#-作業要求) ## 開發過程 ### 環境搭建 為了排除可能的干擾,根據[作業中的提示](https://hackmd.io/@sysprog/linux2022-fibdrv#排除干擾效能分析的因素)執行以下步驟調整至適合實驗的環境 - 抑制 address space layout randomization (ASLR) ```cmd $ 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 ```cmd $ sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo" ``` 修改 `/etc/default/grub` 裡的設定為 `GRUB_CMDLINE_LINUX="isolcpus=0,1"` ,用 `sudo update-grub` 命令使 grub 檔案更新,最後重新開機 ![](https://i.imgur.com/mXT3L2X.png) 開機後可以從內建的 System Monitor 中看到 cpu 0, 1(圖中編號是 1, 2) 已經不會有負載了 以下涉及執行時間觀察的實驗皆是在被保留的 cpu 中執行。 ### fast doubling 將檔案 git clone 下來之後,可以看到裡面計算 fibonacci sequence 的演算法是根據定義實作的,隨著數列的值增加,迴圈次數為 $O(n)$ 線性成長。根據作業內容,我們可以透過 fast doubling 的演算法來實作出迴圈次數為 $O(logn)$ 的程式如下 ```c static long long fast_doubling(int k){ if (k <= 0) return 0; long long a = 0, b = 1; long long t1, t2; for (int i = 31 - __builtin_clz(k); i; i--){ t1 = a * (2 * b - a); t2 = b * b + a * a; a = t1; b = t2; if (k & (1 << i)){ t1 = a + b; a = b; b = t1; } } return a; } ``` 接著修改 fib_read 讓程式執行的同時印出我們想要的訊息,這部分參考了 [OscarShiang](https://hackmd.io/@oscarshiang/linux_fibdrv#使用-fibdrv-進行實驗) 的筆記得知該如何使用 printk 來觀察結果 ```c static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) { ktime_t t1 = ktime_get(); ssize_t ret = fib_sequence(*offset); t1 = ktime_sub(ktime_get(), t1); ktime_t t2 = ktime_get(); fast_doubling(*offset); t2 = ktime_sub(ktime_get(), t2); printk(KERN_INFO "%d %lld %lld", *offset, ktime_to_ns(t1), ktime_to_ns(t2)); return ret; } ``` 執行 `./client` 後,再用 `dmesg` 命令來觀察,部分結果如下 ``` [ 9904.070177] 0 35 25 [ 9904.070182] 1 25 25 [ 9904.070184] 2 35 22 [ 9904.070186] 3 35 20 [ 9904.070188] 4 34 22 [ 9904.070190] 5 34 20 [ 9904.070191] 6 37 19 [ 9904.070193] 7 38 19 [ 9904.070195] 8 37 20 [ 9904.070197] 9 41 19 [ 9904.070199] 10 41 21 [ 9904.070201] 11 59 20 [ 9904.070203] 12 53 21 [ 9904.070205] 13 53 22 [ 9904.070207] 14 45 22 [ 9904.070209] 15 47 21 ... ``` 將結果利用 gnuplot 製作成圖表,fast doubling 花費的時間明顯比原本的演算法快上許多 ![](https://i.imgur.com/nbjne7z.png) > 註 : 此結果為不正確的,詳細請看接下來的討論 ### w/o clz 指令的影響與調整 接著比較一下若不使用 clz 這樣的指令會對系統的運算方面造成什麼影響。以下直接將 i 設為從 MSB 開始,這樣的話無論 k 的大小,迴圈數都是固定的。 ```c static long long fast_doubling_2(int k){ if (k <= 0) return 0; long long a = 0, b = 1; long long t1, t2; for (int i = 31; i; i--){ t1 = a * (2 * b - a); t2 = a * a + b * b; a = t1; b = t2; if (k & (1 << i)){ t1 = a + b; a = b; b = t1; } } return a; } ``` ![](https://i.imgur.com/zo7FduE.png) ![](https://i.imgur.com/IorIlHR.png) >原本以為不使用 clz 這樣個函式會對執行時間有所影響,但實驗結果在花費時間上卻差不多? 但若是搬到 user space 上執行,差距就很大了 (why?) 仔細看過 [KYG-yaya573142 的報告](https://hackmd.io/@KYWeng/rkGdultSU#clz-的影響)後,發現有一段有提到相同的問題,於是我根據裡面提到的步驟去察看編譯後的組合語言,確認程式執行的過程使用了哪些指令 ```cmd objdump -dS fibdrv.ko > out ``` [objdump](https://man7.org/linux/man-pages/man1/objdump.1.html) 可以查看 obj file 的內容,搭配 -S 可以將已經變成 binary code 的檔案反組譯,我們主要是在 fib_read 的函式中執行 fibonacci 數列的計算,因此直接看這一段的組合語言在做甚麼 ```asm 00000000000000c0 <fib_read>: { c0: e8 00 00 00 00 callq c5 <fib_read+0x5> c5: 55 push %rbp c6: 48 89 e5 mov %rsp,%rbp c9: 53 push %rbx t1 = ktime_get(); ca: e8 00 00 00 00 callq cf <fib_read+0xf> cf: 48 89 c3 mov %rax,%rbx t1 = ktime_sub(ktime_get(), t1); d2: e8 00 00 00 00 callq d7 <fib_read+0x17> d7: 48 29 d8 sub %rbx,%rax } da: 5b pop %rbx db: 5d pop %rbp dc: c3 retq dd: 0f 1f 00 nopl (%rax) ``` 對照 C 語言的程式碼 ```c static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) { ktime_t t1; t1 = ktime_get(); long long fib = fast_doubling_l(*offset); t1 = ktime_sub(ktime_get(), t1); return (ssize_t) ktime_to_ns(t1); } ``` 可以看到編譯過後的組合語言就只剩下兩個 `ktime_get()` 的系統呼叫,以及將兩數相減的過程,而 fibonacci 的計算則完全不見了,這並不是我們想要的。而之前的另一個疑問是,為什麼搬到 userspace 上兩者卻又有差距呢 ? 原因是 userspace 的程式編譯過程與 kernel module 不一樣,因此編譯出來的程式並沒有將這個函式呼叫省略掉,所以執行上是正常的 ```asm= main: .LFB7: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl $50, -12(%rbp) movl $0, -16(%rbp) jmp .L8 .L9: movl -12(%rbp), %eax cltq movq %rax, %rdi call fast_doubling movq %rax, -8(%rbp) addl $1, -16(%rbp) .L8: movl -16(%rbp), %eax cmpl -12(%rbp), %eax jl .L9 movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc ``` > 第 18 行有呼叫 `fast_doubling`,是真的有執行到 至於要如何修掉這個問題呢 ? 我猜測這個問題是由於最佳化所導致的,雖然不明白是甚麼使得程式認為這段可以省略(因為沒有用到變數 `fib` ?),但是把最佳化關掉試試看吧,在 Makefile 這段後面加上關閉最佳化的選項 `-O0` ``` ccflags-y := -std=gnu99 -Wno-declaration-after-statement -O0 ``` 重新編譯並再次輸出 `fibdrv.ko` 的資訊 ```asm 0000000000001263 <fib_read>: /* calculate the fibonacci number at given offset */ static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) { 1263: e8 00 00 00 00 callq 1268 <fib_read+0x5> 1268: 55 push %rbp 1269: 48 89 e5 mov %rsp,%rbp 126c: 48 83 ec 30 sub $0x30,%rsp 1270: 48 89 7d e8 mov %rdi,-0x18(%rbp) 1274: 48 89 75 e0 mov %rsi,-0x20(%rbp) 1278: 48 89 55 d8 mov %rdx,-0x28(%rbp) 127c: 48 89 4d d0 mov %rcx,-0x30(%rbp) t1 = ktime_get(); 1280: e8 00 00 00 00 callq 1285 <fib_read+0x22> 1285: 48 89 45 f0 mov %rax,-0x10(%rbp) fib = fast_doubling(*offset); 1289: 48 8b 45 d0 mov -0x30(%rbp),%rax 128d: 48 8b 00 mov (%rax),%rax 1290: 48 89 c7 mov %rax,%rdi 1293: e8 9c fe ff ff callq 1134 <fast_doubling> 1298: 48 89 45 f8 mov %rax,-0x8(%rbp) t1 = ktime_sub(ktime_get(), t1); 129c: e8 00 00 00 00 callq 12a1 <fib_read+0x3e> 12a1: 48 2b 45 f0 sub -0x10(%rbp),%rax 12a5: 48 89 45 f0 mov %rax,-0x10(%rbp) return (ssize_t) ktime_to_ns(t1); 12a9: 48 8b 45 f0 mov -0x10(%rbp),%rax 12ad: 48 89 c7 mov %rax,%rdi 12b0: e8 5a ed ff ff callq f <ktime_to_ns> } 12b5: c9 leaveq 12b6: c3 retq ``` 這次就可以看的到 `fast_doubling` 的函式呼叫了 最後我們再看一下關閉最佳化的情況下,是否用 `__builin_clzll` ,以及原本的方式,三者之間的執行時間差距 ![](https://i.imgur.com/6Kph2w9.png) 雖然未能完全解決關於干擾的問題,但從圖上還是可以明顯看出使用 clz 的 fast_doubling 是最快的,不使用 clz 的 fast_doubling 與加法的實作則隨著 fibonacci 數列增長而有交叉,最終仍是 fast_doubling 略勝一籌。 > 值得一提的是,因為最佳化會導致函式沒有被執行的問題,因此在這張圖中,加法實作比起有最佳化慢了約一倍的時間,所以如果有辦法使函式不被最佳化吃掉的話, fast_doubling 的速度也能更快,例如在前面提到 [KYG-yaya573142 的報告](https://hackmd.io/@KYWeng/rkGdultSU#clz-的影響)中便有使用其他方式去避免最佳化不去執行函式。 ### 計算 fib(92)之後的數 fib(92) 之後的數在 long long 的形態中會造成 overflow (unsigned long long 可以容納至第 93 項),一開始我利用如下的自定義結構來儲存大於 64 bits 的數 ```c typedef struct{ unsigned long long lower, upper; } bignum; ``` 當數字小於 $2^{64}$ 時儲存在 lower 中,一旦超過就使 upper + 1 ,如此一來可以達到相當於 __int128 的大小。針對這種結構的加減法計算相對簡單,只要判斷是否有進位或借位的狀況發生去調整就好,但遇到乘法的時候就不行了,因為結構的大小是固定的,超出範圍的進位無法表示,亦即上限仍然存在。因此考慮另一種可動態調整大小的整數陣列如下 ```c typedef struct{ int len; unsigned long long *bn; } bignum; ``` len 用來記錄陣列 bn 中有多少個元素,而每個元素則是一個 long long 型態的整數,當達到存取的上限時,會動態的去調整陣列大小。 加減法的實作如下 ```c bignum bignum_add(bignum a, bignum b) { bignum ret = bignum_init(); int len = b.len; ret.len = len + 1; ret.bn = malloc(sizeof(long long) * (len + 1)); // c stand for 'carry or not' int i = 0, c = 0; while (i < a.len) { *(ret.bn + i) = *(a.bn + i) + *(b.bn + i) + c; c = (*(ret.bn + i) < *(a.bn + i)) ? 1 : 0; i++; } while (i < b.len) { *(ret.bn + i) = *(b.bn + i) + c; c = (*(ret.bn + i) < *(b.bn + i)) ? 1 : 0; i++; } *(ret.bn + i) = c; bignum_check(&ret); return ret; } bignum bignum_sub(bignum a, bignum b) { bignum ret = bignum_init(); ret.len = a.len; ret.bn = malloc(sizeof(long long) * a.len); int i = 0, c = 0; while (i < b.len) { *(ret.bn + i) = *(a.bn + i) - *(b.bn + i) - c; c = (*(b.bn + i) + c > *(a.bn + i)) ? 1 : 0; i++; } while (i < a.len) { *(ret.bn + i) = *(a.bn + i) - c; c = *(a.bn + i) ? 0 : 1; i++; } bignum_check(&ret); return ret; } ``` 加法會預先多給空間、減法則可能會使陣列的最後一個元素變成 0 ,`bignum_check` 用來檢查是否有最尾端的元素為 0 的情況發生,進而調整記憶體的使用。 為了測試這兩個函示是否正常運作,因此我先參考了[這篇討論](https://stackoverflow.com/questions/8023414/how-to-convert-a-128-bit-integer-to-a-decimal-ascii-string-in-c)的內容,改寫成適用於動態 bignum 結構的輸出十進位值之實作,程式碼如下 ```c char *bignum2dec(bignum a) { char s[64 * a.len / 3 + 2]; long long n[a.len]; char *p = s; int i; memset(s, '0', sizeof(s) - 1); s[sizeof(s) - 1] = '\0'; memcpy(n, a.bn, sizeof(n)); // INT64_M = 0xFFFFFFFFFFFFFFFF // INT64_H = 0x8000000000000000 for (i = 0; i < 64 * a.len; i++) { int j, c, l = a.len; c = (n[l - 1] >= INT64_H); while (--l) { n[l] = ((n[l] << 1) & INT64_M) + (n[l - 1] >= INT64_H); } n[0] = ((n[0] << 1) & INT64_M); for (j = sizeof(s) - 2; j >= 0; j--) { s[j] += s[j] - '0' + c; c = (s[j] > '9'); if (c) s[j] -= 10; } } while (*p == '0' && p < &s[sizeof(s) - 2]) p++; return p; } ``` 概念是透過 bitwise 操作逐步將十進位的值以每次一個乘 2 的方式算出來,並記錄是否進位到下一個字元,缺點是 s 大小是根據 bignum 陣列長度設置,因此還需要在最後將多餘的 '0' 給排除掉。 因為 `*p` 是宣告在函式中,回傳後 stack 被釋放了,並不能保證原本位址裡面的值不會被改變,印出來的數字會發生錯誤,因此前面計算的部分保留,修改 `*p` 的宣告以及給值的方式如下 ```c i = 0; while (s[i] == '0' && i < sizeof(s) - 2) i++; char *p = malloc(sizeof(s) - i); memcpy(p, s + i, sizeof(s) - i); return p; ``` 結合加法與輸出的實作,已經可以計算出相當大的 fibonacci 數了,但是只是根據定義的加法實作,而前面所提到的 fast doubling 還需要乘法。以下是一個簡易的實作方式,主要利用的概念是,兩個 long long 長度的數相乘,其積不可能超過兩個 long long 的長度,因此用一個較大的型態儲存乘積,再將乘積分開成前後兩部份分別加到相對應的陣列位置當中,對兩個 bignum 的陣列種每兩兩一組元素重複著個動作,並考慮進位的情況做調整,就可以得到答案。 再來考慮進位的問題,當乘法循環時,高 64 位元數可能有來自低 64 位元的進位、以及上一次迭代時所產生的進位兩種,因此需要分別計算進位是在哪個時期產生 ```c bignum bignum_mul(bignum a, bignum b) { bignum ret = bignum_init(); ret.len = a.len + b.len; ret.bn = calloc(sizeof(long long) * ret.len); int i, j; for(i = 0; i < a.len; i++){ // 前 64 bits 與結果相加時產生的進位 int uc = 0; for(j = 0; j < b.len; j++){ // 後 64 bits 與結果相加時產生的進位 int lc = 0; __int128 tmp = (__int128)(*(a.bn + i)) * (__int128)(*(b.bn + j)); *(ret.bn + i + j) += (tmp & INT64_M); lc = (*(ret.bn + i + j) < (tmp & INT64_M))? 1: 0; *(ret.bn + i + j + 1) += (long long)(tmp >> 64) + lc + uc; uc = (*(ret.bn + i + j + 1) < (long long)(tmp >> 64))? 1: 0; } *(ret.bn + i + j) += uc; } bignum_check(&ret); return ret; } ``` 於是我們可以用以下的 fast doubling 函式來計算更大的 fibonacci 數了, ```c bignum fast_doubling(int n) { bignum a = bignum_init(), b = bignum_init(); *(b.bn) = 1; bignum t1, t2; for (int i = 32 - __builtin_clz(n); i--;){ // t2 = a * a + b * b t1 = bignum_mul(a, a); t2 = bignum_mul(b, b); t2 = bignum_add(t1, t2); // t1 = a * (2 * b - a) t1 = bignum_add(b, b); t1 = bignum_sub(t1, a); t1 = bignum_mul(t1, a); a = t1; b = t2; if(n & (1 << i)){ t1 = bignum_add(a, b); a = b; b = t1; } } return a; } ``` 輸出結果與查詢到的[前 500 fibonacci 數列](https://blog.abelotech.com/posts/first-500-fibonacci-numbers/)亦符合。 ![](https://i.imgur.com/pLDnfjs.png) ### memory leak 到目前為止的 bignum 實作還是在 userspace 的環境下,在搬到 kernel 之前,我先透過 `valgrind --leak-check=full ./test` 命令來查看 memory leak 相關的問題 ![](https://i.imgur.com/CJHLDPv.png) 果不其然有相當嚴重的 memory leak 的問題,因為大部分的函式都有用到 malloc ,但是都沒 free 掉,而主要會發生 memory leak 的地方有三個 : 第一個是 fast_doubling 函式會回傳一個 bignum 結構,而這個結構中的記憶體沒有釋放掉,並在下一次被覆蓋掉,造成 memory leak ,需要在輸出結果後連同結果的字串將其釋放 第二個是在計算每一個 fibonacci 數時,需要額外幾個暫存器放置運算所需的數,這些數要在函數回傳之前全部釋放(a 回傳 , b, t1, t2 釋放) > 更新 : 最後的版本中是回傳字串, a, b, t1, t2 全部在函式中釋放 第三個則是在運算的過程中所產生的,原本的函式設計是會產生一個新的 bignum 作為回傳,但如此一來原先的變數中所佔據的記憶體卻沒被釋放,於是這裡我改成另一種寫法,將原本回傳要放的變數位址同時傳給函式,並直接改動裡面的值,如下 ```c void bignum_add(bignum a, bignum b, bignum *ret) { ret->len = b.len + 1; // 被加數與加數可能與和是同一個 bignum , bn 的指標是相同的 // 因此先將計算結果寫入在另一塊記憶體上,最後合併到 ret uint64_t *bn = malloc(sizeof(uint64_t) * (ret->len)); int i = 0, c = 0; while (i < a.len) { *(bn + i) = *(a.bn + i) + *(b.bn + i) + c; c = (*(bn + i) < *(a.bn + i)) ? 1 : 0; i++; } while (i < b.len) { *(bn + i) = *(b.bn + i) + c; c = (*(bn + i) < *(b.bn + i)) ? 1 : 0; i++; } *(bn + i) = c; // 替換前將舊的 bn 指標釋放 free(ret->bn); ret->bn = bn; bignum_check(ret); return; } ``` 將全部的函式整理過後再測試一次 ![](https://i.imgur.com/7QU9yMj.png) ### 引入 fibdrv 先看一下原本的函式是如何處理傳入值以及回傳值的, `fib_read` 會把 fibonacci 數的計算結果當做 read 的返回值直接回傳給變數 `sz`,而後者的回傳型態是 long long ,並在 `client.c` 輸出。 而在改成 bignum 結構後,回傳的方式必須要改成用作業說明裡提到的 `copy_to_user` 寫入,然後再從 `client.c` 裡面的 buf 中讀取。 接著在改程式碼的過程中,又做了一些修改,首先是因為 stdint.h 屬於 userspace , kernel 不好使用,因此改成 kernel 底下有的 type,如 `__u64 (unsigned long long)`,但也因為不能使用像`__int128` 這樣大的整數結構,所以將整段程式的存取位元減半成 `__u32`。 再來是 stdlib.h 同樣屬於 userspace,底下的 malloc, free 等函式,也要改成 kernel 使用的 [kmalloc](https://www.kernel.org/doc/htmldocs/kernel-api/API-kmalloc.html), [kfree](https://www.kernel.org/doc/htmldocs/kernel-api/API-kfree.html) 等。 > fibdrv.c ```c /* calculate the fibonacci number at given offset */ static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) { // fast_doubling() retrun a string of number char *ret = fast_boudling(*offset); copy_to_user(buf, ret, strlen(ret) + 1); kfree(ret); return 0; } ``` > client.c ```c for (int i = 0; i <= offset; i++) { lseek(fd, i, SEEK_SET); sz = read(fd, buf, 1); printf("Reading from " FIB_DEV " at offset %d, returned the sequence " "%s.\n", i, buf); } ``` 此外,在編譯時期會出現以下警告 ``` warning: ISO C90 forbids variable length array 'f' ``` 這是因為陣列的記憶體分配是在編譯時期就會做處理,變動長度會使得系統不知道該分配多少空間給該陣列,同時若傳入作為長度的變數太大,程式又缺乏檢查機制,就容易造成 stack overflow,因此改成用 malloc, kmalloc 等方式來索取記憶體是比較安全的方式 最後將 `scripts/expected.txt` 裡的 fib(93) ~ fib(100) 修改成正確的值後,執行 `make check` ,`scripts/verify.py` 終於沒有報錯了~。 ### 效能比較 拿來比較的是老師的 [bignum 實作](https://github.com/sysprog21/bignum),為了方便反覆測試,這裡選擇在 userspace 上測量時間。 ![](https://i.imgur.com/l1fO6Ni.png) 最初的實作慢了相當多,於是第一步先考慮有哪些程式碼可以更加精簡,例如原本 fast_doubling 中有一段程式碼如下註解部分,要將 a = b, b = a + b,原先的做法是先做加法併存到另一個位置,然後再將內容複製過去,但其實可以將 a, b 內容先對調,再把相加的結果存到 b 之中,結果相同 ```c if(n & (1UL << i)){ // bignum_add(a, b, t1); // bignum_copy(a, b); // bignum_copy(b, t1); bignum_swap(a, b); bignum_add(b, a, b); } ``` 以及另一段乘以 2 的實作,原本是用加法來實現,但相對於加法,直接用二進位的位移操作其實更快,因此可以寫一個函式來做乘以二的乘法 ```c void bignum_bwdouble(bignum *ret) { int len = ret->len; if (*(ret->bn + len - 1) > INT64_H){ ret->bn = realloc(ret->bn, sizeof(uint64_t) * (len + 1)); *(ret->bn + len) = 1; } while(--len){ *(ret->bn + len) <<= 1; *(ret->bn + len) |= (*(ret->bn + len - 1) > INT64_H); } *(ret->bn) <<= 1; return; } ``` 第一步的改進成果如下 ![](https://i.imgur.com/tQax82L.png)