contributed by < jim12312321
>
lsmod
的輸出結果有一欄名為 Used by
,這是 "each module's use count and a list of referring modules",但如何實作出來呢?模組間的相依性和實際使用次數 (reference counting) 在 Linux 核心如何追蹤呢?
搭配閱讀 The Linux driver implementer’s API guide » Driver Basics
fibdrv.c
存在著 DEFINE_MUTEX
, mutex_trylock
, mutex_init
, mutex_unlock
, mutex_destroy
等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題。嘗試撰寫使用 POSIX Thread 的程式碼來確認。GRUB_CMDLINE_LINUX
後加入 isolcpus=0
,以空出第 0 個 cpuperformance.sh
performance.sh
中,便於後續重新執行。將 K04: fibdrv 中提到可以測量時間的方法加進 fibdrv.c
DEFINE_KTIME
進行初始化,於這個 PATCH 時,DEFINE_KTIME
已被移除。修改 Makefile 會造成錯誤的指令刪除
scripts/expected.txt
的差別,但是 scripts/expected.txt
中的資料也只是完全沒修改時會印出的內容罷了,留著這到指令會造成make check
時錯誤(我不知道為什麼,不使用 make check
單獨使用這道指令時又沒問題!)且會讓版面很亂,故刪除。將 ./client
指定在特定的 CPU 運行並紀錄結果
在 client.c
中加入對 userspace time 的測量
耗費時間散佈圖
因此由費式數列的基本定義可以得知,除了前兩項,費式數列的算法為 。
更詳細的推導在 K04: fibdrv 中有說明,這邊就不重複,僅列出結論。
這表示我們可以用 和 求出 和 ,舉例來說:
用 fast doubling 求出 F(10):
此時我們可知 ,表示 F(10) 可透過 F(5) 和 F(6) 求出,而 F(5) 和 F(6) 又分別可透過 F(2),F(3) 和 F(3),F(4) 求出…,以此類推,最後我們可以得知要求出 F(10) ,可透過 F(0),F(1),F(2),F(3),F(4),F(5),F(6) 求出答案。
比起原先定義中從 F(0) 一路算到 F(8),F(9) 才能求出 F(10) 明顯更快。
所以費式數列的定義可進一步變成:
關於 clz / ctz 一類的指令如何在運算費式數列中加速,如同上面 K04: fibdrv 中的說明,可以先去除 leading 0s ,直接從 first set bit 開始算起。
從前 92 個的計算結果看來,有無進行 fast doubling 的差異不大。
以下是 fast doubling 的虛擬碼 - from K04: fibdrv
可以發現其中大部分的乘法運用都是在做平方運算,所以如果能針對平方運算做優化,則理論上能減少整體運算成本。
簡單測試 (使用 Online C Compiler 和 <time.h>
進行測量)
目前的結果看來是沒有優化成功的,反而成果越糟
於是我換了一個思路,利用將十進位換成二進位 (e.g. ),然後再利用乘法分配律相加。
而經過和上面一樣的測試環境和方法,以下是得到的結果。
可以看到成果更接近單純使用 *
進行次方運算的速度了,但整體來說還是略慢。
我覺得 v2 的效能損耗主要在第 11 行中的 if(CHECK_BIT(a,cnt))
,如果能改成 branchless 應該有機會進一步增進效能,因此基於 v2 的 v3 (branchless 的 v2) 如下:
利用 CHECK_BIT(x,n)
的輸出只有 0 或 1 的特性,製作 MASK 達成 branchless 。
而以下是當前的測試成果:
終於有機會比用 *
還快了
*
與 square_opt_v3
差異圖
其中 diff 為調整前減去調整後,意即值大於 0 表示有改進,反之小於 0 表示比原先表現更差。
計算 123456 ^ 2 1000 次並紀錄每次的 diff
可以從目前的測試結果看到, v3 依舊無法比原先的乘法算的更快,也沒有任何測試中能得到比原先表現更好的數據。
看著程式碼發呆一下後,突然發現其實我在實作的 square_opt
轉個彎後其實就是在實作二進位的乘法!因此我稍微改一下,附上程式跟測試結果圖。
*
與 mul_opt
差異圖計算 123456 * 654321 1000 次並紀錄每次的 diff
還是無法增進效能,甚至也還無法逼近原先的效能。
可運算最大值
目前到 500-th 都是正確的
耗費時間散佈圖 (log)
實作程式碼 : My fibdrv
在看到作業說明後,覺得大數實作 (by AdrianHuang) 的作法很有意思因此想自己嘗試做做看利用數字字串相加的功能。
就是個單純拿來放數字的結構(現在想想好像用 char 的二維陣列就能實作了)。
當初看到大數實作 (by AdrianHuang) 中的結構體 xs
很複雜,想簡化成簡單明瞭的形式。本結構體一樣能達到擴增到費氏數列第 500 個的功能,因此我不太清楚當初 xs
為什麼要這樣設計。
發想︰
看到大數實作 (by AdrianHuang) 的實作中要 reverse 很多次且要確保 a 恆大於 b ,我想要把這些步驟省下來,因此直接從 a 和 b 的尾端加回來,最後再將輸出 reverse 即可,且透過 _max
讓 a 不一定要恆大於 b 也能運算,其他邏輯跟大數實作 (by AdrianHuang) 相異不大。
實作細節︰
可運算最大值
目前到 500-th 都是正確的
耗費時間散佈圖 (log)
與用加法實作費氏數列的差異 (log)
可以看到利用 fast doubling 後越是後面的費氏數列運算的越快。
實作程式碼 : My fast fibdrv
在 fast doubling 實作中,實作邏輯就按照作業說明中提供的虛擬碼。
k
轉成 2 進位後從高位逐位判斷,在本實作中用 stack 儲存十進位轉二位的位數。實作方式基本跟字串加法相同,與加法不同的是在加法中要紀錄進位 carry
,而在減法中要紀錄借位 borrow
。
整體作法就是直式乘法的思維,須注意 out
要先將裡面的值初始為全為 0 的字串,這樣在抓取先前預算過的值才不會出錯。 (tmp += out[ia + ib] - '0';
)
在老師實作的大數運算中主要由 apm 和 bn 所組成,其中 apm 負責處理高精度運算而 bn 則是利用 apm 中提供的各項基礎運算,包裝成更高階的 API 以利使用。
高精度運算是利用數字陣列來模擬大數運算,舉例來說:
uint8_t 的 MAX 是 255 ,那如果要表達 300 怎麼辦?
- 令一個
uint8_t *digit
的數字陣列來儲存(同時也可以指定此陣列的 size,本例中假設 size = 3)- 已知
- 則 300 可以用 digit[2] = 0,digit[1] = 1,digit[0] = 44 表示
視覺化表示就是
最後要輸出給使用者看的時候要再經過格式轉換,也就是 format.c 在做的事。
在加減法中,運算方法其實與正常數值運算雷同,根據指定的 size 從低位元組運算至高位元組並記錄 carry/borrow 。
以加法為例:
size = 1
240 + 50 = 290
carry = 1
演算法採用 Karatsuba algorithm ,要運算 時,首先會先將 U 和 V 視為 , ,再進行乘法運算。
而在 APM 中數值的表達方式,本身就符合 的形式。
舉例來說:
假設 4 個 bits 為一組,則
在乘法中首先會設定一個閾值 (KARATSUBA_MUL_THRESHOLD
),若 小於閾值則執行一般直式乘法把 , 和 分別算出來並放在對應的位置上進行加減法運算,即可得到兩數乘積,否則繼續依據閾值拆分。
舉例來說:
假設 4 個 bits 為一組且KARATSUBA_MUL_THRESHOLD
為 1
要運算 ,則
,,
,,
(為求簡潔,以下皆用 10 進位取代 2 進位,)
上述例子中 28 和 7 分別被拆成兩項,若是進行正常的大數乘法(逐 digit 相乘相加),則需要 ,考慮兩個大數且都被需拆 次,則需要 次乘法運算。
而使用 Karatsuba algorithm 則會需要 次乘法運算。
因此使用 Karatsuba algorithm 對於大數運算能減少許多乘法運算帶來的成本。
在 bn 中進一步組合 APM 中寫好的高精度運算函式成為面向使用者的 API 。
除此之外,也將在 apm 運算中常用的 digit, size 等封裝成 bn 結構體。
將原先老師提供的 fast doubling (簡化中間過程),以下簡稱為 ver1:
使用不同的 Q-Matrix 改寫成 (簡化中間過程),以下簡稱為 ver2:
在 KYG-yaya573142 的報告聲稱 ver2
可以少掉一次迴圈及避免使用減法,避免使用減法這點很好理解,ver2
的確沒有減法,又因為在 bn_sub 的實現方法為反轉 sign bit 達成減法效果,因此減少減法,的確會達到優化的目的。
而針對少掉一次迴圈的部分我存有一些疑惑,首先為了在同一個基準上比較,首先應該將 ver1 原先的實現方法也加上 __bulitin_clz
,以下為簡易測試函式。
其中關鍵的原因在於 ver1
的 更新方式需要透過 ,因此必須從 n 的 last set bit 開始迴圈,才可運算正確的結果,若一樣用 1U << (30 - __builtin_clz(n))
,則會因為一開始 為 0 進而無法正確更迭費式數列的值。
若要使用與 ver2
相同的迴圈起點, 1U << (30 - __builtin_clz(n))
(換句話說就是從 n 的 last set bit 的下一個 bit 開始),則就必須滿足以下任一條件:
f1
與 f2
的值在 ver2
中利用改變 Q-Matrix 使得運算 時不會受到 為 0 的影響(條件 (2) 的解法)。
但從條件 (1) 著手一樣可以正確處理費式數列的運算,已知由於 為 0 會造成運算錯誤,因此只要將其改成 1 後續即可正確運算 (如同以下的 ver1_1
)。
另外 bn_fib_fdoubling 中本來就有對 n == 1 or 0 的情況做特殊處理,所以更改 f1
的值也不用擔心 n == 0 時會有錯誤的回傳值。
最後,我認為在 KYG-yaya573142 的報告造成時間小幅度減少 3% 的原因,主要是來自引入 ver2
的算法後,必須搭配使用 clz
避免運算結果出錯,而引入 clz
便會在 n 值越小的時候,減少越多次迴圈運算,達成整體效能的優化,而非因為減少一次迴圈及取代減法。
bn_do_add
的實作透過將原先一次將 c->number[0 ... c->size -1]
全部運算的迴圈改成兩個迴圈,變成一個負責 c->number[0 ... bsize -1]
,若 asize > bsize
則再利用一個迴圈將 c->number[bsize ... asize-1]
算完。
不過我認為改善後的實作仍有改進空間,目前想到的有兩點:
在改善後的實作中須確保 才可以後續運算,因此在 bn_do_add
中使用以下指令確保 恆成立。
但有沒有可能基於費式數列的特性,本來就可以確保 恆成立?
先來看看 bn_add
會使用在什麼地方,除了 bn_sub
中用來實現減法外,其餘都使用在計算費式數列中,讓我們列出 bn_fib_fdoubling
中跟 bn_add
有關的操作。
- bn_add(k1, f2, k1); // k1 = 2 * F(k-1) + F(k)
- bn_add(k2, k1, f1); // f1 = k1 + k2 = F(2k-1) now
- bn_add(f1, f2, f2); // f2 = F(2k+2)
接著將三個會用到 bn_add
的時機分開討論,先講 2 和 3 ,他們很直覺,在第 2 點中此時的 k1 和 k2 分別為 和 ,由費式數列的單調遞增特性可知 恆成立,第三點中也可以很輕易的判斷執行 bn_add
時 恆成立。
而在第 1 點中, k1 和 f2 分別為 和 ,接著可以經過一些簡單的推導得知, 恆成立。
在確保所有使用 bn_do_add
的操作都可以確保 恆成立後,便可以改寫 bn_do_add
。
然後改寫 bn_fib_fdoubling
中的 bn_add
(原本的寫法就已經符合 因此僅加上註解)。
第二個迴圈主要的功能為將 a 中剩下的數值複製到 c 中並且加上 carry 。但考慮到進位的次數與 asize - bsize
的差值會隨著費式數列越大而越大,因此可以再拆解第二個迴圈,讓第二個迴圈負責處理 carry 而第三個負責處理剩下的 a->number 複製到 c->number 。
原先的實作(節錄片段)
可以發現為了將運算結果都儲存在 k1
中,因此每次運算都會遇到以下問題:
c == a or c == b
動態配置暫存記憶體而改進的做法便是重構算式,將 k2
也拿來儲存計算結果,從而避免問題 (2) ,再利用 bn_swap
省去 memcpy
的運用解決問題 (1) 。
在 bn
中加入 capacity
儲存實際配置的長度,而 size
變成當前使用的大小,進而減少頻繁呼叫 realloc
所產生的效能損耗。
另外在 KYG-yaya573142 的報告中提到
至少能正確計算至第 100000 項
是因為 unsigned int 能表達的數值上界為 4294967295(,在 LP64 或 LLP64 系統),因此能表達的費式數列上界為 ,考慮可能不需要計算到這麼後面的費式數列值,還可以引入 bit field 的技巧將 size,capacity 和 sign 整合。
更進一步的節省 bn
的空間,不過這樣改寫的話原先很多函式都要一並改寫,整體利弊尚未可知。
根據當前系統不同的 word size 調整 bn
中各項屬性的資料型態,不僅能使 bn
能夠在不同系統運行,同時也確保當使用 64-bit 的 CPU 時能夠增加計算效能與儲存空間。
TODO:
針對 KYG-yaya573142 的報告減少大數運算的成本中的以下章節撰寫報告
參考了 KYG-yaya573142 的報告撰寫一個多執行緒程式來測試。
在原先的程式中,利用 mutex_trylock
和 mutex_unlock
來避免程式進入 critical section ,從而保證每個執行緒的內容正確。而其中使用的 mutex_trylock
當發現該區段已經被 lock 時,會直接返回,屬於非阻斷式 I/O 。
而這樣的特點也可以經由實驗發現,當反覆嘗試原本的 fibdrv 後,會出現兩種結果,一種是出現錯誤訊息的,另一種是兩個執行緒都成功的完成任務。
第一種情況發生的原因是因為兩個執行緒針對 fibdrv 的讀寫順序是相互交雜的,因此當 fibdrv 已經被 lock 時,另一個執行緒嘗試 trylock 失敗,因此印出錯誤訊並結束該執行緒。
而第二種狀況則是運氣好,兩個執行緒的讀寫沒有交雜,因此兩個執行緒都完成各自的任務。
在改成使用 mutex_lock
後,當執行緒程式存取 mutex 時,若該區段已被 lock ,則會持續等待直到 unlock ,屬於阻斷式 I/O 。
去除 mutex 後, thread 將可以依照原先的順序讀取,不用等待其他 thread 釋出程式驅動。
可以看到 thread 1 和 2 交互讀取 fibdrv ,不過有趣的是可以發現其中每個費氏數列的值並沒有出錯,這是因為在原先撰寫的測試程式中, file descriptor 並不是共享資源, thread 間沒有共用同一個 fd 自然也不會有衝突。
在把 fd 改為全域變數之後,可以發現 thread 所得到的值出現錯誤,意味著進入了 critical section 。
不過猜測由於測試程式僅有兩個執行緒,因此在進行 lseek 和 read 的操作時,要出現如 thread 1 lseek 但還沒 read 時, offset 已經被 tread 2 改變造成結果不正確的 race condition 反而機率不大,如果採用像 KYG-yaya573142 的報告中用 sleep 的方法,就可以很明顯的看到效果。
或是單純的增加 thread 也可以較容易觀察到錯誤(增加到 20 個 thread)
在實作大數加法的時候,我直接利用 char* 來儲存數字,企圖"簡化" xs 的架構來達到同樣可以大數運算的效果,但就在多看幾次 SSO (Small/Short String Optimization) 和聽老師上課後才稍微理解為何 xs 要這樣設計。
可以看到 xs 在佔用 16 bytes 且小字串的情況下,可以利用 15 bytes 儲存字串資料,利用 space_left 來計算剩餘可用的 bytes ,且 15 bytes 可用 4 個 bits 就可以儲存資料剩餘大小 () 。最後剩下 4 個 bits 的 is_ptr
和 is_large_string
可以分別對應到 FBString 中提到的中字串與大字串,而 flag2
與 flag3
我認為只是把剩下的 bits 定義完,實際上並不會用到。
至於在處理中字串與大字串時則利用 heap allocated
的 struct 來表達。
xs 這樣的設計使得在小字串時可以直接用 stack 中的記憶體,避免動態配置新記憶體,空間利用上也更完善,直接在原先配置的空間中紀錄了 size, capaity 和盡可能的儲存字串資料。