contributed by < fewletter >
作業要求
fibdrv.c
存在著 DEFINE_MUTEX
, mutex_trylock
, mutex_init
, mutex_unlock
, mutex_destroy
等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題。嘗試撰寫使用 POSIX Thread 的程式碼來確認。參考作業說明:Linux 時間測量和效能分析和 yanjiew 所撰寫的時間測試檔案。
為了要達到時間測量的目的,依照作業說明:Linux 時間測量和效能分析中的步驟實作
$ sudo sh performance.sh
執行,可藉由為了能夠在測量程式在 kernel 中運行的時間,將沒有用到的函式 fib_write
來作為輸出時間的函式。
首先先了解 ktime_t
此 api 如何運作,從 ktime_t
中我們可以知道其中的 ktime_get()
是用來標記時間,所以只要在程式開始和結束時都使用ktime_get()
就可以將程式執行時間擷取出來,程式碼如下:
在 fib_read()
中計時
從 fib_write()
中回傳時間
參考 yanjiew 所撰寫的時間測試檔案,並將其修改成可以輸出 data.txt
來紀錄時間,測試結果
在 scripts
中寫一個簡單的程式可以將上述資料畫圖
針對原始費氏數列的遞迴程式碼進行時間測量就會得到下面的圖
既然可以從輸出 kernel 的執行時間,那接著便是測試 user space 的時間,利用 time.h
中的 clock_gettime
進行時間的測量,從其中文件可以看到 clockid_t
和 timespec
兩個引數型態
往下搜尋可以看到 timespec
這個結構體,記錄著以奈秒(ns)為時間的單位
要利用此結構體只需要將全部的成員換成 ns 的時間單位就好
clockid_t
則是選擇 CLOCK_MONOTONIC
,主要是因為只要形成 process 開始和結束時候的時間標記,程式碼實作如下:
參考作業參考:用統計手法去除極端值和 4ce43c4 來實作 python scripts,實作此 scripts 的目的為降低極端值對圖的影響,如果像上面那張圖只憑計算一次的時間來畫圖,很可能會有極端值出現降低圖的可信度
上圖為原始算法所算的測試時間,在 kernel
中的測試時間隨著費氏數列的項數而增加,但是在 user
中的時間卻變動不大,這部分還在研究中。
為了將上述命令不需要在每次測試程式時都重打一次,將上述命令寫進 Makefile 中並且命名為 test
參考作業說明一和 Fast Fibonacci algorithms ,可以知道費式數列有多種計算方法,第一種為公式解
此方法最大的缺點在於 此數字在電腦中計算時無法取得確切數值,所以當計算 時,誤差會因為 n 次方而增加。
第二種方法為直觀的加法,利用費氏數列原本的定義來計算
此種方法的缺點則在會重複計算某些不必要的數值,只為了形成 和 ,並且每次計算時必須要每項都重新計算一次。
第三種則是 Fast Doubling,為了能夠減少計算量觀察其中規律
從以上算式可以注意到費氏數列的規則, 和 ,接著仔細觀察所有的 和 的費氏數列都可以由 , 兩種數值所組成,那我們每次只需要迭代目標一半的數值,一直迭代到目標就是我們的答案。
那目前為止從上面的資訊可以推斷程式碼可以是使用迭代或是遞迴,那規則呢 ? 我們何時需要使用 或 ,這可以從目標的二進位值來進行分析,假設我們想求 ,那 的二進位如下:
要如何從 變成 ,首先要 然後左移三位
對比到費氏數列中,就可以理解到為什麼在作業說明中,一開始 變成 一定是 ,因為二進位的最高位要是 ,才會有大於 的值。
以上是項數最低位為 的情況,下面舉例項數最低位為 的情況,以 為例
從以上情況可以歸納出兩個規則
Fast doubling 程式實作:
以下程式碼將上面的想法用最直接的方式實作一次
從上圖可以看到光是只有將算法從一般的 iterative
換成 fast doubling
運行時間就降低了不少,但是秉持著任何程式碼都有可以進步的空間,所以繼續優化程式碼,上述程式碼看似沒什麼問題,但是我很想把分支拿掉。
為了把分支拿掉,思考良久,寫出適合此情況的 bitmask
首先先考慮 f[0]
和 f[1]
的組合,可以從有分支的情況下看到
代表 f[0]
和 f[1]
是由 n0
和 n1
所組成,而 f[0]
當中 n0
和 n1
是交替出現,在 f[1]
中,n0
是只有 k & i
不等於 0 時出現,所以可以很清楚的看出其中的 bitmask 的關係
而什麼樣的 bitmask 會依照 &mask
和 &~mask
去保留和棄置此項,答案就是 1111
和 0000
,既然目標是利用 bitwise operation 將 k & i
形成 1111
和 0000
,所以就開始湊可以形成這樣的算式
假設
這樣湊出所需要的 bitmask
從上圖看來兩個方法速度上並沒有明顯差別,可能是測試的項數不夠大又或者是有分支的方法速度本身就和 bitwise operation 差不多快,這些都還需要進一步實驗才可得知。
clz
改寫從 gcc 中的 __builtin_
提供 __builtin_clz()
可以用來計算出現 1 的最高位元在左邊數過來第幾位,利用此函式修改的程式碼如下
測試的結果顯示利用 clz
的程式碼比較慢,並且有隨著項數增加而時間越長的趨勢,所以去找此函式的程式碼了解為何會出現此情況
從程式碼中可以看到 __builtin_clz()
在函式內已經使用了 bitwise operation 來計算數值的 leading zeros,所以在函式內移過一次, for 迴圈開始時又移了一次,時間上會比只有在 for 迴圈移一次還多,至於隨著項數增加而時間越長的趨勢則需要去實作大數運算才能確認。
bn
結構體為了能夠計算超過 long long
位元組所限制的費氏數列,所以利用 bn
結構體來實作費氏數列。
bn
結構體
從結構體中可以將大數視為 進位的數字,size
則是此大數的大小,單位則為 32 位元,以費氏數列 做舉例,此數字超過 32 位元必須要用 long long
才能儲存,以 bn
結構體表示為
number[0] = 7540113804746346429 & 4294967295
number[1] = (7540113804746346429 >> 32) & 4294967295
size = 2
而問題來了,當有一個數值超過了 long long
所能表示的範圍的時候,我們要如何去表示,所以在這裡參考了 作業說明:預先計算儲存 所需的空間 ,可以利用公式解先算出此數值的位元數
接著是如何創建一個 bn
結構體,以下是參考自作業說明
有 bn_alloc
就要有 bn_free
bn
運算然後觀察費氏數列需要什麼運算符號,從最原始的算法中可以看到需要加法和互換兩種運算式
bn_add
此程式在做 進位加法,其中的運算與 進位加法完全相同,先算低位元的加法,用 unsigned long long int
去儲存結果,若有進位的數值自然會在右移 32 位元之後出現。
bn_swap
互換兩個 bn
的位置,在迭代時常會用到
bn_to_string
透過 ASCII code 轉換,將數字和 '0'
的差以 char 陣列儲存,並且從高位逐一檢查是否有進位,若有進位則處理成沒有進位的數字
bn_multSSA
利用 Schönhage–Strassen Algorithm 來實作乘法,Schönhage–Strassen Algorithm 的主要觀念是講兩個要相乘的數字分成若干的小數字相乘後相加,而在大數中,我們已經有分好的小數字也就是 bn
結構體中的 number
,所以應用在大數中的 Schönhage–Strassen Algorithm 會變成以下型式
在大數 和大數 相乘之中, 為 和 的線性卷積,而若大數 ,那 當中的 就可以對應到 和 的線性卷積
從上面 與 的對應關係可以看出 ,其對應的程式碼則如下
而為了不讓大數 的大小在 fast doubling 時一直膨脹,所以在乘法開始執行之前,使用下面程式碼來限制 的大小
在測試時發現 和 The first 500 Fibonacci numbers 當中 數字並不符合
293 7654090467756936378415884538348976340768064993978954512095813
檢查過後發現在進行加法時, 的總和超過了 unsigned long long int 的上限,所以將 unsigned long long int 改成 __uint128_t ,由此可計算超過 1000 項費氏數列
bn
支援費氏數列運算使用 bn
重新實作一次原始費氏數列運算
修改 fib_read
,使得 fib_read
可以回傳字串到 user space 中
調整測試上限,便可獲得超過原本 的數值,測試結果如下
甚至可以測到 的費氏數列
將上面的算法跑 50 次後算平均可以發現不管是在 user space 還是 kernal to user 所花的時間都大到誇張,表示在程式在處理 copy_to_user
時花了大量的時間,而上圖中 kernel 看似沒有太大的變化,但當我們單獨拉出 kernel 運行時間的圖
就可以發現,kernel 運行時間也是隨著費氏數列的增加而變多
實作大數 fast doubling
從上面兩張圖可以看到相比於原始費氏數列運算速度快上許多,但是在 kernel space 的運行速度和 user space 的運行速度相比還是相差太多,所以開啟 perf 檢查到底是什麼造成 user space 的時間增加