contribute by < Shiritai
>
如果純粹將 reposiroty clone 好後使用 VSCode 開啟比如 fibdrv.c
,相信會看到貼心的 IntelliSense 的警告。
解決辦法整理如下。
開始寫作業前都應按流程安裝好 linux kernel header,確認其安裝的路徑,可移動至 /usr/src
下查看。
由以下指令確認當前 Linux 核心版本,得知第二個是我們感興趣的 kernel header。
由前車之鑑 1 和 2 得知 IntelliSense 需要調整 C/C++ Extension 設定檔: c_cpp_properties.json
。
準備 Linux 設定檔模板
由 @DokiDokiPB 的建議,對此份作業來說,cStandard
的前綴應該改成 gnu,以協助 intelliSense 解析一些結構。對此我也更新腳本法的實作,詳見 commit df77974。
加入 includePath
設定
要調整兩點。
LINUX_KERNEL_HEADER
換成自己的 kernel header 路徑名。GCC_VERSION
換成自己的 GCC 版本。基本上 includePath
中前五項都是開發 Kernel Module 的常客,當然第七項編譯器本身也是。第六項本次實作會用到,不過未來開發其他模組的話可能會引入其他的 header。
調整 defines
使 MODULE_*
等巨集被正確的識別
defines
是為了解決條件編譯的問題,當中加入的字串會使 IntelliSense 在 indexing 時看見需指定條件編譯後才會編譯到的程式碼。
到此便完成設定,IntelliSense 復活了 :)
Reference
樹狀遞迴版
線性遞迴版
Binet’s Formula
當中指數與根號運算造成的計算誤差和複雜度都不容小覷。費氏數列分析一文使用 GNU 的 GMP
, MPFFR
這倆透過盡情使用記憶體資源計算遞迴版 (下述分解法)和本公式解的運算時間,得到前者大勝後者的結果。
最早可追溯至 Nikolai. N. Vorobev 提出的方法。
由費氏數列的遞迴定義,也許你會猜測是否能尋找費氏數列中各項的關聯,分解法便是對此疑問的解答。令費氏數第 項為 ,則
上式對 成立。
證明
如果根據費氏數列定義拆解此式,可以得到如回音般的結果。
這樣的結果與原本分解式的展開只差在 和 的互換。並不能給我們額外的結論,也許我們應該另尋其道。通常遇到這種難以直接證明的,我們可以用終極絕招數學歸納法:
Base case: 帶入即正確。
好像沒有寫的必要 :)
Inductive case
假設分解式對 成立,則當 時,利用費氏數列的定義和 的情況,可得
由數學歸納法得證。
以分解法為基礎,今若要求 ,有
可以發現 需要 。那麼 呢?
也需要 ,真好。
不過其實有其他做法,同樣考慮 ,有
由於我們透過合併改關注 項,故改成考慮 項 ,有
後者的結論是 和 都需要 和 ,是原文中的方法,兩者的結果都很漂亮,畢竟是等價的東西。
以前者 (, 需 ) 為例,取得二的冪就不是問題,假設我們想知道 ,可以藉由以下順序求得:
繼續以前者為例,那如果是任意數呢?假設要計算 ,我們需要 和 ,為此我們還需要 ,同樣我們又需要 , 最後回到 便皆大歡喜。看來無論哪個正整數,都可以使用此演算法。下圖圖像化這段流程,與上圖不同在於稍作一點簡化。
另外前述「前者」與「後者」的差異可透過上圖描述。前者是以數字大者 (比如 中的 ) 為主視角,後者則是以數字小者 ( 中的 ) 為主視角,兩者實際上會畫出一樣的關係圖。
由於實作上以後者視角比較好描述,今後將採用該視角,與作業說明同步。
以 為例,當初我們以 top-down 的角度分析,實作時勢必需要遞迴,如果不想要遞迴呢?我們要如何知道接下來是單純從 爬兩倍到 ,還是額外加一,從 到 ?
把之前的流程圖加上是否「加一」的標記:令以 表加一,以 表不加,同時將 這個 trivial 項補回來…
敏銳而有看作業說明的你想必已經觀察到了,標記的 不正是二進制版的 嗎?這顯然不是巧合,因為前述問題實際上等價於:
給定兩種操作:乘二、加一。
如何從 開始用最少計算次數抵達某正整數 ?
上述是在任意進位制的角度下問的問題,換成熟悉二進制與位元運算的你所熟悉的語言就是:
給定兩種操作:左移一位、加一。
如何從 開始用最少計算次數產生某二進制數 ?
這一問等於白問,因為答案即藏在 的二進制編碼 XD
不考慮大數運算的情況下,針對 long long
,為了實現 Fast doubling,首先須取得目標費波那契數的二進制編碼,接著由高至低走訪這些編碼完成乘二和加一的邏輯。
取得一個數字精確的二進制編碼,有以下兩個針對 long long
的 gcc 內建函式:
__builtin_clzll
: 其在 intel x86_64 架構下可能使用硬體指令 bit search reverse 找尋首個 1
bit,接著以 63 減去而取得領導的 0
bit 數量。__builtin_ctzll
同理在 intel x86_64 可能使用 bit search forward,最終取得末尾的 0
bit 數量。前者的使用可以加速略過領導之 0
bits 的過程,後者則由於 fast doubling 的「加一與否」牽扯到 branching,而且很難直接避免而轉換成 branchless,不過若所有位元皆為零就不需顧慮 branch 操作,故可以透過後者完成末尾 0
bits 的 branchless 乘二操作,同時降低 branching 時的比較開銷。
具體實作解釋如下。
0
bits 數量,並調整 的 ,將領導的 1
bit 上調至最高位元。
注意 c{l,t}z_long_long
為自己實作的函式,以巨集控制編譯時要採用何者。
1
bit 的存在,即 是否為零。
0
bits 所代表之「乘二」邏輯。
迴圈內 if…else… 的部份可用
-!!(target & (1 << i))
作為 mask 的技巧簡化成:
(target & (1 << i))
的意思為檢查某位元是否為 1
,前面加上 !!
表取得真或假 (0 ro 1),加負號表取得 0xFFFFFFFFFFFFFFFF
。
最後根據此 mask 過濾是否採用 fib_2n0
, fib_2n1
與否,十分精妙。
最後我採納此技巧,將原本的 branching 調整成 branchless,不過還是保留第三階段的 branchless,以達到效能的最大提升。以下展示原 branching 調整的部分。
雖然 __builtin_clzll
和 __builtin_ctzll
好用且高效,但本方法的使用受限於 gcc 與特定型別 (支援 unsigned int
, unsigned long
和 unsigned long long
);且參考 GCC 官方文件,當輸入為 0
時屬於 undefined behavior,可見略有不足之處。於是我受 bitwise 操作教學文件的啟發,設計巨集來生成 clz
和 ctz
函式,支援所有長度在 64 以內的型別。
原本教學文件中的程式碼如下。
要改成 long long
很簡單,簡單調整一下即可,如下。
計算時間一樣是確定的 (deterministic),不過實在有失優雅。主要有三點:
== 0
邏輯可以簡化第一點即採用 !(x & __)
。第二點的做法可能根據考量點而有所不同,比如此文提供一個很有趣的想法,考量最後一步的效能,他利用 bitwise 操作將分支與加法合併:
若考慮消去 bad smell 的話,採用迴圈可能是更好的做法。雖然效能上可能會稍微降低,不過程式碼會變的簡潔易讀,且容易擴展。搭配前置處理器技巧,想擴展至更多型別便不是問題。
首先將函式調整成迴圈版。
可以發現,透過改成迴圈版,我們將與 long long
型別有關的變因獨立在迴圈之前,那是不是調整這些變因就可以適用於其他型別呢?沒錯,並且搭配前置處理器,程式碼的前段變成:
如此一來,我們可以輕鬆產生長度在 64 bits 內的所有 clz
,產生方法如下:
上百行的函式就這麼樸實無華的產生了。ctz
同理,只要進行如下的調整…
注意到第一個差異使用 ~
而非反方向位移是因為右移與型別 (type
) 的符號有關,對於有號數,全一再怎麼 (算數) 右移都是全一。
如此,我們又可以使出四兩撥千斤之術生成 ctz_XXX
函式們,一勞永逸 :)
測試的部分,可利用與 __builtin_c{l,t}z
來比較輸出,在此省略細節。不過我也是因此注意到 __builtin_c{l,t}z
的 undefined behavior。
恩,我不知道自己多久以後才會完善測試與筆記,還是直接放連結吧。
至於 Repo 中怎麼沒什麼東西…不好意思等我一…下 QwQ
Shiritai
Repo 的部分…
循著作業說明操作的紀錄。
可以看見 510, 0
兩數,分別為 device major/minor number。
看完作業說明影片後發現自己讀+實作/實驗太少了,先回去讀…