contributed by < hsuedw
>
編譯 fibdrv
時,發現下列警告訊息。雖然 fibdrv
仍可安裝並計算 Fibonacci number。不過,既然 Linux kernel 不允許使用 variable length array (VLA),那就把先把這個警告訊息處理掉。
目前採用的解法是用動態規劃 (Dynamic Programming) 裡的狀態壓縮技巧處理。原本的 fib_sequence()
裡有個 f
陣列,用來計算 f[k]
。這樣做的時間與空間複雜度都是 O(k)。不過,仔細想想,以 buttom-up DP 實作的演算法只需要 f[k - 1]
與 f[k - 2]
就可以計算出 f[k]
。所以不需要一個長度為 k
的陣列。所以改用下列實作,既可避開禁止使用 VLA 的警告訊息,又可降低空間複雜度到 O(1)。
fibdrv
遇到的第一個要解決的問題就是因為 C 語言內建的 long long
資料型態所能表示的最大值為 263 - 1 = 9,223,372,036,854,775,807。導致在計算 F93 出現溢位 (overflow)。
F93 = 15080227609492692858
就算是改用 unsigned long long
最大值也只能是 264 - 1 = 18,446,744,073,709,551,615。勉強可以用來計算 F93。但是作業的要求是至少要計算到 F100,甚至是更後面的 Fibonacci number。所以必須要使用自訂資料型態實作 big number。不過,若是以目前 fibdrv
所採用的 VFS,實作 open
、close
、read
、write
等 system call 的方式將 fibdrv
計算出來的 Fibonacci number (用自訂資料型態實作 big number 表示) 困擾。在用 VFS 的實作前提下,可能的解決方案有兩種。
ioctl
這個 system call。這樣可以一次把自訂 big number 型態的 object 讀到 user space 中。不過,這樣做的話, fibdrv
必須向 client
揭露 big number 資料型態定義與其他相關實作細節。client
所提示的實作方式,用 lseek
與 read
兩個 system call 搭配操作。先用 lseek
設定準備要讀取的資料的位置,再用 read
把指定的資料讀出來。一次讀一個 long long
的資料,直到把自訂 big number 資料結構中記錄 Fibonacci number 的所有 long long
讀完為止。這兩種做法對 client
而言,都不是很友善。因為 client
必須重組資料才能得到完整的 Fibonacci number。而且必須揭露許多 fibdrv
的實作細節給 client
。會加深 fibdrv
與 client
間 coupling 的程度。
比較好的做法應該是 client
能夠直接讀到計算完成的 Fibonacci number。並且不要揭露任何關於 fibdrv
內部的實作細節給 client
。所以我打算使用 sysfs 介面。
目前打算在 sysfs 下產生三個檔案。藉以向 client
揭露 fibdrv
的計算結果。
/sys/kernel/fibonacci/input
: client
在此檔案中寫入想要計算的 Fibonacci number 的項數。例如,寫入 10 表示想要計算 F10。/sys/kernel/fibonacci/output
: fibdrv
計算的 Fibonacci number。例如,F10 = 55。/sys/kernel/fibonacci/time
: fibdrv
計算 Fibonacci number 所花費的時間。本階段目標
fibdrv
可用原本的迭代演算計算 Fibonacci number (F92 以上)。big number 資料結構
num
與 sz
兩個欄位。num
是指向一個 kmalloc()
動態宣告的記憶體空間。其大小為 BIGNUM_SZ * sizeof(NUM_TYPE)
。目前 BIGNUM_SZ
設定為 500
。NUM_TYPE
定義為 uint32_t
。以目前這樣的實作可以算到 F23000。big number 加法運算
TMP_TYPE
被定義為 uint64_t
。NUM_TYPE
被定義為 uint32_t
。而 a->num[i]
的型態為 NUM_TYPE
也就是 uint32_t
。a->num[i]
) 與加數 (b->num[i]
) 均被轉型為 uint64_t
。然後在第 9 行判斷變數 tmp
是否大於 uint32_t
所能表示的最大值決定是否有發生進位。big number 轉字串。在字串中以十六進位表示 Fibonacci number
big number bitwise left shift
big number 減法運算
big number 乘法運算
Debug tools:
參考實作:
在 fibdrv.c
中使用 ktime
量測每次計算 Fibonacci number 所花費的時間。單位是 nano second。我把實作 fast doubling (fib_fast_doubling()
) 與 iteration (fib_sequence()
) 的函式原型設計得一樣,然後利用函式指標 (function pointer) 依據不同的需求將不同的演算法送入 fib_time_cost()
中。
在 client.c
中使用 clock_gettime()
量測讀取 Fibonacci number 所需花費的時間。單位是 nano second。
這個階段的目標是要透過下列方法簡化實驗流程。
sysfs
中新增 /sys/kernel/fibonacci/algo
。利用它來動態調整計算 Fibonacci number 的演算法。
fibdrv.c
中,實作 iteration 與 fast doubling 的函式原型如下所示:
fib_algo
動態指向上述兩個函式中的一個。fib_algo
預設指向 fib_fast_doubling
。
/sys/kernel/fibonacci/algo
分別寫入 "iteration"
與 "fast-doubling"
就可以控制計算 Fibonacci number 的演算法。
執行 taskset
並使用 -cp
參數再加上 process ID 查看指定行程的處理器親和性(CPU affinity)。已執行結果看來,我可以使用 CPU 0~3。
接下來使用 boot loader 所提供的自訂開機參數功能,手動加入 isolcpus=cpu_id
參數。讓特定的 CPU 核心在開機時就被保留下來,不讓其他的行程干擾我要執行的程式。我先保留 CPU 0。首先切換目錄到 /etc/default
,然後備份 grub
。
然後修改 /etc/default/grub
中的 GRUB_CMDLINE_LINUX_DEFAULT
參數。在此參數後加上 isolcpus=0
也就是保留 CPU 0 給特定程式使用。
執行下列指令更新 boot loader 的開機參數。
先檢查 /sys/devices/system/cpu/isolated
的內容,應該要是空的。
然後,重新開機。
待重新開機完成後,在檢查 /sys/devices/system/cpu/isolated
的內容。應該要是 0。也就是我剛剛在 /etc/default/grub
中用 isolcpus=0
保留下來的 CPU。
接著就可以用 taskset
指定 CPU 來執行程式。在這個作業中,執行 client
並透過 sysfs
介面操作 fibdrv
計算 Fibonacci numbers。
將這個動作整合到 Makefile
中。以簡化後續的實作流程。以下是在 Makefile
中新增的兩個 rule。
暫時抑制 address space layout randomization (ASLR)
設定 scaling_governor 為 performance。準備以下 shell script,檔名為 performance.sh
:
然後再用下列執行 performance.sh
。
針對 Intel 處理器,關閉 turbo mode
將上述這些排除干擾效能分析的因素的動作全部整合進 Makefile
以簡化後續實驗流程。
參考資料: 排除干擾效能分析的因素
以下列這個 commit 的程式碼搭配本文一開始所描述的環境做實驗。
commit: ab44371
branch: perf.built-in-type
執行下列指令畫出 iteration
與 fast doubling
的效能比較。
結果會畫在 fibdrv_perf.png
中。
Iteration 演算法的時間複雜度為 O(n),fast doubling 演算法的時間複雜度為 O(logn)。由上圖的實驗結果可看出這個趨勢。
由作業說明 (H03: fibdrv, 費氏數列) 中的虛擬碼與示範案例,以及Calculating Fibonacci Numbers by Fast Doubling 的說明可窺見,以 fast doubling 計算 Fibonacci number 可對所欲求取的項數的二進位表示法決定計算步驟。所以可以利用 gcc
提供的 __builtin_clz()
減少尋找 MSB (most significant bit) 的時間。
commit: ab44371
branch: perf.built-in-type
以下列這個 commit 的程式碼搭配本文一開始所描述的環境做實驗。
commit: 2b8c55a
branch: master
執行下列指令畫出 iteration
與 fast doubling
的效能比較。
結果會畫在 fibdrv_perf.png
中。
commit: 2b8c55a
branch: master
由實驗數據看來,原本的 iteration 演算法隨著項數的增長,所花費的時間以線性關係成長。而 fast doubling 演算法的曲線則是呈現類似對數 (logarithm) 的關係。雖然兩個演算法的時間複雜度與理論吻合,可是fast doubling 所花費的時間卻比 iteration 來的高。以這個趨勢來看,當項數足夠大的時候,iteration 所花費的時間會超越 fast doubling。
不過,已經算到 F23000 了 fast doubling 所花費的時間仍然比 iteration 高。看來我實作的 big numger 還有很大的改善空間。
搭配閱讀 The Linux driver implementer’s API guide » Driver Basics