2022q1 Homework3 (fibdrv)
contributed by < tinyynoob
>
作業要求
最近的方向可以跳到 June rework 開始看
因為一篇筆記放不下了,再開一篇:fibdrv 2
研讀資料
Fibonacci 相關性質
https://hackmd.io/@sysprog/fibonacci-number
Algorithms for Computing Fibonacci Numbers Quickly
Fast doubling 算法
作業說明中有給出 pseudocode :
注意到這個 ^
是指 power 而非 xor 。
目前看不出 clz()
能對計算 Fib 做到什麼強力優化,似乎只能加快 initialize i
而已,需要再研究研究。
Follow 《The Linux Kernel Module Programming Guide》
將自己研讀的摘要整理到另一篇筆記: https://hackmd.io/@tinyynoob/Hk89qPw-9
本地版本:
根據書中範例嘗試建立 hello-1.c 及對應的 makefile 。
make 時馬上出現問題:
log
發現原來檔名把 Makefile
寫成 makefile
也不行,更改後得到:
log
看似往正確的方向前進了,也有產出 hello-1.ko ,但沒有完全成功,它在 vmlinux
遇到問題
根據 modinfo
可以發現在 hello-1.ko 的 vermagic 欄位有 modversions
。回去閱讀書中提到 Modversioning 的部份,得知我可能需要關掉它。
參考此,試著進到 /boot/config-5.13.0-30-generic 關掉 CONFIG_MODVERSIONS
。
關掉 modversions 但尚未 recompile kernel
把 hello-1.c 裡的 return 值改成負數會導致:
研讀數字字串運算範例程式
source code
數字字串在運算時可以因應字串 size 選擇不同的策略。
範例程式使用的字串使用 union 定義如下:
- 在短碼的情況會使用第 9 行的 struct ,直接將資料字串存在
filler
並把一些 flag 資訊放在最後一個 byte
- 在長碼的情況會使用第 20 行的 struct ,使用一個
ptr
指標並配置空間在 heap ,後面的 8 byte 拿來存放一些 flag
還沒完全理解在長碼情況下,
- 如何繼續使用
is_ptr
flag
capacity
使用的詳細狀況
範例程式在 fib_sequence()
計算 Fib ,算法使用逐項相加:
因此若是我們能改善乘法及平方的複雜度,則可以使用 fast doubling 的技巧來改善目前算法。
Linux 效能分析
嘗試查看 CPU affinity :
ff 的二進位為 11111111 ,代表這個 process 可以在 7~0 任意一個 CPU 上執行。試著更改:
根據網路上的教學,在 /boot/grub/grub.cfg 中加入 isolcpus=0,1
。
最後,完成 排除干擾效能分析的因素 中的所有設定。
reboot 之後查看 process 發現 core mask 變成了 fc :
雖然有些已經變成 fc 但有些仍顯示 ff ,因此重新依據參考共筆進行設定。結果仍失敗,目前的在設定 smp_affinity 上因腳本錯誤遇到困難。
Fork
嘗試掛載模組之後輸入 sudo ./client
可以得到
結果
Reading from /dev/fibonacci at offset 0, returned the sequence 0.
Reading from /dev/fibonacci at offset 1, returned the sequence 1.
Reading from /dev/fibonacci at offset 2, returned the sequence 1.
Reading from /dev/fibonacci at offset 3, returned the sequence 2.
Reading from /dev/fibonacci at offset 4, returned the sequence 3.
Reading from /dev/fibonacci at offset 5, returned the sequence 5.
Reading from /dev/fibonacci at offset 6, returned the sequence 8.
Reading from /dev/fibonacci at offset 7, returned the sequence 13.
Reading from /dev/fibonacci at offset 8, returned the sequence 21.
Reading from /dev/fibonacci at offset 9, returned the sequence 34.
Reading from /dev/fibonacci at offset 10, returned the sequence 55.
Reading from /dev/fibonacci at offset 11, returned the sequence 89.
Reading from /dev/fibonacci at offset 12, returned the sequence 144.
Reading from /dev/fibonacci at offset 13, returned the sequence 233.
Reading from /dev/fibonacci at offset 14, returned the sequence 377.
Reading from /dev/fibonacci at offset 15, returned the sequence 610.
Reading from /dev/fibonacci at offset 16, returned the sequence 987.
Reading from /dev/fibonacci at offset 17, returned the sequence 1597.
Reading from /dev/fibonacci at offset 18, returned the sequence 2584.
Reading from /dev/fibonacci at offset 19, returned the sequence 4181.
Reading from /dev/fibonacci at offset 20, returned the sequence 6765.
Reading from /dev/fibonacci at offset 21, returned the sequence 10946.
Reading from /dev/fibonacci at offset 22, returned the sequence 17711.
Reading from /dev/fibonacci at offset 23, returned the sequence 28657.
Reading from /dev/fibonacci at offset 24, returned the sequence 46368.
Reading from /dev/fibonacci at offset 25, returned the sequence 75025.
Reading from /dev/fibonacci at offset 26, returned the sequence 121393.
Reading from /dev/fibonacci at offset 27, returned the sequence 196418.
Reading from /dev/fibonacci at offset 28, returned the sequence 317811.
Reading from /dev/fibonacci at offset 29, returned the sequence 514229.
Reading from /dev/fibonacci at offset 30, returned the sequence 832040.
Reading from /dev/fibonacci at offset 31, returned the sequence 1346269.
Reading from /dev/fibonacci at offset 32, returned the sequence 2178309.
Reading from /dev/fibonacci at offset 33, returned the sequence 3524578.
Reading from /dev/fibonacci at offset 34, returned the sequence 5702887.
Reading from /dev/fibonacci at offset 35, returned the sequence 9227465.
Reading from /dev/fibonacci at offset 36, returned the sequence 14930352.
Reading from /dev/fibonacci at offset 37, returned the sequence 24157817.
Reading from /dev/fibonacci at offset 38, returned the sequence 39088169.
Reading from /dev/fibonacci at offset 39, returned the sequence 63245986.
Reading from /dev/fibonacci at offset 40, returned the sequence 102334155.
Reading from /dev/fibonacci at offset 41, returned the sequence 165580141.
Reading from /dev/fibonacci at offset 42, returned the sequence 267914296.
Reading from /dev/fibonacci at offset 43, returned the sequence 433494437.
Reading from /dev/fibonacci at offset 44, returned the sequence 701408733.
Reading from /dev/fibonacci at offset 45, returned the sequence 1134903170.
Reading from /dev/fibonacci at offset 46, returned the sequence 1836311903.
Reading from /dev/fibonacci at offset 47, returned the sequence 2971215073.
Reading from /dev/fibonacci at offset 48, returned the sequence 4807526976.
Reading from /dev/fibonacci at offset 49, returned the sequence 7778742049.
Reading from /dev/fibonacci at offset 50, returned the sequence 12586269025.
Reading from /dev/fibonacci at offset 51, returned the sequence 20365011074.
Reading from /dev/fibonacci at offset 52, returned the sequence 32951280099.
Reading from /dev/fibonacci at offset 53, returned the sequence 53316291173.
Reading from /dev/fibonacci at offset 54, returned the sequence 86267571272.
Reading from /dev/fibonacci at offset 55, returned the sequence 139583862445.
Reading from /dev/fibonacci at offset 56, returned the sequence 225851433717.
Reading from /dev/fibonacci at offset 57, returned the sequence 365435296162.
Reading from /dev/fibonacci at offset 58, returned the sequence 591286729879.
Reading from /dev/fibonacci at offset 59, returned the sequence 956722026041.
Reading from /dev/fibonacci at offset 60, returned the sequence 1548008755920.
Reading from /dev/fibonacci at offset 61, returned the sequence 2504730781961.
Reading from /dev/fibonacci at offset 62, returned the sequence 4052739537881.
Reading from /dev/fibonacci at offset 63, returned the sequence 6557470319842.
Reading from /dev/fibonacci at offset 64, returned the sequence 10610209857723.
Reading from /dev/fibonacci at offset 65, returned the sequence 17167680177565.
Reading from /dev/fibonacci at offset 66, returned the sequence 27777890035288.
Reading from /dev/fibonacci at offset 67, returned the sequence 44945570212853.
Reading from /dev/fibonacci at offset 68, returned the sequence 72723460248141.
Reading from /dev/fibonacci at offset 69, returned the sequence 117669030460994.
Reading from /dev/fibonacci at offset 70, returned the sequence 190392490709135.
Reading from /dev/fibonacci at offset 71, returned the sequence 308061521170129.
Reading from /dev/fibonacci at offset 72, returned the sequence 498454011879264.
Reading from /dev/fibonacci at offset 73, returned the sequence 806515533049393.
Reading from /dev/fibonacci at offset 74, returned the sequence 1304969544928657.
Reading from /dev/fibonacci at offset 75, returned the sequence 2111485077978050.
Reading from /dev/fibonacci at offset 76, returned the sequence 3416454622906707.
Reading from /dev/fibonacci at offset 77, returned the sequence 5527939700884757.
Reading from /dev/fibonacci at offset 78, returned the sequence 8944394323791464.
Reading from /dev/fibonacci at offset 79, returned the sequence 14472334024676221.
Reading from /dev/fibonacci at offset 80, returned the sequence 23416728348467685.
Reading from /dev/fibonacci at offset 81, returned the sequence 37889062373143906.
Reading from /dev/fibonacci at offset 82, returned the sequence 61305790721611591.
Reading from /dev/fibonacci at offset 83, returned the sequence 99194853094755497.
Reading from /dev/fibonacci at offset 84, returned the sequence 160500643816367088.
Reading from /dev/fibonacci at offset 85, returned the sequence 259695496911122585.
Reading from /dev/fibonacci at offset 86, returned the sequence 420196140727489673.
Reading from /dev/fibonacci at offset 87, returned the sequence 679891637638612258.
Reading from /dev/fibonacci at offset 88, returned the sequence 1100087778366101931.
Reading from /dev/fibonacci at offset 89, returned the sequence 1779979416004714189.
Reading from /dev/fibonacci at offset 90, returned the sequence 2880067194370816120.
Reading from /dev/fibonacci at offset 91, returned the sequence 4660046610375530309.
Reading from /dev/fibonacci at offset 92, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 93, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 94, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 95, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 96, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 97, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 98, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 99, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 100, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 100, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 99, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 98, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 97, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 96, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 95, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 94, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 93, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 92, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 91, returned the sequence 4660046610375530309.
Reading from /dev/fibonacci at offset 90, returned the sequence 2880067194370816120.
Reading from /dev/fibonacci at offset 89, returned the sequence 1779979416004714189.
Reading from /dev/fibonacci at offset 88, returned the sequence 1100087778366101931.
Reading from /dev/fibonacci at offset 87, returned the sequence 679891637638612258.
Reading from /dev/fibonacci at offset 86, returned the sequence 420196140727489673.
Reading from /dev/fibonacci at offset 85, returned the sequence 259695496911122585.
Reading from /dev/fibonacci at offset 84, returned the sequence 160500643816367088.
Reading from /dev/fibonacci at offset 83, returned the sequence 99194853094755497.
Reading from /dev/fibonacci at offset 82, returned the sequence 61305790721611591.
Reading from /dev/fibonacci at offset 81, returned the sequence 37889062373143906.
Reading from /dev/fibonacci at offset 80, returned the sequence 23416728348467685.
Reading from /dev/fibonacci at offset 79, returned the sequence 14472334024676221.
Reading from /dev/fibonacci at offset 78, returned the sequence 8944394323791464.
Reading from /dev/fibonacci at offset 77, returned the sequence 5527939700884757.
Reading from /dev/fibonacci at offset 76, returned the sequence 3416454622906707.
Reading from /dev/fibonacci at offset 75, returned the sequence 2111485077978050.
Reading from /dev/fibonacci at offset 74, returned the sequence 1304969544928657.
Reading from /dev/fibonacci at offset 73, returned the sequence 806515533049393.
Reading from /dev/fibonacci at offset 72, returned the sequence 498454011879264.
Reading from /dev/fibonacci at offset 71, returned the sequence 308061521170129.
Reading from /dev/fibonacci at offset 70, returned the sequence 190392490709135.
Reading from /dev/fibonacci at offset 69, returned the sequence 117669030460994.
Reading from /dev/fibonacci at offset 68, returned the sequence 72723460248141.
Reading from /dev/fibonacci at offset 67, returned the sequence 44945570212853.
Reading from /dev/fibonacci at offset 66, returned the sequence 27777890035288.
Reading from /dev/fibonacci at offset 65, returned the sequence 17167680177565.
Reading from /dev/fibonacci at offset 64, returned the sequence 10610209857723.
Reading from /dev/fibonacci at offset 63, returned the sequence 6557470319842.
Reading from /dev/fibonacci at offset 62, returned the sequence 4052739537881.
Reading from /dev/fibonacci at offset 61, returned the sequence 2504730781961.
Reading from /dev/fibonacci at offset 60, returned the sequence 1548008755920.
Reading from /dev/fibonacci at offset 59, returned the sequence 956722026041.
Reading from /dev/fibonacci at offset 58, returned the sequence 591286729879.
Reading from /dev/fibonacci at offset 57, returned the sequence 365435296162.
Reading from /dev/fibonacci at offset 56, returned the sequence 225851433717.
Reading from /dev/fibonacci at offset 55, returned the sequence 139583862445.
Reading from /dev/fibonacci at offset 54, returned the sequence 86267571272.
Reading from /dev/fibonacci at offset 53, returned the sequence 53316291173.
Reading from /dev/fibonacci at offset 52, returned the sequence 32951280099.
Reading from /dev/fibonacci at offset 51, returned the sequence 20365011074.
Reading from /dev/fibonacci at offset 50, returned the sequence 12586269025.
Reading from /dev/fibonacci at offset 49, returned the sequence 7778742049.
Reading from /dev/fibonacci at offset 48, returned the sequence 4807526976.
Reading from /dev/fibonacci at offset 47, returned the sequence 2971215073.
Reading from /dev/fibonacci at offset 46, returned the sequence 1836311903.
Reading from /dev/fibonacci at offset 45, returned the sequence 1134903170.
Reading from /dev/fibonacci at offset 44, returned the sequence 701408733.
Reading from /dev/fibonacci at offset 43, returned the sequence 433494437.
Reading from /dev/fibonacci at offset 42, returned the sequence 267914296.
Reading from /dev/fibonacci at offset 41, returned the sequence 165580141.
Reading from /dev/fibonacci at offset 40, returned the sequence 102334155.
Reading from /dev/fibonacci at offset 39, returned the sequence 63245986.
Reading from /dev/fibonacci at offset 38, returned the sequence 39088169.
Reading from /dev/fibonacci at offset 37, returned the sequence 24157817.
Reading from /dev/fibonacci at offset 36, returned the sequence 14930352.
Reading from /dev/fibonacci at offset 35, returned the sequence 9227465.
Reading from /dev/fibonacci at offset 34, returned the sequence 5702887.
Reading from /dev/fibonacci at offset 33, returned the sequence 3524578.
Reading from /dev/fibonacci at offset 32, returned the sequence 2178309.
Reading from /dev/fibonacci at offset 31, returned the sequence 1346269.
Reading from /dev/fibonacci at offset 30, returned the sequence 832040.
Reading from /dev/fibonacci at offset 29, returned the sequence 514229.
Reading from /dev/fibonacci at offset 28, returned the sequence 317811.
Reading from /dev/fibonacci at offset 27, returned the sequence 196418.
Reading from /dev/fibonacci at offset 26, returned the sequence 121393.
Reading from /dev/fibonacci at offset 25, returned the sequence 75025.
Reading from /dev/fibonacci at offset 24, returned the sequence 46368.
Reading from /dev/fibonacci at offset 23, returned the sequence 28657.
Reading from /dev/fibonacci at offset 22, returned the sequence 17711.
Reading from /dev/fibonacci at offset 21, returned the sequence 10946.
Reading from /dev/fibonacci at offset 20, returned the sequence 6765.
Reading from /dev/fibonacci at offset 19, returned the sequence 4181.
Reading from /dev/fibonacci at offset 18, returned the sequence 2584.
Reading from /dev/fibonacci at offset 17, returned the sequence 1597.
Reading from /dev/fibonacci at offset 16, returned the sequence 987.
Reading from /dev/fibonacci at offset 15, returned the sequence 610.
Reading from /dev/fibonacci at offset 14, returned the sequence 377.
Reading from /dev/fibonacci at offset 13, returned the sequence 233.
Reading from /dev/fibonacci at offset 12, returned the sequence 144.
Reading from /dev/fibonacci at offset 11, returned the sequence 89.
Reading from /dev/fibonacci at offset 10, returned the sequence 55.
Reading from /dev/fibonacci at offset 9, returned the sequence 34.
Reading from /dev/fibonacci at offset 8, returned the sequence 21.
Reading from /dev/fibonacci at offset 7, returned the sequence 13.
Reading from /dev/fibonacci at offset 6, returned the sequence 8.
Reading from /dev/fibonacci at offset 5, returned the sequence 5.
Reading from /dev/fibonacci at offset 4, returned the sequence 3.
Reading from /dev/fibonacci at offset 3, returned the sequence 2.
Reading from /dev/fibonacci at offset 2, returned the sequence 1.
Reading from /dev/fibonacci at offset 1, returned the sequence 1.
Reading from /dev/fibonacci at offset 0, returned the sequence 0.
可以發現到 92 項以後數值就不再改變,代表這裡存在嚴重缺陷,對於超過 64 位元的數值無能為力。
另外在輸入指令
後,不是得到 235 也不是得到 236 而是 505
ktime doc
看參考共筆依樣畫葫蘆,由於目前 fibdrv.c 的 fib_write()
沒有功能,所以在其中塞入測量時間的程式碼:
然而輸出的數值看起來都差不多,回頭想找找看這個 offset
到底是怎麼設的。我發現在 client.c 呼叫時傳入的參數並不包含 offset
,不過在呼叫 write()
跟 read()
時有個不同是 read()
多了一行:
其中 SEEK_SET
將被展開為 0 。觀察發現呼叫 lseek()
會連到 fibdrv.c 中的:
雖然可以猜測這個函式的回傳值會更改 offset
,但是目前不知道這到底是如何達成的,我甚至不曉得 offset
到底存在哪裡。
TODO:
答:
我認為就是存在 file 結構的 f_pos
成員中
研讀參考共筆中的大數運算
https://hackmd.io/@KYWeng/rkGdultSU#如何減少大數運算的成本
這份共筆提出多種實作上改進計算 Fib 速度的技巧,摘要如下:
調整 Fast Fib 算法
將原先的算法由
改良為:
以迴避所有減法運算,並且不會再出現任何負數,可以全部用 unsigned 型態儲存。
使用 memory pool
在大數結構中引入 memory pool 的概念來避免頻繁的 realloc()
。
使用內嵌組合語言加速乘法
做乘法運算時直接指定高低位的位置,而不是使用擴展整數型別。
以參考共筆中的語法為例:
其中 instruction 顯然為 mulq
,而
=
是 write only 的意思
a
, d
分別表示 $rax
, $rdx
( )
內使用 C 變數
rm
表示由 compiler 自己選擇存放位置
%0
在此等同 a
暫存器
%3
代表 rm
參考:
https://ithelp.ithome.com.tw/articles/10213500
https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
核心思想為在做乘法時,將一些乘法運算改為加減,考慮:
原先需要做 4 次乘法運算,經過一些設計可以變成:
便能少做一次乘法。
改進 fibdrv
實作程式碼 github
大數運算
在為 fibdrv 改進效能之前,我們需要先確保它的正確性,為此我們首先引入大數運算的機制。對於存放大數的方式前面提出了兩種設計:
- 單純使用結構
- 使用 union 達成 small string optimization
由於在計算 Fib 的情境下不會一直需要宣告新的變數,也不會有頻繁居於小數計算的情況,因此我們選擇使用結構的方式,如此也較便利於 maintain resize 的功能。
因為我目前不太熟悉 kernel programming 的各種 function 用法,因此我打算先在 user space 開發,並 debug 確認功能沒問題之後再移植到 kernel space 。
我想借助 macro 來完成此工作,例如這樣:
由於在計算 Fib 的過程中不會遇到任何負數,因此我的大數運算假設不存在負數,相關的運算也都沒有提供。
引入共筆用的 macro :
目前還未使用到加倍的型別,暫時先註解掉。
基於沒有負數的假設,我們得以直接移除共筆結構中的 sign
成員,訂出:
預期 (無號) 大數運算的界面提供以下功能:
ubignum_init()
ubignum_free()
ubignum_resize()
ubignum_add()
ubignum_mult()
ubignum_assign()
目前比較沒有頭緒的是 assign()
的作法
ubignum_init()
程式碼
其中 DEFAULT_CAPACITY
目前訂為 16 。
靜態分析出現一堆問題,這才發現我原本的寫法根本不會正確把初始化後的 N
回傳,因此修改程式碼:
ubignum_resize()
程式碼
目前不確定允許 new_size
傳入 0 是否是個好主意。
ubignum_add()
目前設計讓使用者傳入存放 sum 的變數,但這麼做就需要解決 pointer aliasing 的問題。
由低到高做加法的流程大致可以分成兩段:
- 兩數相加
- 一數加上 1. 的 carry out
首先開發 1. 的部份:
若超出 out
範圍則對它進行 resize()
,其中加入 builtin_expect()
協助 compiler 進行 branch prediction 。
撰寫過程馬上讓我遭遇了幾個問題:
- 第 11 行,我嘗試引入 quiz2 第一題 計算平均的方式再取 MSB 來得出新的
carry
,但我發覺這樣的話舊的 carry
沒有被加進來,所以在例如 (a, b, c) = (UINT_MAX, 0, 1)
的情況下會算出錯誤結果。
- 我不熟悉 gcc 對於 unsigned 型態的右移會如何處理
根據 gcc 的 mail list :
For right shifts, if the type is signed, then we use an arithmetic shift;
if the type is unsigned then we use a logical.
因此對於 unsigned 右移的行為符合我的預期。
為了應對 pointer aliasing 的問題,我們應該不能直接拿 a->data
和 b->data
去運算,因為它們在第 9 行可能會被改變 (如果他們跟 out
指到同樣位置) ,那麼第 12 行的運算將會錯誤。因此需要先將它們在第 9 行的值存起來,將程式碼改成:
其中第 4 行跳到最後去把 a
跟 b
還原成原本的樣子,這也是因為 aliasing 才有的問題。
第二段程式碼:
先使用 remain
來記住哪一個加數還有剩餘的位數沒加,接著就進入迴圈繼續加完,大部分的情況都會走 12 行,唯一會走到 17 行的情況是最高位有 carry out 而且剛好 carry 到一個新的 ubn_unit
chunk 。
同樣處理 pointer aliasing 的問題並改進寫法得:
處理 aliasing 的程式碼:
直接使用一個變數 alias
中的 bit 來觀察,若有 aliasing 則配置空間給保留原值。
注意到這裡 2, 3 共用的原因是,如果 a
和 b
指到同一個位置,那麼恢復 b
也同時恢復了 a
。
aliasing 的問題比我預期的還更難處理,我直接重改邏輯,變成如果有 aliasing 則配置一個新空間存放計算結果,算完再放到 out
,改寫過程中也修復了一些 bug 。
更改後的 ubignum_add()
程式碼
其中 ubn_unit_extend
在 x64 定義為 unsigned __int128
,目前嘗試在 user space 配合 gdb 工具進行測試,得到錯誤的結果, carry
沒有被正確計算
疑似 uint128 的 shift operation 有問題,我嘗試去找 gcc doc ,但是內容只有三行,沒有提到是否支援 shift operation 。
我在 assignment 之前加入強制轉型就有我預期的結果了
我好像已經吃了很多次這種虧,卻都沒有記住,我一直誤以為計算的時候就會轉成左邊的型態,這次特別寫進 commit message ,希望能確實記得。
ubignum_mult()
雖然前面有提及加速的算法,但是因為我目前的大數機制沒有減法,所以暫時先略過此項。
在開發時發現 ubn_unit
的加法一直重複遇到,所以將其模組化
長得有點像硬體加法器的樣子。 cin
和 cout
都只會用到一個 bit ,用什麼型態似乎都蠻浪費的,最後選用了最常見的 int 。
mult()
程式碼
目前的 bug 是 inline asm 沒有正確作用,看起來像是 low
跟 high
相反了,也可以看出確實是把 $rdx
, $rax
分別接到 high
跟 low
。
難道說它執行 mulq
instruction 時的資料放置方式是跟原本相反嗎?
我嘗試去查看組合語言的編譯結果,看到:
雖然它把 "rm"
選成 $rdx
,但這應該也不會影響指令執行結果才對。
我發現其實它沒有算錯,這個結果是正確的,但因為我對乘法的直覺還不夠,誤用加法的邏輯來思考乘法才誤判結果。
在 pointer aliasing 的情況下無法做到 in place 的算法,否則錯誤處理會出問題。
大數運算 第二版
在開發的過程中,因為大數運算難以 debug 只能經由 gdb 慢慢嘗試,因此修修改改花了很多時間,改了又錯又重改的情況多次發生,難以一一列舉,在此大略敘述過程遇到的狀況。
pointer aliasing issues
首先是前面提及 pointer aliasing 的問題,這個狀況是我在第一個版本遇到最大的問題,第一次是在開發加法時遇到。例如當想計算 a = a + b
時,若加出更大的位數則需要 reallocation ,但中途的 reallocation 失敗會導致傳入值 a
被破壞而正確的新 a
也沒有算出來,進退兩難的情況。
起初為了應對這個問題,我每次計算都先查看是否有 aliasing ,若有則進行複製, maintain 一份副本以利在 reallocation 失敗時能將數值復原。然而這個方法顯然導致很糟糕的效能,雖然之後為了節省交換的操作而直接計算在新空間,但仍然用了 3 個數。
使用者在使用界面時若指定 a = a + b
,那通常直覺也會認為自己使用了 in place 的算法,可是我在實作上卻無論如何都需要 3 個數的空間,違反使用直覺。
原程式碼 (add)
對於每個 測量 1000 次 計算後印出平均消耗時間。
其糟糕效能如下:

為此我後來發現若是能提早知道 reallocation 的大小,或至少知道該次計算的上界,那麼即可在開始計算之前就進行 reallocation 。如此一來即使配置失敗,那麼直接回傳 false 原先的值也不會被破壞。因此在計算 a = a + b
時便可以達成 in place 的目標,使用 2 個數的空間完成加法,也因為每次 ubignum_add()
都只會做一次 reallocation ,大幅降低了 memory allocation 的花費。此外也不再需要確認 aliasing ,移除判斷條件後也使程式碼簡潔許多。
改良 (add)
效能:

相比原先的版本加速將近十倍之多。
列印
第二個困難出在列印,雖然我的 ubignum
已能進行數種運算,也正確地將數值儲存在檔案中,但它們就是一串由上百、上千個 0, 1 組成的資料,難以為人類所理解。
為了將很長的二進位轉成十進制,我左思右想許久,最後認為終究是離不開除法的命運,還是得乖乖寫一個除法。我想起前幾周的測驗題中,恰好有計算除法的內容,於是我就嘗試套用這個模式,意圖以位移和減法來達成除法運算。此外因為目前沒有其它除法的應用場景,所以可以只考量「除以 10 」的情況並進行特化。
為了實作出 ubignum_divby_ten()
,首先需要 ubignum_left_shift()
及 ubignum_sub()
。
ubignum_sub()
由於我的大數體系原先完全不存在任何負的概念,因此想引入減法也需要做一些權衡。
不過因為除法中使用的減法會保證結果為非負 (餘數) ,如此一來就不必引入負數,減法直接使用二補數加法進行,並把最高的進位拋棄。基於直接假設減法都是大減小,原本的 ubignum 體系可以維持不變。
以下程式碼都展示解決前述 pointer aliasing 問題後的版本。
sub()
程式碼
ubignum_left_shift()
left_shift()
程式碼
大致上就是一些搬移的操作,在此解釋 29 至 33 行的操作,假設我們要將 a
:
左移 3 位。
- 注意到 chunk 編號是 ,左邊為 most significant 。
每次我們需要目前 chunk 的最低 5 位放到高 (置左) 與其右方 chunk 的最高 3 位放到低 (置右) 進行合併,以編號 為例即是:
- 取最低 5 位放到高:左移
- 取最高 3 位放到低:右移
最高跟最低的 chunk 都只有分別一邊所以需要特殊處理,最終得:
由於我們每次都不會去取目標左方的新 chunk ,因此 pointer aliasing 在此並不會造成問題。
另外,考量到當 shift
為 0 時,原本的程式碼會導致數值右移超過自己的 size ,產生 undefined behavior ,因此需要第 28 行的 if..else 來進行 branch 。
ubignum_divby_ten()
概念源於 quiz3 的算法。
在撰寫的過程中,再多寫了一個 ubignum_compare()
函式以利比較。
divby_ten()
程式碼
之後同樣如 quiz3 引入 builtin_clz()
並重新包裝成 ubignum_clz()
進行改進,關鍵段落變化:
ubignum_2decimal()
在實作完成除以 10 之後,我們即可透過不斷除以 10 來將二進位數轉成字串印出。
2decimal()
程式碼
目前我認為字串反轉的方式還可以再改良,但因為這個部份並非 fib 運算的主軸,因此暫時就先如此。
fibdrv 效能分析與改進
原先想與範例程式碼比較,然而稍微更改後在自己電腦實驗的結果卻非常糟:


因此目前先以自己改進為方向,
由於速度的提昇,我們現在將範圍拉到 來測試。

對於目前 fib sequence 逐項往上加的算法
我認為仍有可能改進的部份:
- 使用 inline asm 來做加法,如此 (在 64 位元情況) 可以避免用到
uint128_t
今天 (3/24) 上課的時候,聽到老師介紹 gcc 有 builtin 的 overflow 函式,藉由這點我們即可改寫原本的 ubn_unit_add()
,不需要低到 asm 的層級,相對 asm 的優點在於 architecture independence。然而這裡是需要 3 個數相加,因此需要呼叫兩次 builtin_uadd_overflow()
。

測試後可以發現在較大的 case 上,使用 builtin_uadd_overflow()
可以得到略佳的效果。再測到更大一些得:

當我嘗試將 提高到 3000 時,卻觀察到一個奇怪的現象,就是在 1000 之後,計算時間就不再有提昇的趨勢,如圖:

我覺得這非常不合理,因為以我的算法對於越大的數肯定有越高的計算量。目前想到的可能出在,因數值太大而產生 overflow ,但 20000 1000 也仍不到 32-bit int 的上界,應不可能超出 long long ,且可以從圖中見得有些數字遠遠超過水平線卻仍被印了出來。或許需要更理解 ktime_t 相關的知識。
On 64-bit systems, a ktime_t is really just a 64-bit integer value in nanoseconds.
–- The high-resolution timer API
以這個資料來說,顯然更不是時間變數資料型態的問題了。
經過數天之後我終於發現問題,是 fibdrv.c 中的 MAX_LENGTH
。之前改過一次之後我就忘記有這個參數,目前直接設定成 1000000 以利後續實驗。
更正後試跑已可正常進行統計:

嘗試引入 fast doubling 算法加速

效能改善的情況高於我的預期,我原先以為 fast doubling 要到很大的 case 才會有顯著的改善。但從實驗結果可以發現,在 左右兩種算法即出現執行時間的交叉。
試著仿造使用 perf stat
測量我寫的 fast doubling 算法到 :
可以看到我的實作程式碼有高達 15% 的 cache miss 。
我想利用 cachegrind 來測試是哪個函式發生最多 cache miss ,結果如下:
$ sudo valgrind --tool=cachegrind ./exp 1 > /dev/null
==37329== Cachegrind, a cache and branch-prediction profiler
==37329== Copyright (C) 2002-2017, and GNU GPL'd, by Nicholas Nethercote et al.
==37329== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==37329== Command: ./exp 1
==37329==
--37329-- warning: L3 cache found, using its data for the LL simulation.
==37329==
==37329== I refs: 69,563,678
==37329== I1 misses: 1,159
==37329== LLi misses: 1,128
==37329== I1 miss rate: 0.00%
==37329== LLi miss rate: 0.00%
==37329==
==37329== D refs: 28,270,312 (24,764,894 rd + 3,505,418 wr)
==37329== D1 misses: 3,323 ( 2,585 rd + 738 wr)
==37329== LLd misses: 2,733 ( 2,070 rd + 663 wr)
==37329== D1 miss rate: 0.0% ( 0.0% + 0.0% )
==37329== LLd miss rate: 0.0% ( 0.0% + 0.0% )
==37329==
==37329== LL refs: 4,482 ( 3,744 rd + 738 wr)
==37329== LL misses: 3,861 ( 3,198 rd + 663 wr)
==37329== LL miss rate: 0.0% ( 0.0% + 0.0% )
$ sudo cg_annotate cachegrind.out.37329
--------------------------------------------------------------------------------
I1 cache: 32768 B, 64 B, 8-way associative
D1 cache: 32768 B, 64 B, 8-way associative
LL cache: 6291456 B, 64 B, 12-way associative
Command: ./exp 1
Data file: cachegrind.out.37329
Events recorded: Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw
Events shown: Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw
Event sort order: Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw
Thresholds: 0.1 100 100 100 100 100 100 100 100
Include dirs:
User annotated:
Auto-annotation: off
--------------------------------------------------------------------------------
Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw
--------------------------------------------------------------------------------
69,563,678 1,159 1,128 24,764,894 2,585 2,070 3,505,418 738 663 PROGRAM TOTALS
--------------------------------------------------------------------------------
Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw file:function
--------------------------------------------------------------------------------
33,078,041 4 4 15,024,011 1 0 3,012,013 0 0 ???:main
27,000,072 1 1 6,000,016 0 0 0 0 0 /build/glibc-sMfBJT/glibc-2.31/io/../sysdeps/unix/sysv/linux/write.c:write
6,054,221 27 24 3,027,082 11 0 15 1 1 ???:???
1,493,991 48 47 398,997 19 6 261,001 6 2 /build/glibc-sMfBJT/glibc-2.31/stdio-common/vfprintf-internal.c:__vfprintf_internal
648,170 4 4 141,032 4 1 114,012 1 0 /build/glibc-sMfBJT/glibc-2.31/libio/fileops.c:_IO_file_xsputn@@GLIBC_2.2.5
418,262 3 3 30,733 2 1 24,733 0 0 /build/glibc-sMfBJT/glibc-2.31/stdio-common/_itoa.c:_itoa_word
228,000 5 5 21,000 3 2 0 0 0 /build/glibc-sMfBJT/glibc-2.31/string/../sysdeps/x86_64/multiarch/strchr-avx2.S:__strchrnul_avx2
163,977 2 2 29,983 0 0 17,985 63 63 /build/glibc-sMfBJT/glibc-2.31/string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:__memcpy_avx_unaligned_erms
90,000 4 4 18,000 3 0 33,000 3 2 /build/glibc-sMfBJT/glibc-2.31/stdio-common/printf.c:printf
78,000 1 1 24,000 0 0 6,000 0 0 /build/glibc-sMfBJT/glibc-2.31/stdio-common/../libio/libioP.h:__vfprintf_internal
71,545 10 10 14,316 1,080 893 15 2 0 /build/glibc-sMfBJT/glibc-2.31/elf/dl-addr.c:_dl_addr
cachegrind 似乎無法配合 taskset 使用
改用
即可正常使用 cachegrind ,推測為 Valgrind 實作方式的影響
測試結果竟顯示 data cache miss 為 0% ,與從 perf stat 收到的資訊天差地遠。
可以觀察到 cache reference 在 perf 顯示 95,829,386 ;而在 cachegrind 顯示為 28,270,312 。
進行更多次 perf 測試,發現每次的 cache references 竟有巨大落差:
我不曉得為什麼會出現這樣的情況。
後續發現是測試次數過少而導致的數值不穩定,後面會補充結果。
回去觀摩參考共筆,發現因為我先前閱讀的重點都在運算加速,沒注意到資料搬移優化的部份,嘗試將其納入我的實作:
效能差異:

我測出來的結果和參考共筆的結論不符合,並沒有出現效能大幅改善的情形。
perf 測起來仍然非常不穩:
perf 測試
嘗試在 perf 命令設置參數 -r 10
來降低測量數值的波動,為避免時間過長,改為測試 fib 到 。
經過這個設定後,每次測試間得到的數值就不再有顯著差異,至少每個 events 的前兩個數字會保持不變。
確立測試方法之後再回去重新測量使用 ubignum_copy()
的版本,得:
對照之下,從舊版到新版雖然 cache-miss-rate 由 13% 增長到 20% ,但我們可以注意到 cache-references 總數大約降低了一半,我推測這個差距來自於 copy 的過程對資料的 access 。
因此就 data cache 而言,因為省下這些操作,新版仍是更佳的實作方式。
20% 的 cache miss 非常驚人,因此目前最直接的改進目標就是要想辦法降低 miss rate 。
引入 ubignum_square()
差異實在也不是很明顯,甚至是落後於全數使用 ubignum_mult()
的版本。
雖然我想要嘗試測到大一點的 case ,
執行測試有時會出現頻繁的失敗,不確定原因是什麼,但與引入 ubignum_square()
應該沒有關聯,因為即使改回 mult 的版本也沒有恢復。其錯誤情景如下:
我想看看是否是 fib_fast()
的計算過程中的問題,例如 reallocation 失敗等等,因此嘗試引入 flag
變數來紀錄:
fib_fast()
執行後於 dmesg
中並未看到任何 flag
相關的報錯,不過出現了一段紅底黑字:
June Rework
隔一段時間沒有著手於這份專案,回來先進行一些實驗重做等等,並補上一些之前忽略的內容。
由於第一次做的時候認為需要專注於提昇運算 Fibonacci number 的速度,而只測量計算消耗的時間。然而這類 character device 在使用上總是要 read,因此應該也進行實驗量測 user space 到 kernel space 的時間、以及資料複製的時間等等納入成本計算,進行效能的全盤考量,完整地進行實驗才能正確認知到效能瓶頸何在。
user-kernel transition time
時間量測
首先實驗量測由 user mode 進出 kernel mode 的耗時
code
以 iteration 算法測試,並使用較小的 offset 來突顯該數值,兩個時間相減大致可得 (user kernel) (kernel user) 的時間。

即使用上了作業說明和參考共筆提及的所有方式,仍會產生這種巨大的 peak。從 csv 檔觀察,發現其位於
- fib(13) = 233
- fib(43) = 433494437
- fib(44) = 701408733
- fib(63) = 6557470319842
- fib(64) = 10610209857723
第一猜想是使用 ubignum_resize()
導致的消耗,但結果這幾個值看起來也並非是 UINT32_MAX
或 UINT64_MAX
,從此 value 上看不出有特別意義。接著又測試幾次,發現每次 peak 都產生在不同地方。
試著排除 peak 點再次作圖,結果發現在其它情況我的量測也不太穩定。

換成另一種叫簡易的方式來測量:將 fib_write()
函式清空並直接把 user space 下的來回時間當成是 transition time。

無論怎麼做都一直量到 peak,但可以看出這個來回時間幾乎都非常貼近 1000 ns。
為了考察這個問題,我試著把每次測試中過大的的耗時數值印出來,結果發現偶爾會出現 case 有十倍左右的時間消耗。於是我了解到排除統計上的離群值有其必要性,果然是不該偷吃步。
排除統計離群值
在統計的過程中,對於某些不特定的 會出現標準差大於平均值的奇怪現象,於是我同樣把過程中的資料全部存下來,再去奇怪的 看看出了什麼問題,借助 sprintf()
的小技巧來把資料全部存起來:
我這裡跑完時間測量後,看到 時 ,於是查詢 data/67.dat,原來其中一次的耗時測量到 42656。然而經過離群值的排除,調整後的平均值正常地落在了 1085。
outlier elimination
其中 sqrt()
為我自己實作的整數開根號函式。
至此我們成功排除掉了 peak 的問題,於是耗時的細微變化便可在圖表上呈現出來。

圖表的刻度變小之後,可以看出測量上有 100 ~ 300 ns 的起伏,仍無法達到如 KYG 同學的平滑結果。這裡由於任何 的輸入都是直接 return 沒有計算,因此最理想的狀況應該要是一條水平線。
打算做一個 commit 時收到警告訊息:
於是改為使用 snprint()
並順利完成 commit。
於 desktop 量測
由於怎麼測總是拿到漂浮的數據,我想 laptop 或許天生就較為不穩定,因此我想從 laptop 改成用 desktop 跑跑看是否會出現相同的結果。
cpu info
然而同樣的程式碼和 scripts 移過來執行竟然出現 segmentation fault。
我使用 gdb 工具查詢,發現出錯在 fprintf()
處,我的 f
意圖打開目錄 data 下的檔案,然而不存在這個 directory 因此產生記憶體區段錯誤,mkdir data
後解決。
跑出來的圖表:

雖然看似仍有很大的浮動,但仔細觀察可以注意到 y 軸的刻度由 50 降至 10,確實穩定了不少。從比較大的 scale 看是像這樣子:

雖然還沒辦法變成完全的水平線,但已從最初上千 ns 的 peak 改善許多。
版本 commit id:b80d536b08bd1a3c3325b53d684f97e39ac734f9
接著再把之前從 fib_write()
清空的內容補回進行測試:

可以看出無論 的變化,user space 到 kernel space 的來回時間穩定落在 600 ns 左右。
即使一路算到 也是如此:

版本 id:766a4b80f6fbcf8a0f5ee0931113b16e6ea629ec
運算時間
繼續使用 desktop 來重新量測兩種 Fibonacci number 算法在 kernel space 的耗時:

像之前一樣看到我實作的 fast fib 算法會出現這種階梯形的跳躍現象,這次我想要好好研究這些階梯的成因。
先從小一點的 case 開始探討:

從 fast.csv 查看發現跳點出現在 ,每次往上跳 500 或 1000 ns。每個跳躍竟然都恰好落在 2 的冪,這肯定有很好的理由可以解釋。
仔細想想之後也很直觀,因為 fast fib 算法每次迴圈都會將 加倍,因此需要 次 iteration 以算完要求結果。而這麼解釋就代表每個跳躍便是 fast doubling 算法 each iteration 的耗時,從 fast_fib()
程式碼也可以看出有個迴圈:
在這裡可以簡單比較兩種算法的特性如下:
|
sequencial |
fast doubling |
迭代次數 |
|
|
每輪迭代的複雜度 |
低,約 16 ns |
高,約 834 ns |
該數字使用 到 的測量結果算得
比較
KYG 同學
為了確認自己實作的水準,clone KYG 同學的 fibdrv 進行對比,使用 optimize-bn 分支在我的電腦上執行 make plot
,結果超乎我的預期:

耗時僅僅我的 ,顯然我的程式碼有不只一點改進空間。此外,為何使用相同的設定,它的程式就能有非常平滑的結果,而我卻會一直抖動,這也需要再好好研究。
但仔細去看程式碼發現其測量有誤,我 clone 到的版本並非 bignum 的實作。接著繼續閱讀之後,發現同學並未把自己的最新版本推上 github,因此我無法在自己的電腦上與其進行耗時比較。
研讀同學的實作,看看是否有值得借鏡的地方。
apm_internal.h
此 apm 取的是 Arbitrary-Precision arithmetic 的意思
首先我看到同學將 inline assembly 的部份包裝成 macro:
我認為這是非常好的想法,由於 inline assembly 的語法邏輯跟 C 的邏輯有些不同,因此包裝起來可以使人閱讀時統一依 C 的邏輯理解,較為易讀,同時也方便重複使用。
同時他在無法使用 x86 assembly 的情況下仍提供了對應實作:
apm.c apm.h
這部份提供許多大數的 operations 包括:
- increment/decrement
- add
- sub
- dmul,計算 partial product 的函式
- shift
- cmp
預設是 u
和 v
運算後將結果放到 w
,其中沒看到計算 apm 乘法的函式,僅有計算 partial product 的 dmul()
。
- 帶 i postfix 的 operations 似乎是代表 in-place,都是
u
, v
運算後放到 u
- 帶 n postfix 的 operations 不確定語意上是代表哪個英文單字,不過其共通點為
u
, v
兩數的初始 size 相同
mul.c
在其乘法實作當中混用了兩種乘法
短碼時使用直式乘法,長碼時使用 Karatsuba,使兩者各展所長。
Karatsuba multiplication 解說
假設我們要作 ,那麼藉由分解較大的 及 ,可以達到:
因 ,可將乘法次數由 4 次降到 3 次。
此外每個 又呈現 的形式,因此可以再次遞迴下去,總結而言這是個 divide and conquer 的算法。
bn.h
此部份將前面大數相關的 type 使用 struct 包起來:
並使用 apm 的介面提供一系列的 arithmetic operations,再次包裝成 bn 介面。