2023q1 Homework3 (fibdrv)
開發環境
作業要求: L04: fibdrv
基本觀念研讀筆記在這邊
自我檢查清單及作業要求
自我檢查清單
為什麼不該在虛擬機器中進行實驗
KVM、Hypervisor、What Is a Hypervisor? Types, Use Cases, and Career Opportunities
Hypervisor
又稱 Virtual machine monitor (VMM) ,用以建立與執行虛擬機器 (VM) 的軟、韌或硬體,可分為兩種形式如下圖
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
- Type 1: native or bare-metal hypervisors
- 直接運行於硬體上 (也就是需要硬體 support),Hypervisor 和 CPU 之間不存在額外的 OS layer
- 優點:
- 缺點:
- 需要一台獨立於主機的硬體來負責管理虛擬機的環境以及資源
- TYPE 2: hosted hypervisors
- 運行於 OS 上,就像是平常使用到的應用程式一樣,所以必須在額外透過硬體的 OS 才能訪問記憶體
- 優點:
- 安裝快速,易於使用 (user-friendly)
- 缺點:
Discussion
本次作業一大重點為效能分析,而間接記憶體存取 (type-2 hypervisor) 會大幅的影響效能,這也是為何不該使用虛擬機器進行本實驗。
但 type-1 的 hypervisor 可以直接存取記憶體,或許本次作業的效能分析容許使用 type-1 hypervisor ? 例如透過 Hyper-V 建立 Linux 環境進行?
perf
會去呼叫 PMU 共用硬體下時間量測沒有參考價值,記憶體存取只是其中一個問題
時間測量
參照 核心模式的時間測量、Linux 核心設計: Timer 及其管理機制
如果只在 user space 做測量,那就只能測量進出 kernel 內部的時間,測量不到 kernel 內部,進出 kernel 其實有其他的成本開銷 (例如 mode switch)
參考 The high-resolution timer API 引入 ktime_t
commit 218dfe3
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
由圖可見 fast doubling 的 iteration 版本比直觀實作的 iteration 版本快上許多,但 recursion 版本卻比原版慢。
排除干擾因素
參照Linux 效能分析的提示、KYG-yaya573142、研讀筆記
修改 /etc/default/grub
使編號 0
的 CPU (用 system monitor 看的話是 CPU 8) 不被排程器干擾
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
將程式固定在特定的 CPU 中執行
比對一下設定 processor affinity 前後差別 (以 normal interation 和 fast doubling iteration 舉例)
在關閉 SMT (Hyper-Threading) 時發現一關掉 CPU 5 至 CPU 8 就會停止運作,檢查了一下規格 (Intel® Core™ i7-7700HQ),發現核心數量為 4、執行緒數量為 8
- 原來 system monitor 上顯示的 CPU 數量是執行緒的數量
未指定 CPU、未排除干擾效能分析的因素
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
指定 CPU、 排除干擾效能分析的因素
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
加速 Fibonnaci 運算
Fast doubling
commit 05fd783
參考作業說明以及硬體加速章節,將 fast doubling 手法引入
以計算 為例
每一次的迭代都會需要 以及 的值,其樹的深度 (不含 root) 為 向下取整, 也就代表了樹的深度 (不含 root),而觀察 11
轉換為 2 進制為 0b1011
其介於 0b1000=8
以及 0b10000=16
之間,其向下取整對應到 0b1000
,可透過型態長度減掉 leading bit 的數量再減 1 取得,也就是向下取整後 0b1000
的 tailing bit 數量,可對應到作業說明提供的範例中計算 count
的方式
引入 Bignum
引入作業說明、實作程式碼中的 bn
結構體,使其能夠計算 之後的數值
為了驗證引入後是否能夠正確計算 Fibonacci number,必須修改原專案中的測試程式 (筆記) commit 9c8ea72 包含以下修改
- 修改
client.c
以及 fibdrv.c
以計算至 的值
- 修改
expected.txt
使其在與 Makefile
中的 diff
命令判斷時使用
- 修改
verify.py
使其驗證至第 1000 項
- 修改
pre-commit.hook
: 新增 --inline-suppr
讓程式可以 inline suppressions (在 client.c
sz
回傳 copy_to_user
不可被 copy 的值 but not use,後續應新增條件)
且 verify.py
沒有顯示任何訊息 (代表通過)
效能分析及改善大數運算
筆記
在 measure.c 使用 clock_gettime
量測 user-level 所花的時間,再透過 user-level 所花的時間減去 kernel-level 所花的時間 (fobdrv
中使用 ktime
) 來計算 copy_to_user
所耗費的時間
bignum (iteration method)

透過以下指令將 Hyper-Threading 關閉後可以明顯改善

在此我指定只有 CPU 0 可以執行我的程式,但在 Hyper-Threading 開啟的情況下 CPU 0 其實有兩個執行緒在同時運作 (這也是 intel 想要做到的,在 CPU 內部僅複製必要資源,讓兩個執行緒可以同時執行,以此在一個單位時間內處理兩個執行緒的工作以類比真正的雙核心),故猜測未關閉 HT 的凸起可能是因為其 CPU 內部資源被另一條執行緒給搶用了
如果我一邊播影片且不關閉 HT 測量會更明顯,如下圖

在計算 時可以比較明顯的看出階梯狀的情況(待分析)

bignum (fast-doubling within iteration)

與 正常 iteration 版本比較

- 可以看到大約在 之前 iteration 版本是比較快的,而之後 fast doubling 比較快 (待分析)
改寫 bn_fib_fdoubling
Reference
參考資料提及的方案在直接引入時遇到一些 Bug,解決過程放在筆記中
新增改善後函式,可透過編譯時定義巨集來指定演算法,以本節為例
commit 4448082
在 bn
中定義新的 element 用以管理 bn
結構體的可用大小,以此來減少 bn_resize
呼叫 krealloc
的次數
其中以 k 為單位配置更大的空間可以透過修改巨集 ALLOC_CHUNK_SIZE
達成。
commit 1740327
引入根據處理器的位元數結構體
其中在 64 位元處理器下 bn_data
定義為 uint64_t
,而再進位時需要用到雙倍的空間所以使用 gcc
支援的 __int128
來定義 bn_data_tmp
,並根據不同架構定義 #define DIGITS
的長度 (32
or 64
) 來配合大數運算。
而在 bn_do_sub
中原本的 carry
作為借位使用 long long int
定義,需為 128 位元有號數,故改為 __int128
。
而在大數運算的函式中也需根據資料型態的改變做調整,否則一個不小 overflow 或是進位條件不滿足導致 bn
沒有正確 resize
,都有可能至導致計算錯誤或者運算中直接被 kill
掉,也有可能直接當機。
重開機了無數次…
測試計算到 的效能

- 可以看到 user level 的花費時間明顯比 kernel level 多的多,代表著
kernel_to_user
的成本很大 (包含 copy_to_user
隨著傳輸資料增加所耗費的時間)
把 kernel level
的值抓出來看

改善 bn_mult
Reference
參考資料中提到, bn_mult
原本為依序將兩個陣列相乘在將結果跌加至輸出變數,也就是在緣版本中,將兩個陣列相乘,放入 carry
,在將其作為引數, 呼叫 bn_mult_add
更新 c->number
的值,而改善版本定義一個新的 function _mult_partial
將上述的兩個步驟縮減為一次做完。

inline assembly
Reference
在 x86-64
的架構下 rax
和 rdx
是 64 biits 的暫存器,且 mulq
的作用是將兩個 64 位元無號整數相乘,當在執行 mulq
指令時,第一個 引數為 rax
,第二個為 rdx
,在另執行後, rdx
暫存器會儲存高的 64 位元的結果,而 rax
暫存器會儲存低 64 位元的結果。

Fast doubling 過程中也去查找 hashtable
使用 hashtable 紀錄以計算過的值
查看目前為止最優化版本的 fast-doubling
line 7
to line13
負責 fast-doubling 的計算,for-loop 結束後會將使用者 request 的 存放於 f2
指向的地址,以計算 為例,這個 for loop 一共會跑三次,
- 第一圈 k=1, ,
- 第二圈 k=2, , ,此時因
n=10 (0'b1010)
, i=2 (0'b10)
- 此時結果為
f1 = F(2k)=3
,且 、 f2=F(2k+1)
=
- 第三圈在第二圈的時候已經計算出
f2
= ,並且會在 line 9
時計算出
在 line9
以及其相關運算中是為了找到 F(2k)
,在 line13
以及相關運算是為了找到 F(2k-1)
,在 line16
是為了找到 ,在計算前先進行查找 hash table 的動作,並且若不是從 table 中找到的,則將計算後的值放入 hashtable 中。
實作一個 resursion 版本的 fast-doubling 計算,並在函式內新增判斷是否存在於 hashtable 的函式,若不存在,進行計算並插入 hashtable
static void bn_fib_fdoubling_vhash(bn *dest, unsigned int n)
{
bn_resize(dest, 1);
if (n <= 2) {
dest->number[0] = !!n;
return;
}
bn *f1 = NULL;
bn *f2 = NULL;
unsigned int key1 = n >> 1;
unsigned int key2 = (n >> 1) + 1;
if(is_in_ht(key1)) {
f2 = hlist_entry(htable[key1].first, hdata_node, list)->data;
printk(KERN_DEBUG "f2: find offset %d value = %lld at p %p\n", (key1), f2->number[0], f2);
} else {
f2 = bn_alloc(1);
t1_node = kcalloc(1, sizeof(hdata_node), GFP_KERNEL);
if (t1_node == NULL)
printk("kcalloc failed \n");
bn_fib_fdoubling_vhash(f2, key1);
printk(KERN_DEBUG "f2: no find offset %d, push value %lld to hashtable %p \n", key1, f2->number[0], f2);
t1_node->data = f2;
INIT_HLIST_NODE(&t1_node->list);
hlist_add_head(&t1_node->list, &htable[key1]);
}
if(is_in_ht(key2)) {
f1 = hlist_entry(htable[key2].first, hdata_node, list)->data;
printk(KERN_DEBUG "f1: find offset %d value = %lld at p %p\n", key2, f1->number[0], f1);
} else {
f1 = bn_alloc(1);
t2_node = kcalloc(1, sizeof(hdata_node), GFP_KERNEL);
if (t2_node == NULL)
printk("kcalloc failed \n");
bn_fib_fdoubling_vhash(f1, key2);
printk(KERN_DEBUG "f1: no find offset %d, push value %lld to hashtable %p\n", key2, f1->number[0], f1);
t2_node->data = f1;
INIT_HLIST_NODE(&t2_node->list);
hlist_add_head(&t2_node->list, &htable[key2]);
}
bn *k1 = bn_alloc(1);
bn *k2 = bn_alloc(1);
bn_lshift_2(f1, 1, k1);
bn_sub(k1, f2, k1);
bn_mult(k1, f2, k2);
bn_mult(f2, f2, k1);
bn_swap(dest, k2);
bn_mult(f1, f1, k2);
bn_add(k2, k1, f1);
if(n & 1) {
bn_swap(f1, dest);
}
bn_free(k1);
bn_free(k2);
}
但在驗證計算 卻出現了預期外的結果
首先,遞迴運作的順序正確,但是在 line 4
第一次將 的值 2
放入地址 12bc4a60
時在 line 6
被檢測到存在於 hashtable 中,從中取值時竟然與放入的值不同,這點導致計算出來的結果錯誤。若將本段程式碼有關 hahs table 插入的部份註解掉可以通過 的 make check
檢測,推論在插入數值時並沒有按照預期的流程插入。
on going
Git Hooks and GitHub Action
首先在專案中透過 install-git-hooks
建立軟連結 (會連結到隱藏資料夾中的 .git/hooks
下的檔案),如此一來修改 scripts
裡面有連結的檔案就會像是在直接操作在 .git/hooks
中的目標。
之前一直沒發現在 push 上去之後 github action 會 run fail ,GitHub Action 測試的內容寫在 main.yml
中,以本次作業來說, push 上去後執行 make
以及 make check
- #3 中為沒有修改
expected.txt
以至於在測試時回報錯誤
- #9 中存在定義了函式卻未使用
- #18 中
k_free()
implicit function declaration 是因為沒有 include 到 slab
修正完成後在 #21 通過 GitHub Action 自動測試的檢查項目
觀察 Linux 核心模組若沒用到 mutex,會發生什麼問題
Note
將原先 client.c
用另外的 function 包裝起來供 pthread_create()
使用,在程式運行時第一個執行緒會負責運行 main()
,故在 main()
執行負責執行緒的 create and join。
commit fd26bb0
運行中為了查看執行緒 ID,可以使用 pthread_self()
獲得 POSIX 執行緒 ID,也可以使用 syscall(__NR_gettid)
系統呼叫獲得執行緒 ID,但這兩個值並不會一樣, POSIX thread ID 由 thread 來實作,而 gettid
回傳的執行緒 ID 則由 kernel 分配數字
What is the difference between pthread_self() and gettid()?
也可以使用封裝後的 gettid()
函式,但要記得 #define __USE_GNU
或 #_GNU_SOURCE
,因為 gettid
不是標準的 POSIX 函式,而是 GNU C library 提供的 extension
建立兩個 thread,並由 main
function 負責 create 和 join,同時使用 pthread_self
觀察其輸出。
- 可以看到 main function 共建立兩個 thread,而第一個進入
fibdrv
的執行緒 139658358945344
拿到 mutex lock,導致在執行緒 139658350552640
進入時在 fib_open
這邊會透過 mutex_trylock
回傳 EBUSY
(lock contension) 使其在測試程式呼叫 exit(1)
並結束,也可以用 sudo dmesg
查看訊息。
mutex_trylock: Try to acquire the mutex atomically. Returns 1 if the mutex has been acquired successfully, and 0 on contention.
- 這邊要注意的是 kernel 中的
mutex_trylock
回傳值正數代表成功 (鎖成功取的),而 0 代表失敗
pthread_mutex_trylock
0 的代表成功,非 0 代表失敗
這裡使用 mutex_trylock
而非 mutex_lock
,差別在於普通的 lock
操作該執行緒會一直等到鎖被釋放,若為 try_lock
,該執行緒就會立即返回一個失敗的值,可以避免等待,但是 trylock
的缺點在於當鎖被其他執行緒持有時,如果一直去嘗試獲取所,會增加 CPU 資源的消耗,但在專案中一旦 trylock
偵測到鎖被持有時就會返回 EBUSY
並使得測試程式 exit
。
嘗試把 exit(1)
註解掉並測試
Create thread ID = 140648573302336
Create thread ID = 140648564909632
TID = 140648573302336, Reading from /dev/fibonacci at offset 0, returned the sequence 0.
TID = 140648573302336, Reading from /dev/fibonacci at offset 1, returned the sequence 1.
thread ID - 140648564909632 Failed to open character device
TID = 140648564909632, Reading from /dev/fibonacci at offset 0, returned the sequence .
TID = 140648573302336, Reading from /dev/fibonacci at offset 2, returned the sequence 1.
TID = 140648573302336, Reading from /dev/fibonacci at offset 3, returned the sequence 2.
TID = 140648564909632, Reading from /dev/fibonacci at offset 1, returned the sequence .
TID = 140648564909632, Reading from /dev/fibonacci at offset 2, returned the sequence .
TID = 140648564909632, Reading from /dev/fibonacci at offset 3, returned the sequence .
TID = 140648564909632, Reading from /dev/fibonacci at offset 4, returned the sequence .
TID = 140648573302336, Reading from /dev/fibonacci at offset 4, returned the sequence 3.
TID = 140648564909632, Reading from /dev/fibonacci at offset 5, returned the sequence .
TID = 140648564909632, Reading from /dev/fibonacci at offset 6, returned the sequence .
TID = 140648564909632, Reading from /dev/fibonacci at offset 7, returned the sequence .
TID = 140648564909632, Reading from /dev/fibonacci at offset 8, returned the sequence .
TID = 140648564909632, Reading from /dev/fibonacci at offset 9, returned the sequence .
TID = 140648564909632, Reading from /dev/fibonacci at offset 10, returned the sequence .
TID = 140648573302336, Reading from /dev/fibonacci at offset 5, returned the sequence 5.
TID = 140648573302336, Reading from /dev/fibonacci at offset 6, returned the sequence 8.
TID = 140648573302336, Reading from /dev/fibonacci at offset 7, returned the sequence 13.
TID = 140648573302336, Reading from /dev/fibonacci at offset 8, returned the sequence 21.
TID = 140648573302336, Reading from /dev/fibonacci at offset 9, returned the sequence 34.
TID = 140648573302336, Reading from /dev/fibonacci at offset 10, returned the sequence 55.
join! thread ID = 140648573302336
join! thread ID = 140648564909632
- 同樣是兩個執行緒,不過在測試程式這邊沒有在偵測到已經被上鎖時直接
exit
,可以看到的確只有第一個獲取 mutex lock 的 執行緒 140648573302336
才有正確的返回值,而執行緒 140648564909632
為空。
- 也可以看到因為是使用
trylock
,所以在發現鎖被持有時不會一直等待,兩個執行緒是交替執行
將 mutex_trylock
替換成 mutex_lock
輸出為
Create thread ID = 140546622355008
TID = 140546622355008, Reading from /dev/fibonacci at offset 0, returned the sequence 0.
TID = 140546622355008, Reading from /dev/fibonacci at offset 1, returned the sequence 1.
TID = 140546622355008, Reading from /dev/fibonacci at offset 2, returned the sequence 1.
TID = 140546622355008, Reading from /dev/fibonacci at offset 3, returned the sequence 2.
Create thread ID = 140546613962304
TID = 140546622355008, Reading from /dev/fibonacci at offset 4, returned the sequence 3.
TID = 140546622355008, Reading from /dev/fibonacci at offset 5, returned the sequence 5.
TID = 140546622355008, Reading from /dev/fibonacci at offset 6, returned the sequence 8.
TID = 140546622355008, Reading from /dev/fibonacci at offset 7, returned the sequence 13.
TID = 140546622355008, Reading from /dev/fibonacci at offset 8, returned the sequence 21.
TID = 140546622355008, Reading from /dev/fibonacci at offset 9, returned the sequence 34.
TID = 140546622355008, Reading from /dev/fibonacci at offset 10, returned the sequence 55.
TID = 140546613962304, Reading from /dev/fibonacci at offset 0, returned the sequence 0.
TID = 140546613962304, Reading from /dev/fibonacci at offset 1, returned the sequence 1.
TID = 140546613962304, Reading from /dev/fibonacci at offset 2, returned the sequence 1.
Join thread ID = 140546622355008
TID = 140546613962304, Reading from /dev/fibonacci at offset 3, returned the sequence 2.
TID = 140546613962304, Reading from /dev/fibonacci at offset 4, returned the sequence 3.
TID = 140546613962304, Reading from /dev/fibonacci at offset 5, returned the sequence 5.
TID = 140546613962304, Reading from /dev/fibonacci at offset 6, returned the sequence 8.
TID = 140546613962304, Reading from /dev/fibonacci at offset 7, returned the sequence 13.
TID = 140546613962304, Reading from /dev/fibonacci at offset 8, returned the sequence 21.
TID = 140546613962304, Reading from /dev/fibonacci at offset 9, returned the sequence 34.
TID = 140546613962304, Reading from /dev/fibonacci at offset 10, returned the sequence 55.
Join thread ID = 140546613962304
- 可以看到雖然執行緒
140546613962304
在 140546622355008
運行時創建,但是因前者拿著鎖,故後者只能等待鎖的釋放,直到前者運算完 後將鎖釋放才換後者執行。
直接將 fibdrv.c
中的 try_lock
以及 unlock
註解掉後兩個執行緒都可以正常運作,不會發生 lock contension
Create thread ID = 140630458103360
TID = 140630458103360, Reading from /dev/fibonacci at offset 0, returned the sequence 0.
TID = 140630458103360, Reading from /dev/fibonacci at offset 1, returned the sequence 1.
TID = 140630458103360, Reading from /dev/fibonacci at offset 2, returned the sequence 1.
Create thread ID = 140630449710656
TID = 140630458103360, Reading from /dev/fibonacci at offset 3, returned the sequence 2.
TID = 140630458103360, Reading from /dev/fibonacci at offset 4, returned the sequence 3.
TID = 140630458103360, Reading from /dev/fibonacci at offset 5, returned the sequence 5.
TID = 140630458103360, Reading from /dev/fibonacci at offset 6, returned the sequence 8.
TID = 140630458103360, Reading from /dev/fibonacci at offset 7, returned the sequence 13.
TID = 140630449710656, Reading from /dev/fibonacci at offset 0, returned the sequence 0.
TID = 140630449710656, Reading from /dev/fibonacci at offset 1, returned the sequence 1.
TID = 140630458103360, Reading from /dev/fibonacci at offset 8, returned the sequence 21.
TID = 140630458103360, Reading from /dev/fibonacci at offset 9, returned the sequence 34.
TID = 140630458103360, Reading from /dev/fibonacci at offset 10, returned the sequence 55.
TID = 140630449710656, Reading from /dev/fibonacci at offset 2, returned the sequence 1.
TID = 140630449710656, Reading from /dev/fibonacci at offset 3, returned the sequence 2.
TID = 140630449710656, Reading from /dev/fibonacci at offset 4, returned the sequence 3.
TID = 140630449710656, Reading from /dev/fibonacci at offset 5, returned the sequence 5.
TID = 140630449710656, Reading from /dev/fibonacci at offset 6, returned the sequence 8.
TID = 140630449710656, Reading from /dev/fibonacci at offset 7, returned the sequence 13.
TID = 140630449710656, Reading from /dev/fibonacci at offset 8, returned the sequence 21.
Join thread ID = 140630458103360
TID = 140630449710656, Reading from /dev/fibonacci at offset 9, returned the sequence 34.
TID = 140630449710656, Reading from /dev/fibonacci at offset 10, returned the sequence 55.
Join thread ID = 140630449710656
使用 hashtable 紀錄以計算過的值
Linux 核心的 hash table 實作
預期引入 Linux 核心的 hash table 實作,並以 中的 n 作為 key,紀錄以儲存過的值
初步引入 hashtable (single thread)
commit #0bcdb63
引入 hlist
系列結構體存取已經計算過的值
另外,新增自定義結構 hdata_node
嵌入 hlist_node
並包含一個指向大數 bn
結構體指標,用以儲存 value 值。
直接將 中所要計算的第 個 Fibonnaci number 作為 hashtable 的 key,value 則指向 bn
結構體
呼叫 fib_read()
時先判斷 hashtable 該 key 值是否有值,若有值則直接從 hashtable 中取用,直接取用第一個因為在設計上直接將 作為 key 值,所以不會發生 collision,若沒有設計足夠大的 hashtable 就會發生 collision ,需要走訪該 key 值對應到的 list 的節點找出是否存在。
is_in_ht()
hlist_empty
該 key 值對應到的 list 是否有值。
執行 client
,測試程式為先計算 至 ,接著再反著計算回來,使用 printk
測試是否有正確運行
記憶體釋放
lkmpg 4.3 The __init and __exit Macros
The __exit macro causes the omission of the function when the module is built into the kernel, and like __init , has no effect for loadable modules. Again, if you consider when the cleanup function runs, this makes complete sense; built-in drivers do not need a cleanup function, while loadable modules do.
在使用 rmmod
卸載 fibdrv
模組時會呼叫帶有 __exit
macro 的函式, kernel 會將這個函式放入 read-only 的 __exit
section 中,
__section(".exit.text")
: 告訴編譯器使用 .exit.text
section
__exitused
: 告訴編譯器確保不會將還未使用的 __exit
函式從 executable file 中刪除
__cold
: 告訴編譯器 __exit
函式標記為不常用的函式,以進行更好的優化
notrace
: 不要在函式執行期間 trace
故需在帶有 __exit
macro 的 exit_fib_dev
進行記憶體釋放
走訪 hashtable 並釋放有 allocate 的記憶體,在 fib_read
中若在 hashtable 中找不到對應的值,則分配記憶體給 hdata_node
結構體以及其元素之一的 bn
以儲存大數,新增一個 function 來處理記憶體釋放。
並在帶有 __exit
macro 的 function 新增卸載核心模組的記憶體釋放操作。
hashtable 大數測試
測試計算 至 ,並從 至 的時間 ()

- 圖中測量的 x 座標不是 ,而是第 n 次的時間量測,x 座標
10000
至 20000
為由 至 的時間測量
在前 筆中計算 至 ,因 hash table 中都沒有值,所以和原本沒有實作 hash table 的趨勢一樣,而第 至 比測量中計算 至 ,因 hash table 中都已經有儲存計算過的值,所以直接取用即可
將後半段的資料獨立出來看 (found it in hash table)

可以看到雖然有高低起伏,但基本上已經是常數時間了,特別的突起原因不明,但因為數值 (y 座標) 很小,且在 hlist_entry
中的 contain_of
也是常數時間的函式,這邊特別突起的原因我認為只是量測的雜訊。
初步進行多執行緒測試
commit 661e543、commit da57624
前述提到在讀取時不會有 race condition 問題,但在執行 hash table 的寫入時須避免重複的寫入 (雖然以上面的設計來說不會有 collosion,但重複寫入顯得沒效率) 以及查找到值才做讀取,原程式的 mutex_trylock
置於 fib_open
避免多個執行緒同時 access 這個模組,一旦偵測到 mutex
被 lock 住則跳出並 exit
,修改並允許多個執行緒 access 這個模組,並保護讀寫 hash table 的值,避免執行緒 A 判斷 hash table 沒有值並進行 fibonacci number 的計算,但是在存入 hash table 之前被執行緒 B 給搶斷,以至於執行緒 B 也判斷 hash table 中沒有值,並進行 fibonacci number 計算
在 kernel module 中引入 sched.h
並印出當前 process ID, 參考輸出
- 在
line 2
至 line 8
由 thread 6160
先運行,因 hash table 中沒有值,所以會進行 fibonacci number 的運算並將計算後的結果放入 hashtable
- 在
line 9
中第二個執行緒 thread 6161
開始運行並查找 offset
為 0 存在於 hashtable 中就直接取用,不必進行 fibonacci number 的運算。
測試多執行緒運行
測試流程: 掛載模組 建立 10 個執行緒並運算 程式結束 在運行一次 建立 10 個執行緒並運算 卸載模組
第一次運行
- 初次運算的值會顯示
Not find offset
再次運算則會找到並直接取用
第二次運行
- 在第一次運行時,已建立好 hashtable,第二次運行時都可以直接從 hashtable 取用。
卸載模組
成功卸載沒有運行中的 thread 阻礙卸載。
測試多執行緒下的效能
測試案例為 10 個執行緒,各自計算至 的值,且用 pthread_mutex_lock
及 pthread_mutex_unlock
保護 user level 的時間量測,其中 kernel level 計算時間的範圍為 critical section 的範圍,若為有實作 hash table 版本則包含 找到 key
的 hlist_entry
找資料、沒找到 key
的節點空間分配、計算 Fibonacci number、初始化節點以及將節點插入 hash table。

- 10 個執行緒各自 1000 筆資料,故總共有 10000 筆
而未實作 hash table 版本的效能如下

兩者 user level 計算 fibonacci number 的比較

- 由圖上明顯可見實作 hash table 的版本在多執行緒的情況下表現明顯比較優秀,因為真正需要進行計算的只有第一個計算到的值
兩者 kernel level 計算 fibonacci number 的比較

由圖上可以看到 10 個執行緒分別計算至 的條件下,有實作 hashtable 的表現明顯比沒有實作得來的好,在有實作 hashtable 的程式中 (紫色線段),只要計算過且讓 hashtable 儲存後直接取用的話即為常數時間,而若該值沒有被計算過則花費的時間會和沒有實作 hashtable 的差不多 (綠色線段),即都是要計算 Fibnacci number。
- 紫色線段的後半段比較多 hash table 沒有找到的情況,推測是因為在計算 , 值小的情況下計算的很快,在沒有被其他執行緒搶掉的情況下可以運算比較多組 Fibonacci number,數字比較大的 Fibonacci number 都集中在後半段運算。
Reference