contributed by < r34796Kevin >
實作Jserv老師的Linux 核心設計 (Linux Kernel Internals)的作業3,並且盡量完成作業的額外要求
研讀上述 Linux 效能分析的提示 描述,在自己的實體電腦運作 GNU/Linux,做好必要的設定和準備工作 從中也該理解為何不希望在虛擬機器中進行實驗;
研讀上述費氏數列相關材料 (包含論文),摘錄關鍵手法,並思考 clz / ctz 一類的指令對 Fibonacci 數運算的幫助。請列出關鍵程式碼並解說
複習 C 語言 數值系統 和 bitwise operation,思考 Fibonacci 數快速計算演算法的實作中如何減少乘法運算的成本;
研讀 KYG-yaya573142 的報告,指出針對大數運算,有哪些加速運算和縮減記憶體操作成本的舉措?
lsmod
的輸出結果有一欄名為 Used by
,這是 "each module's use count and a list of referring modules",但如何實作出來呢?模組間的相依性和實際使用次數 (reference counting) 在 Linux 核心如何追蹤呢?
注意到 fibdrv.c
存在著 DEFINE_MUTEX
, mutex_trylock
, mutex_init
, mutex_unlock
, mutex_destroy
等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題。嘗試撰寫使用 POSIX Thread 的程式碼來確認。
在 GitHub 上 fork fibdrv,目標是修正 Fibonacci 數計算的正確性 (現有實作存在缺陷,請指出),隨後改善 fibdrv
計算 Fibinacci 數列的執行效率,過程中需要量化執行時間,可在 Linux 核心模組和使用層級去測量
/dev/fibonacci
裝置檔案來變更不同的 Fibonacci 求值的實作程式碼。逐步最佳化 Fibonacci 的執行時間,引入費氏數列分析 提到的策略,並善用 clz / ffs 一類的指令 (Linux 核心有對應的包裝函式),過程中都要充分量化
嘗試研讀 bignum (fibdrv
分支) 的實作,理解其中的技巧並與你的實作進行效能比較,探討個中值得取鏡之處。
Fibonacci數列定義為
考量到Processor affinity,讓行程 (process) 在特定的 CPU 核心中持續執行,不受作業系統排程的干擾。
查看指定行程的處理器親和性,可使用 taskset
加上 -p
參數再加上該行程的 PID
(process ID):
目前PID 1的行程可以被作業系統排程安排在CPU0~7上。
修改 /etc/default/grub
內的 GRUB_CMDLINE_LINUX_DEFAULT
,加入 isolcpus=7
來指定編號 7 的核心不被排程器賦予任務,修改完成後需 sudo update-grub
來更新設定,重開機後即生效。
重新使用taskset -cp 1
查看,目前PID 1的行程可以被作業系統排程安排在CPU0~6上。
透過指定處理器親和性 (processor affinity,亦稱 CPU pinning),可以將程式固定在特定的 CPU 中執行
在 Linux 核心模組中,可用 ktime 系列的 API;我挪用read function
來輸出上一次 fib 的執行時間。
在 userspace 可用 clock_gettime 相關 API;
在Linux man page中提到
在這邊我使用CLOCK_REALTIME
會產生不正確的結果如下:
如果使用CLOCK_MONOTONIC
可以修正這個問題:
針對 SMP IRQ affinity 進行設置,盡量避免 CPU 7 去處理 IRQ。
將userspace
得到的時間減去kernelspace
得到的時間,可以測量System Call
的時間開銷,我測得的時間約為340ns。
到這裡就能正確進行效能分析的實驗,然而儲存Fibonacci數列的資料型態為long long
,只能儲存64位元的資料,因此F92之後的數值會產生overflow,於是接下來實作可以儲存任意精度的數值系統(Big Num)
由於計算Fibonacci數列只會用到正整數運算,所以接下來的數值系統實作中運算子(加減乘除)都是針對正整數做運算。
為了能夠儲存任意精度,定義一個資料結構bigN
,unsigned int *d
指向配置的記憶體位置,int size
紀錄使用了幾個unsigned int
來儲存數值。
有了資料結構後,要使我們儲存的資料有意義,要可以將儲存的資料轉換為看得懂的數值,實作一個function將bigN
轉換成string
。
若一個bigN
使用了x個位元儲存資料,最多可以換算成位數,
e.g. 若一個bigN
使用了8個位元儲存資料,最多可以儲存也就是 = 3位數。
二進位轉十進位的時候,使用一個mask
一一取出most sinificant bit,取出下一個bit的時候要將之前的數值乘2才能正確解讀數值,e.g. 二進位數值1101 取出第一個bit 1,此時string儲存1,取出第二個bit 1,此時string應儲存,取出第三個bit 0,此時string應儲存,取出第四個bit 1,此時string應儲存。
由於bigN
是儲存記憶體位置,故需要搭配使用bigNinit
函式配置記憶體位置以及數值。
value
為儲存的數值。
兩個bigN
相加時有可能進位,故要先配置一個大小為newsize
的記憶體位置來避免overflow。
實作兩個uint32_t
相加的時候,可以用一個小技巧,用一個uint64_t
來儲存結果,再使用mask跟位元移位來得到正確的32位元數值與它的進位。
減法的實作也差不多。
目前採用最簡單的 long multiplication,就像手算乘法一樣疊加上去,輔助函式 bigN_mul_add
負責將每一行的計算結果疊加上去。
釋放bigN記憶體。
交換兩個bigN的數值。
比較兩個bigN的數值,並返回A=大的,B=小的。
做加法或乘法時為了避免overflow會多分配記憶體空間,bigN_resize
可以調整記憶體空間為剛剛好。
有了數值系統之後,就可以實作大數版本的Fibonacci數列,先實作一般的Fibonacci運算。時間複雜度為O(n)。
如此一來便可以正確計算之後的數值,試著計算第500項得到的數字與The first 1001 Fibonacci Numbers相同。
研讀Calculating Fibonacci Numbers by Fast Doubling 後了解到還有一種方法可以時間複雜度O(logn)來計算Fibonacci數列。
Fibonacci數列定義為
接下來實作大數版本的Fast Doubling。
重新測量可以看到運行時間大幅減少,Iterative方法時間複雜度為O(n),Fast Doubling方法時間複雜度為O(logn)。
這邊已經大幅降低計算Fibonacci數列的時間,接下來在Fast Doubling方法下繼續做最佳化。
許多現代處理器提供 clz / ctz 一類的指令,可搭配上述 Fast Doubling 手法運用:
這樣可以更求出leading 1 bit在右邊數來第幾個bit。
因為這個運算在整個大數版本的Fast Doubling演算法中佔比很小,所以得到的加速很小。
Fast Doubling運算中需要求出2f1的運算可以使用左移運算取代。
紫色的線段使用上了左移運算以及硬體支援的clz運算,得到了更進一步的加速。
原本的運算上都直接分配一個新的記憶體空間,將舊的記憶體空間釋放,頻繁的這樣操作導致效能下降。例如:
所以我重新寫過所有的運算,如果非必要(例如值會溢位),否則都寫入原本分配的記憶體空間。
並且使用新的資料結構加入memory pool的概念
一開始就分配比較大的陣列給資料,減少值會溢位的情況。capacity紀錄總共可以使用的陣列,size紀錄目前的資料需要幾個unsigned int來儲存。
在計算比較小的(200)Fibonacci數列計算效率可以有 50% 的提昇。
在計算比較大的(1000)Fibonacci數列計算效率也有 20% 的提昇。
(因為Fast Doubling方法時間複雜度為O(logn),數列越大提昇越有限)。
在Iterative方法中也是50%的提昇。