主講人: jserv / 課程討論區: 2023 年系統軟體課程
返回「Linux 核心設計/實作」課程進度表Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
應用 Fast Doubling 手法,搭配 bitwise 運算開發遞迴版的程式碼:
運用 fast doubling 計算,可降低運算成本:
在費式數列的計算上,原本使用迭代方式計算,迴圈迭代次數與欲求費式數成正比,時間複雜度爲 。運用 fast doubling
後,至多只要迭代 64 (或 32,依設定有所不同)次,實際上去除 MSB 為 0 的 bit 不用做迭代,時間複雜度為 。
不過,上述方法雖減少計算量,但仍有重複計算的部份:
當 target
數值越大,重複的計算會效能衝擊越顯著。
觀察數值的 2 進制表示,可發現該數是如何產生,以 為例:
若是進行移項並反過來看的話會變成:
從 開始看,可發現每次位移後,只要檢查目標數對應的位元,即可知曉下次應以 還是 為基礎進行右移。
整理前述觀察,可知:
對應的程式碼:
而迴圈內 if...else...
的部份可用 -!!(target & (1 << i))
作為 mask 的技巧簡化成:
初次執行 client
會發現從 之後輸出的數值都一樣,這是因為 fibdrv 中預設限制最大項目為
fib_read
返回值的型態為 long long
,即 64 位元有號整數,其有效範圍是 至 之間,比對費氏數列的正確值,可確認 會超出此範圍,這也是預設限制最大可用項目為 92 的原因
移除限制並重新觀察輸出,會從 F(93) 開始 overflow
雖然結果 overflow,但可根據二補數,算出 overflow 後為何是這個數值
將使用的資料由 long long
更改為 uint64_t
,可多計算出一項正確的數值 ,不過從 開始仍會 overflow
一樣可以檢驗 overflow 後為何是這個數值
bn
結構體為了計算 92 項以後的費氏數列,我們引入長度可變動的數值表示法,動態配置不同大小的記憶體來呈現更大範圍的整數,定義的資料結構如下
number
- 指向儲存的數值,之後會以 array 的形式來取用size
- 配置的記憶體大小,單位為 sizeof(unsigned int)
sign
- 0 為正數、1 為負數由於大數沒辦法直接以數值的形式列出,這裡改用字串來呈現,轉換的部分利用 ASCII 的特性並根據 fast doubling 的邏輯來「組合」出 10 進位字串
加法與減法由於需要考慮數值的正負號,因此分為兩個步驟,先由 bn_add
與 bn_sub
判斷結果的正負號,再使用輔助函數 bn_do_add
與 bn_do_sub
進行無號整數的計算
bn_add
負責所有正負號的判斷,所以 bn_sub
只是改變 b 的正負號後,再直接交給 bn_add
判斷
tmp
來暫時的賦予不同的正負號bn_cmp
負責比對兩個 bn
物件開絕對值後的大小,邏輯類似 strcmp
c
的大小足以儲存計算結果DIV_ROUNDUP
的用法參考自 /arch/um/drivers/cow_user.ccarry
來實行兩個 4 bytes 項目的加法來避免 overflow
bn_msb
和 bn_clz
是 bn 版本的 clz,詳見 bn_kernel.ca
與 b
,並於計算完成後再再補上負號bn_do_add
一樣,不過此時 carry 是作為借位使用c == a || c == b
,就必須配置記憶體來儲存計算結果,避免 a
與 b
在計算途中就被改變bn_mult_add
負責將每一行的計算結果疊加上去,如下number
紀錄的是指標,因此這麼做可以確實的互換兩個 bn 的數值,但不用更動儲存在 heap 中的數值使用實作的大數運算來計算第 項以後的費氏數列,以下展示迭代演算法
接著是 fast doubling 的實作
我們可使用 Python 程式碼進行驗證,至少能正確計算至第 項
list_head
的大數運算解讀:
malloc
以及 free
的成本printf
類的格式化輸出函式)程式碼中的 l
以及 s
這二個間接指標 (indirect pointer) 雖看似多餘,但是其實不可被簡化,否則會影響答案,因為第 14 行的 NEW_NODE
會新增一節點並透過 list_add_tail
加到 slr
的尾端,因此若是單純使用指向 list_head
的指標的話,則需要在 NEW_NODE
後重新將 s
指向新的節點,否則第 18 行的 list_entry
相當於對鏈結串列的 head 使用 list_entry
,造成存取到預期外位址。
因此運用間接指標 *s
來省略將 s
指向新的尾端節點的步驟。
為了盡可能的減少節點數量,原本規劃以 UINT64_MAX
作為上限,但解碼時仍須進行大數運算,因此改以 10 的冪()作為上限值,來減少解碼成本。
而不選擇以 作為上限的原因則是考慮到兩個 相加時會超過 UINT64_MAX
,造成需要額外判斷是否有 overflow 的發生,因此選擇少一位數的 作為上限配合 進行簡單的判斷。
由於目前只有實作加法的功能,因此先以最基本的遞迴呼叫測試:
大約在 秒左右計算完成,且與 WolframAlpha 上的結果相同。
而 大約在 30 秒內計算完成,但因位數過多無法與 WolframAlpha 上的結果相比。
先以 perf stat
分析前述程式碼,作為後續比對的基準
接下來使用 perf record
量測 call graph (省略部分內容)
bn_fib_fdoubling
內,其中有 83.03% 的時間會再呼叫其他函式bn_mult
佔整體 48.43% 的時間,因此優化乘法會帶來明顯的效能增益bn_fib_fdoubling
內有接近一半的時間在管理動態記憶體與複製資料,顯然需要相關的策略來降低這部分的成本bn_add
與 bn_sub
共佔 9% 的時間,需要再單獨使用 iterative 版本的 bn_fib
來進行分析與優化,否則很難在 bn_fib_fdoubling
內觀察到效能增益bn_free
占有高比例的原因不明,目前先猜測可能是因為 bn_mult
過度頻繁的呼叫 bn_alloc
與 bn_free
bn_fib_fdoubling
原本的實作局限於使用 bn_cpy
來更新暫存變數 k1
與 k2
的數值,其實可以藉由 bn_swap
以及改變各函式儲存結果的位置來達成一樣的目的,將所有的 bn_cpy
去除來降低複製資料造成的成本
當資料來源與目的重疊時 (c == a || c == b
),bn_mult
必須先配置暫存的記憶體空間來儲存計算結果,因此可以進一步確保呼叫 bn_mult
時不要發生此狀況,降低使用 malloc
及 memcpy
的次數。
結果如下 (v1 綠線)
觀察 sysprog21/bignum 中費氏數列的實作函式 fibonacci,會發現雖看似採用 fast doubling,但實際是 Q-matrix 這樣的變形,推導如下:
整理後可得
使用上述公式改寫 bn_fib_fdoubling
,搭配使用 clz
,後者可讓 n 值越小的時候,減少越多次迴圈運算,從而達成加速。
結果如下 (v2 紅線)
原本實作的大數運算會在計算前先使用 bn_resize
(realloc
),確保有足夠大的空間來儲存計算結果,再於計算結束後檢查是否有多餘的空間 (msb 所在的 array 數值為 0) 並進行修剪 (trim),避免造成 memory leak 與增加後續計算的成本 (因為要存取的空間會越來越長),然而頻繁使用 realloc
可能會造成降低效能。
引入 memory pool,以 capacity 的方式管理 bn 實際可用的記憶體大小,降低 bn_resize
實際呼叫 realloc
的次數
realloc
,並以 4 為單位配置更大的空間realloc
來縮小空間結果如下 (v3 紅線)
realloc
造成的成本非常可觀bn
結構體中原本每個陣列的資料型態使用 unsigned int
,在 64 位元微處理器下改為使用 uint64_t
,搭配合適的 alignment,可確保每次記憶體存取都是 word size 寬度,從而提高資料存取的速度。
__int128
實作結果如下 (v4)
uint64_t
更能發揮 64 位元 CPU 的優勢bn_add
和 bn_mult
bn_add
的效能為了凸顯 bn_add
對效能的影響,這個章節改為量測 bn_fib
(iterative) 作為判斷依據,並將量測的範圍提高到 F(10000)。由於上述幾個改善策略也會提升 bn_add
的效能,因此先重新量測現有的效能,結果如下 (v1 紅線)
原本的實作會在每次迴圈判斷需要相加的數值,這麼做的優點是只需寫一個迴圈就能完成計算,但缺點是每次迴圈都有兩個 branch 要判斷。為了改善這點,改為使用兩個迴圈進行計算,第一個迴圈先計算兩者皆有資料的範圍,再於第二個迴圈處理 carry 與剩餘的資料範圍。另外,藉由無號整數不會 overflow 的特性 (modulo),可以進一步避免使用 __int128
(bn_data_tmp
) 進行計算
bn_mult
的效能改回量測 bn_fib_fdoubling
作為判斷依據,並接續上述 fast doubling v4 版本,將測試範圍提高至 F(10000),會發現 bn_mult
的效能明顯低於對照組
原本實作 bn_mult
的方式為依序將兩格陣列相乘,再將結果直接疊加至輸出的變數,然而這會導致每行乘法被拆分成 2 個步驟 (相乘後先將 carry 疊加至下個 array,下次迴圈又再從該 array 取出數值來進行乘法),降低計算的速度。接下來參考 bignum/mul.c 來改寫 bn_mult
,改為一次將乘積與 carry 疊加至輸出的變數來提升效能
sysprog21/bignum 中使用內嵌組合語言 (inline assembly) 來直接取得乘法運算的高位與低位,直接使用一樣的方式實作乘法,取代原本使用的 __int128
(bn_data_tmp
)
__int128
好的效能表現,主因是沒辦法藉由使用 __int128
直接把乘積的高位與低位儲存至指定的空間bn_sqr
考慮上述 的計算過程,會發現數值 、 與 各會重複一次,因此可先計算對角線其中一邊的數值,將數值的總和直接乘二,最終再加上對角線上的 、 與 。藉由這種方式,平方運算的成本可由本來的 次乘法降為 次乘法
結果如下 (v7 藍線)
雖然上述 v7 版本所花的時間已略低於參考組,但若將量測範圍逐漸提高,會發現效能仍不及參考組,至 時差距約有 1 倍,觀察 sysprog21/bignum 的原始碼會發現使用 Karatsuba algorithm 來加速乘法與平方運算,因此接下來一樣實作該演算法來提升效能
Karatsuba algorithm 的核心概念是將 a 與 b 拆分為高位與低位再進行計算,考量計算 ,且 a 與 b 的位數皆為 位 (2 進位下的位數,不過 10 進位時邏輯相同),可將 a 與 b 表示如下
因此 可進一步整理為
由於 可藉由 bit shift 達成,因此實際使用乘法的部分只剩 3 項,遠少於直接使用乘法的 項,可大幅度降低乘法運算的成本
將 Karatsuba multiplication 應用至 bn_mult
與 bn_sqr
後,效能如下 (v8 藍線)
圖中設定的閾值與參考組一樣,但縮小閾值,在數值長度較小時,不會顯著提升效能