依據 fibdrv 作業規範,繼續投入 Linux 核心模組和相關程式的開發。
原先的 fibdrv
因為 uint64_t
的限制,僅能計算至 ,需要提供新的資料結構以進行計算和儲存之用。
參考作業說明 - 基於 list_head 的大數運算, 資料結構使用 linked-list,並以 linux 的 list.h
進行實作:
bn_head
僅儲存該鏈結串列的長度 ,而 bn_node
則以 uint64_t
的格式儲存資料。若以一個鏈結串列儲存一個大數,則可以將該鏈結串列看成一個有 bits 的數。
經過測試,此資料結構可計算至 (以 wolfarmalpha 作為基準)
bn_new
以 Binet formula 可以近似出 的值,將其取 即可計算所需要的 bn_node
的數量,可在一開始便配置好記憶體。
bn_add
bn
的加法bn_add
會將第二個 bn
的值加至第一個 bn
,以 __bn_add
進行操作:
__bn_add
則為 bn
加法的實作:
bn_sub
由於在計算 Fibonacci 數時不會出現負數,bn_sub
的實作是將兩數比較後將較大的 bn
減去較小的 bn
:
bn_mul
bn_mul
會將兩個 bn
得相乘結果儲存在另外的 bn
中。此實作僅將每個 bn_node
分別作相乘並將結果加至對應的位置中,時間複雜度為 :
bn_lshift
和 bn_rshift
由於 bn_node
中所儲存的資料為 uint64_t
, 一次左移或右移的操作最多僅能移動 63 bits,實作中會暫存所要移動的 bits 數量,以每 63 bits 作為單位分次移動:
bn_to_array
為了減少 copy_to_user
所需複製的大小,在複製前會將 bn
中的資料儲存至陣列:
fibdrv
效能Fibonacci 數的定義為:
其運算之時間複雜度為 ,程式實作如下:
而fast doubling
的方法,可以將時間複雜度減少至 ,以下則參考了作業解說中的 Bottom-up 方法進行實作:
將輸出時間使用 gnuplot 作圖,可以看出兩者之間的耗時相差甚巨(僅使用 ktime
測量上述函式所用時間):
copy_to_user
的傳送量bn_to_array
會將 bn
轉換為 uint64_t
的陣列,若單純使用 copy_to_user
複製該陣列, 在其儲存的元素所使用的空間小於 64 bit 的情況下將會多複製了不必要的空間。
針對 little-endian 架構,非零的位元組會被存在較低的記憶體位址。以 為例,需要兩個 uint64_t
來儲存,非零的位元組數為 9 個:
參考作業說明中的方法,使用 gcc
內建的 __builtin_clzll
計算陣列最後一元素的 leading zeros 之後僅從陣列複製剩餘的非零位元組(以上述例子來說便是 9 個):
其中 lbytes
為避免 src
為僅一元素 0 的狀況,須額外判斷並保留至少一位元組。
可以用 dmesg
指令確認所計算之位元組數結果:
在 client.c
中可初始化 buf
為 uint64_t 的陣列並傳入 read
作為複製的目的地,在輸出時將其轉換為字串即可,程式碼如下:
參考 yanjiew 探討系統環境的設定,使用 cset
進行 cpu 的獨立:
將執行緒指定於獨立出來的 cpu 執行:
參考作業說明中的 python script 並引入 scripts/preprocess.py中,假設資料分佈為自然分佈,將兩個標準差之外(即 的信賴區間)的數據去除後計算其平均值並作圖, 程式碼如下:
閱讀 fibdrv 作業規範,包含「作業說明錄影」和「Code Review 錄影」,本著「誠實面對自己」的心態,在本頁紀錄所有的疑惑,並與授課教師預約討論。
過程中,彙整 Homework3 學員的成果,挑選至少三份開發紀錄,提出值得借鏡之處,並重現相關實驗。
Q: 依據作業說明中的解釋, 此演算法是將大數分成小數字後將小數們構成的向量線性捲積最後將其進位,此算法看似跟長乘法相似,不知差異為何?
A: 線性卷積可使用 FFT 和 iFFT 進行計算;在考量定義域的情況下,也可以使用數論轉換在整數環上計算。
回答「自我檢查清單」的所有問題,需要附上對應的參考資料和必要的程式碼,以第一手材料 (包含自己設計的實驗) 為佳
理解其中的技巧並導入到 fibdrv 中,並留意以下:
參照 Schönhage–Strassen algorithm,在上述大數運算的程式碼基礎之上,改進乘法運算,確保在大多數的案例均可加速,需要有對應的驗證機制。
以下為一個 進位的直式乘法 :
假設 ,可以將計算的結果進行進位以得到 :
若將 視為一長度 的序列 , 視為長度 的有列 ,上述計算便可以視為兩序列的 linear convolution:
其結果則為一長度 的序列
在此介紹另一種卷積 circular convolution,定義如下:
可以發現關係式與 linear convolution
相似,主要的差別在於序列 有一週期 。若對 和 作 circular convolution
可得到長度 的序列 :
可以發現原先在 linear convolution
中超過 的項( *
號項目) 繞回了序列的尾端。
由此可知若將 和 兩序列補上 個 延伸成長度 的序列並對他們作 circular convolution
(週期 ):
其運算結果等同於對兩序列作 linear convolution
(須將長度補到至少 ,補 的動作稱為 zero padding
)。
根據 convolution theorem,兩序列的 circular convolution 會等於兩序列的 discrete fourier transform (DFT) 進行 element-wise multiplication
後再進行 inverse DFT
,即:
而 DFT
可由 FFT
演算法進行加速。
若要計算 ,其流程為:
zero padding
FFT
計算 和 Schönhage–Strassen algorithm
遞迴地計算 FFT
計算 標準的直式乘法是一項一項相乘, 因此時間複雜度為 。
若假設將一長度 位元的數分成 個 位元的段落,使用 Schönhage–Strassen algorithm
計算時間複雜度為: ,推導如下:
設計算時間為
其中:
ntt
和 intt
中的乘法的運算時間(遞迴計算)ntt
和 intt
中的加減法的運算時間假設 , ,可得:
可以分為三種情況:
因此在 時為最佳解;在 為奇數時可選擇 或 ,並不會影響最終的時間複雜度。
參考〈快速傅立葉轉換〉一文關於求解多項式函數相乘的方法:
設一多項式 ,可以用 個點 來表示這個 階多項式;反之若給定 個 , 可以用計算出的的 個 反推回原式(此時稱該多項式 為 )。
若要計算兩個 階多項式相乘 , 我們可以在 和 上分別找出 個點 和 ,如此一來 便可以用 表示,計算的時間複雜度可以由將 和 各項係數相乘的 縮減為 。
我們可以將一個數字的二進位表示拆解成一個多項式 ,例如:
因此大數相乘問題其實可以看成多項式相乘問題,在計算完後將 代入適合的 即可 (實作上以 bit shift 進行)。
以下將以 radix-2 DIT(Cooley–Tukey FFT algorithm) 為主
DFT
的公式為:
取 在 上的點 便可以化成 DFT
型式:
其中 為 的 個根,並具有以下性質:
假設 為 的冪,若將 以次方的奇偶數分成兩部份:
令:
則 可表示為:
由上述性質3可知 :
可以發現 被拆成了 和 兩個子問題,但其 縮減為 ,因此可以將他一路分解到 為 (此時直接回傳係數即可)。
在合併時,假設已計算出大小為 的傅立葉轉換 和 ,則合併後的結果 為:
而透過性質4.可以發現:
每次迭代時計算 和 便可以同時計算 與 ,須計算的 數量變成了一半,因此時間複雜度為。
假設 ,演算法的遞迴關係如下面的樹狀圖所示:
可以發現所有 children 是按照特定順序排列的 (將該 child 的 index 的位元反轉,如 ),若事先將其排列好,可以將其變成 bottom-up 的實作,參考〈Cooley–Tukey FFT algorithm: Data reordering, bit reversal, and in-place algorithms〉的虛擬碼:
兩者的比較如下圖:
The discrete Fourier transform is an abstract operation that can be performed in any algebraic ring; typically it's performed in the complex numbers, but actually performing complex arithmetic to sufficient precision to ensure accurate results for multiplication is slow and error-prone.
FFT
中對複數的操作需要大量浮點數計算,若需要計算到一定的精準度將會消耗極大的運算資源,另外在作業說明中也提到了核心文件中不建議使用浮點運算。由於 DFT
的性質大部分是基於 () 的性質,因此若是能在體之下能找到單位根也能夠進行運算。NTT
是 DFT
的特例,指的是在有限體之下所進行的 DFT
。
參考<數論轉換>一文,考慮一有限體 , 是質數,首先尋找 滿足:
在 和 互質的情況下,根據費馬小定理 ,可以推論:
也就是 為 的原根,如此一來有限體中的每個數皆可用 的冪表示。 裡的 階單位根 ()可以用 表示,將上面式子中的 替換成 即可,:
NTT.h
(commit d3385bc)ntt
ntt
使用了 iterative FFT
演算法,參考了上面的虛擬碼進行實作,在求解 wm
時使用快速冪。
intt
IDFT
的公式為:
公式與 DFT
非常類似,僅有兩點需要更改:
ntt
相同,程式碼如下:bn_strassen
由於數論轉換的限制,計算出的結果將落在有限體之內。為避免模數溢出,這裡將分割的大小 固定為 位元,同時不使用遞迴來計算 ,因此時間複雜度將提昇為 ,實作程式碼如下:
由於乘法的時間複雜度為其所佔位元數的函數,可以將乘數和被乘數初始化為 1 ,透過左右移的方式控制乘數和被乘數的位元數,並計算其 trailing zeros
來驗證乘法結果的準確性。測試程式如下:
測試結果如下:
可以觀察到 bn_strassen
在計算超過大約 170000 位元的數之後表現比起直式乘法有顯著的改善,而根據 Binet Formula:
因此可以在需要計算至 時將乘法計算用 bn_strassen
代替。