contributed by <ray90514
>
執行的時候使用 taskset -c 1
每一組測試總共執行 50 次,去掉最大值和最小值後剩下的取平均。
使用 unsigned long long
的動態陣列儲存大數,並且有紀錄陣列長度跟最大長度的變數。
將大數傳遞給 user 的時候只要根據 len
傳遞對應大小的 digits
。
對 BigN
初始化, digits_num
是陣列的大小。
釋放 BigN
的空間。
參考 bignum ,可以省下複製大數的時間。
兩個大數相加,將結果存進 output
。具體作法是由低位將兩數相加,如果有 overflow 就將 carry
加至下一位的結果中。 output
可以與要做運算的兩數是相同的位址。
這裡有個細節是因為還有 carry
要補上去,可能造成額外的 overflow ,所以要做兩次判斷。
兩個大數相減,將結果存進 output
, output
可以與要計算的兩數位址相同。具體作法是從低位開始將兩數相減,如果有需要借位的情況,就將下一位的結果減一。這裡跟加法一樣,前一位的借位可能造成額外的借位,所以要判斷兩次,最後調整 output
的長度至第一個不為 0 的位數。有個要注意的地方是 x
的值必須大於等於 y
。
將大數向左位移一位,相當於乘以二,不保證超出 max_len
的結果。具體作法是對陣列每個數做位移,將最高位的值放到下一個數的最低位。
兩數相乘,將結果存入 output
,這裡 output
不能與兩數的位址相同。具體作法使用最慢但比較直觀的直式乘法,x[i] * y[j]
的結果累加至 output[i + j]
,為了避免補 carry
時要一直判斷有沒有 overflow ,將 overflow 的 carry
存至別的數,最後再一次加起來。
加一個常數至大數裡,處理進位即可。
將大數減一個常數,處理借位即可。
user 取得大數後,因為 printf
無法直接輸出,我的想法是先將陣列轉成方便輸出的形式,原本儲存的值改儲存unsigned long long
範圍內最大的 的冪次方也就是 ,這樣就可以直接輸出。
中間那段程式碼在做進位轉換,先是對大數除以 ,第 n 次除法的餘數為轉換後第 n 位的值,然後商成為下一輪的被除數,直到商為 0 。對大數的除法是採用長除法,在這段程式碼,餘數直接放在當前的位置,商放在高一位的位置,這樣不用額外的空間。
以上這些大數的運算比較針對 Fibonacci Number 的計算,所以會有些限制,但都符合之後計算的需求。
Fibonnaci Number 有一個近似值的公式
利用這個公式我們可以近似出所需的空間。
unsigne long long
儲存 : 2 + 7 * n / 640
unsigne long long
儲存 : 2 + 21 * n / 1900
為了運算方便有多加一,如果要更準確一點的話使用以下的值
比較直觀的作法是根據定義 F(n) = F(n - 1) + F(n - 2), F(0) = 0, F(1) = 1
寫出一個迴圈的版本。
因為 add_BigN
可以輸出至與輸入相同的大數,將結果儲存在算 F(n + 1) 時不需要用到的原本的 f_n_prev
,最後將已經是 F(n) 的 f_n_prev
跟 F(n - 1) 的 f_n
交換,這樣就不需要額外的空間。
利用 F(n) 與 F(n + 1) 可以算出 F(2n) 與 F(2n + 1) 的值,公式如下。
a
為 F(n) , b
為 F(n + 1)
每隔 60 測試一次,總共測試 100 次
由圖可以很明顯看出迴圈的執行時間遠大於 fast doubling ,接下來會對 fast doubling 進行修改。
在之前的程式碼中每一輪都會算出 F(n) 和 F(n + 1) ,就連最後一輪也是,但實際上只需要 F(n) 或是 F(n + 1) 就好,因此對最後一輪做特別的處理。
接下來比較兩者的差異,每隔 99 測試一次,總共測試 100 次。
從上面的程式碼可以看出 k 為奇數的時候可以省下一次的乘法運算,為偶數時可以省下兩次乘法運算,省下的乘法運算也是計算中最耗時的最後一輪,因此對比原本的寫法可以改進約 20% 和 43% 。奇數與偶數差了一次乘法運算造成了時間呈鋸齒狀。
參考自 斐波那契数列第一千万项怎么求 ,使用 Catalan's identity 改寫原本的公式
這樣就可以省下一次乘法,程式碼如下
對比了前一個改進的寫法加速約 20% 。使用 perf record
查看結果如下
很明顯的運算的瓶頸在乘法上面,若要再改進的話要從 mul_BigN
下手。
參考 KYG-yaya573142 的報告 ,使用 inline asm 直接使用乘法結果的高位和低位。與前次改進比較結果如下圖,加速了約 10% 。
實作 Karatsuba algorithm ,假設 AB 和 CD 相乘,乘積可以看成 AC BC + AD BD 三部分,中間的部分可以改寫為 (A + B)(C + D) - AC - BD ,這樣可以少一次 O(n / 2) 的乘法,除此之外 AC BD (A + B)(C + D) 也可以用同樣的方法算乘積,以下是遞迴至某個長度的兩數相乘就改用原本乘法的比較。
由以上圖可以看出 threshold 為 8 和 64 的效果較好,太大或太小的效果較差。