# 2023q1 Homework3 (fibdrv)
contribute by < [`Shiritai`](https://github.com/Shiritai/fibdrv) >
:::spoiler 開發環境
## 開發環境
```bash
$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 12
On-line CPU(s) list: 0-11
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
CPU family: 6
Model: 158
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
Stepping: 10
CPU max MHz: 4100.0000
CPU min MHz: 800.0000
BogoMIPS: 4399.99
```
:::
## 以 VSCode 開發 Linux Kernel Module 的準備
:::success
根據 @jserv 的延伸問題,為此單元的內容進行擴充。
以下留下舊的懶人包,[點我瀏覽完整筆記](https://hackmd.io/@Eroiko/vscode-for-linux-kernel)與[客製化腳本](https://github.com/Shiritai/make2IntelliSense)。
:::
如果純粹將 reposiroty clone 好後使用 VSCode 開啟比如 `fibdrv.c`,相信會看到貼心的 IntelliSense 的警告。
![vscode-intellisense-error](https://i.imgur.com/xoUqDkX.png)
解決辦法整理如下。
### 確認 kernel header 路徑
[開始寫作業前](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-b)都應按流程安裝好 linux kernel header,確認其安裝的路徑,可移動至 `/usr/src` 下查看。
```bash
$ ls /usr/src | grep linux-headers
linux-headers-5.19.0-32-generic
linux-headers-5.19.0-35-generic
```
由以下指令確認當前 Linux 核心版本,得知第二個是我們感興趣的 kernel header。
```bash
uname -r
5.19.0-35-generic
```
### 修改 C/C++ Extension 設定檔
由前車之鑑 [1](https://stackoverflow.com/questions/58386640/how-to-develop-linux-kernel-module-with-vscode-without-incorrect-error-detection) 和 [2](https://github.com/microsoft/vscode-cpptools/issues/5588) 得知 IntelliSense 需要調整 [C/C++ Extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) 設定檔: `c_cpp_properties.json`。
1. 準備 Linux 設定檔模板
:::success
由 @DokiDokiPB 的建議,對此份作業來說,`cStandard` 的前綴應該改成 gnu,以協助 intelliSense 解析一些結構。對此我也更新腳本法的實作,詳見 [commit df77974](https://github.com/Shiritai/make2IntelliSense/commit/df7797490d4304961a2b60f488d45b6b7f0be9a8)。
:::
```diff
{
"configurations": [
{
"name": "Linux",
"browse": {
"limitSymbolsToIncludedHeaders": true,
"databaseFilename": ""
},
"intelliSenseMode": "linux-gcc-x64",
"compilerPath": "/usr/bin/gcc",
- "cStandard": "c99",
+ "cStandard": "gnu99",
"cppStandard": "gnu++17"
}
],
"version": 4
}
```
2. 加入 `includePath` 設定
```json
...
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/usr/include",
"/usr/local/include",
"/usr/src/LINUX_KERNEL_HEADER/include",
"/usr/src/LINUX_KERNEL_HEADER/arch/x86/include",
"/usr/src/LINUX_KERNEL_HEADER/arch/x86/include/generated",
"/usr/lib/gcc/x86_64-linux-gnu/GCC_VERSION/include"
],
"browse": {
...
```
要調整兩點。
* `LINUX_KERNEL_HEADER` 換成自己的 kernel header 路徑名。
* `GCC_VERSION` 換成自己的 GCC 版本。
基本上 `includePath` 中前五項都是開發 Kernel Module 的常客,當然第七項編譯器本身也是。第六項本次實作會用到,不過未來開發其他模組的話可能會引入其他的 header。
3. 調整 `defines` 使 `MODULE_*` 等巨集被正確的識別
`defines` 是為了解決條件編譯的問題,當中加入的字串會使 IntelliSense 在 indexing 時看見需指定條件編譯後才會編譯到的程式碼。
```json
...
"compilerPath": "/usr/bin/gcc",
"defines": [
"__GNUC__",
"__KERNEL__",
"MODULE"
],
"cStandard": "c99",
...
到此便完成設定,IntelliSense 復活了 :)
![vscode-intellisense-good](https://i.imgur.com/9B5yt8R.png)
## 費氏數列的計算方法
> Reference
> * [L04: fibdrv](https://hackmd.io/@sysprog/linux2023-fibdrv-a#-%E8%B2%BB%E6%B0%8F%E6%95%B8%E5%88%97)
> * [費氏數列分析](https://hackmd.io/@sysprog/fibonacci-number)
> * [費氏數怎麼算](https://scm.iis.sinica.edu.tw/ncs/2019/06/how-to-compute-fibonacci/)
### 經典款
* 樹狀遞迴版
$$
fib :: \mathbb{R} \rightarrow \mathbb{R} \\
fib(0) = 0 \\
fib(1) = 1 \\
fib(n+2) = fib(n+1) + fib(n)
$$
* 線性遞迴版
$$
fib2 :: \mathbb{R} \rightarrow (\mathbb{R}, \mathbb{R}) \\
fib2(0) = (0, 1) \\
fib2(n+1) = (y, x + y) \\
where\ (x, y) = fib2(n)
$$
### 公式解
> Binet’s Formula
$$
\dfrac{1}{\sqrt{5}} \Big(
\big(
\dfrac{1 + \sqrt{5}}{2}
\big)^n - \big(
\dfrac{1 - \sqrt{5}}{2}
\big)^n
\Big)
$$
當中指數與根號運算造成的計算誤差和複雜度都不容小覷。[費氏數列分析](https://hackmd.io/@sysprog/fibonacci-number#%E8%A7%A3%E6%B1%BA%E9%81%8B%E7%AE%97%E7%B2%BE%E6%BA%96%E5%BA%A6%E7%9A%84%E5%95%8F%E9%A1%8C)一文使用 GNU 的 `GMP`, `MPFFR` 這倆透過盡情使用記憶體資源計算遞迴版 (下述分解法)和本公式解的運算時間,得到前者大勝後者的結果。
### 分解法與 Fast Doubling
> 最早可追溯至 [Nikolai. N. Vorobev 提出的方法](https://www.amazon.com/Fibonacci-Numbers-Dover-Books-Mathematics/dp/048648386X)。
由費氏數列的遞迴定義,也許你會猜測是否能尋找費氏數列中各項的關聯,分解法便是對此疑問的解答。令費氏數第 $n$ 項為 $f_n$,則
$$
f_{n+k} = f_{n-1} \cdot f_k + f_n \cdot f_{k+1}
$$
上式對 $n \ge 1, k \ge 0 \ \land n, k \in \mathbb{R}$ 成立。
:::success
**證明**
如果根據費氏數列定義拆解此式,可以得到如回音般的結果。
$$
f_{n+k} = f_{n-1} \cdot f_k + f_n \cdot f_{k+1} \\
= f_{n-1} \cdot f_k + f_{n} \cdot \big(
f_{k} + f_{k-1}
\big) \\
= \big(
f_{n-1} + f_{n}
\big) \cdot f_{k} + f_n + f_{k-1} \\
= f_{n+1} \cdot f_k + f_n \cdot f_{k-1}
$$
這樣的結果與原本分解式的展開只差在 $n$ 和 $k$ 的互換。並不能給我們額外的結論,也許我們應該另尋其道。通常遇到這種難以直接證明的,我們可以用終極絕招數學歸納法:
* Base case: 帶入即正確。
> 好像沒有寫的必要 :)
$$
f_{1} = f_{1+0} = f_{0} \cdot f_{0} + f_{1} \cdot f_{1} = f{1} = 1
$$
* Inductive case
假設分解式對 $t \le n$ 成立,則當 $t = n + 1$ 時,利用費氏數列的定義和 $t \le n$ 的情況,可得
$$
f_{(n+1)+k} \stackrel{\rm{def\ of\ fib}}{=}
f_{n+k} + f_{(n-1)+k} \\
\stackrel{\rm{eq\ when\ }t \le n}{=}
f_{n-1} \cdot f_k + f_n \cdot f_{k+1} + f_{n-2} \cdot f_k + f_{n-1} \cdot f_{k+1} \\
= \big(
f_{n-1} + f_{n-2}
\big) \cdot f_k + \big(
f_{n} + f_{n-1}
\big) \cdot f_{k+1} \\
\stackrel{\rm{def\ of\ fib}}{=}
f_{n} \cdot f_k + f_{n+1} \cdot f_{k+1}
$$
由數學歸納法得證。
:::
以分解法為基礎,今若要求 $f_{2n}$,有
$$
f_{2n} = f_{n+n} = f_{n-1} \cdot f_{n} + f_{n} \cdot f_{n+1} \\
= f_{n-1} \cdot f_{n} + f_{n} \cdot (f_{n} + f_{n-1}) \\
= f_{n-1} \cdot 2 f_{n} + f_{n} \cdot f_{n}
= f_n \cdot (2f_{n-1} + f_{n})
$$
可以發現 $f_{2n}$ 需要 $f_{n-1}, f_{n}$。那麼 $f_{2n-1}$ 呢?
$$
f_{2n-1} = f_{n+(n-1)} = f_{n-1} \cdot f_{n-1} + f_{n} \cdot f_{n}
= f_{n-1}^2 + f_n^2
$$
也需要 $f_{n-1}, f_{n}$,真好。
不過其實有其他做法,同樣考慮 $f_{2n}$,有
$$
f_{2n} \stackrel{\rm{prev\ result}}{=} f_n \cdot (2f_{n-1} + f_{n}) \\
= f_n \cdot (2f_{n+1} - f_{n})
$$
由於我們透過合併改關注 $n+1$ 項,故改成考慮 $2n+1$ 項 $f_{2n+1}$,有
$$
f_{2n+1} = f_{(n+1)+n} = f_{n} \cdot f_{n} + f_{n+1} \cdot f_{n+1}
= f_{n}^2 + f_{n+1}^2
$$
後者的結論是 $f_{2n}$ 和 $f_{2n+1}$ 都需要 $f_n$ 和 $f_{n+1}$,是[原文](https://hackmd.io/@sysprog/fibonacci-number#Vorobev-%E7%AD%89%E5%BC%8F)中的方法,兩者的結果都很漂亮,畢竟是等價的東西。
以前者 ($f_{2n}$, $f_{2n-1}$ 需 $f_{n}, f_{n-1}$) 為例,取得二的冪就不是問題,假設我們想知道 $f_{16}$,可以藉由以下順序求得:
```graphviz
digraph twos_power {
rankdir=LR;
node[shape=record];
edge[arrowsize=0.5]
12[label="<b> f2|<a> f1"]; 34[label="<b> f4|<a> f3"];
78[label="<b> f8|<a> f7"]; 16[label="f16"];
12:a -> 34:a -> 78:a -> 16;
12:b -> 34:b -> 78:b -> 16;
12:a -> 34:b -> 78:a;
12:b -> 34:a -> 78:b;
}
```
繼續以前者為例,那如果是任意數呢?假設要計算 $f_{21} = f_{2n-1}$,我們需要 $f_n = f_{11}$ 和 $f_{n-1} = f_{10}$,為此我們還需要 $f_6, f_5$,同樣我們又需要 $f_3, f_2$, 最後回到 $f_2, f_1$ 便皆大歡喜。看來無論哪個正整數,都可以使用此演算法。下圖圖像化這段流程,與上圖不同在於稍作一點簡化。
```graphviz
digraph flow_21 {
rankdir=LR;
node[shape=square];
1[label="f2 \n f1"];
2[label="f3 \n f2"];
3[label="f6 \n f5"];
4[label="f11 \n f10"];
5[label="f22 \n f21"];
1 -> 2 -> 3 -> 4 -> 5;
}
```
另外前述「前者」與「後者」的差異可透過上圖描述。前者是以數字大者 (比如 $[22, 21]$ 中的 $22$) 為主視角,後者則是以數字小者 ($[22, 21]$ 中的 $21$) 為主視角,兩者實際上會畫出一樣的關係圖。
由於實作上以後者視角比較好描述,今後將採用該視角,與[作業說明](https://hackmd.io/@sysprog/linux2023-fibdrv-a#-%E8%B2%BB%E6%B0%8F%E6%95%B8%E5%88%97)同步。
### Fast Doubling 的程式化
以 $f_{21}$ 為例,當初我們以 top-down 的角度分析,實作時勢必需要遞迴,如果不想要遞迴呢?我們要如何知道接下來是單純從 $(5, 6)$ 爬兩倍到 $(10, 11)$,還是額外加一,從 $(2, 3)$ 到 $(5, 6)$?
```graphviz
digraph flow_21 {
rankdir=LR;
node[shape=square];
subgraph 1 {
3[label="f6 \n f5"];
4[label="f11 \n f10"];
3 -> 4;
}
subgraph 2 {
1[label="f3 \n f2"];
2[label="f6 \n f5"];
1 -> 2;
}
}
```
把之前的流程圖加上是否「加一」的標記:令以 $1$ 表加一,以 $0$ 表不加,同時將 $f_0, f_1$ 這個 trivial 項補回來...
```graphviz
digraph flow_21 {
rankdir=LR;
node[shape=square];
0[label="f1 \n f0"];
1[label="f2 \n f1"];
2[label="f3 \n f2"];
3[label="f6 \n f5"];
4[label="f11 \n f10"];
5[label="f22 \n f21"];
0 -> 1 [label="1"];
1 -> 2 [label="0"];
2 -> 3 [label="1"];
3 -> 4 [label="0"];
4 -> 5 [label="1"];
}
```
敏銳而有看作業說明的你想必已經觀察到了,標記的 $10101_{2}$ 不正是二進制版的 $21_{10}$ 嗎?這顯然不是巧合,因為前述問題實際上等價於:
:::info
給定兩種操作:乘二、加一。
如何從 $0$ 開始用最少計算次數抵達某正整數 $n$?
:::
上述是在任意進位制的角度下問的問題,換成熟悉二進制與位元運算的你所熟悉的語言就是:
:::success
給定兩種操作:左移一位、加一。
如何從 $0$ 開始用最少計算次數產生某二進制數 $n$?
:::
這一問等於白問,因為答案即藏在 $n$ 的二進制編碼 XD
希望這能幫助你理解[作業說明中虛擬碼](https://hackmd.io/@sysprog/linux2023-fibdrv-a#-%E8%B2%BB%E6%B0%8F%E6%95%B8%E5%88%97)和[硬體加速手法](https://hackmd.io/@sysprog/linux2023-fibdrv-a#%E8%80%83%E6%85%AE%E5%88%B0%E7%A1%AC%E9%AB%94%E5%8A%A0%E9%80%9F-Fn-%E7%9A%84%E6%89%8B%E6%B3%95)的原理。
### Fast doubling 實作
不考慮大數運算的情況下,針對 `long long`,為了實現 Fast doubling,首先須取得目標費波那契數的二進制編碼,接著由高至低走訪這些編碼完成乘二和加一的邏輯。
取得一個數字精確的二進制編碼,有以下兩個針對 `long long` 的 gcc 內建函式:
1. `__builtin_clzll`: 其在 intel x86_64 架構下可能使用硬體指令 bit search reverse 找尋首個 `1` bit,接著以 63 減去而取得領導的 `0` bit 數量。
2. `__builtin_ctzll` 同理在 intel x86_64 可能使用 bit search forward,最終取得末尾的 `0` bit 數量。
前者的使用可以加速略過領導之 `0` bits 的過程,後者則由於 fast doubling 的「加一與否」牽扯到 branching,而且很難直接避免而轉換成 branchless,不過若所有位元皆為零就不需顧慮 branch 操作,故可以透過後者完成末尾 `0` bits 的 branchless 乘二操作,同時降低 branching 時的比較開銷。
具體實作解釋如下。
1. 取得領導、末尾 `0` bits 數量,並調整 $f(n)$ 的 $n$,將領導的 `1` bit 上調至最高位元。
:::success
注意 `c{l,t}z_long_long` 為[自己實作的函式](#Count-leading-zeros-%E5%AF%A6%E4%BD%9C),以巨集控制編譯時要採用何者。
:::
```c=
long long fn = 0ll, // f(n)
fnp1 = 1ll; // f(n+1) n plus 1
#ifdef MY_CZ
const int lz = clz_long_long(n);
const int tz = ctz_long_long(n);
#else
const int lz = __builtin_clzll(n);
const int tz = __builtin_ctzll(n);
#endif /* MY_CZ */
n <<= lz; // so now there is no leading zeros
```
2. Branching 部分: 根據當前最高位元,處理「乘二」和「是否加一」,注意到「乘二」的邏輯可使用 bitwise left shift 完成,且 branching 的條件只需考慮是否還有 `1` bit 的存在,即 $n$ 是否為零。
```c=
while (n) { // exists "1" bit
long long f2n = fn * ((fnp1 << 1) - fn),
f2np1 = fn * fn + fnp1 * fnp1;
if (n & 0x8000000000000000ll) {
fn = f2np1, fnp1 = f2np1 + f2n;
} else {
fn = f2n, fnp1 = f2np1;
}
n <<= 1;
}
```
3. Branchless 部分: 完成末尾 `0` bits 所代表之「乘二」邏輯。
```c=
// speed up for tailing zeros: branchless
for (int i = 0; i < tz; ++i) {
long long f2n = fn * ((fnp1 << 1) - fn),
f2np1 = fn * fn + fnp1 * fnp1;
fn = f2n, fnp1 = f2np1;
}
return fn;
```
### 與作業文件的比較
> 迴圈內 if...else... 的部份可用 `-!!(target & (1 << i))` 作為 mask 的技巧簡化成:
>
> ```c=
> static inline uint64_t fast_doubling_iter(uint64_t target)
> {
> if (target <= 2)
> return !!target;
>
> // find first 1
> uint8_t count = 63 - __builtin_clzll(target);
> uint64_t fib_n0 = 1, fib_n1 = 1;
>
> for (uint64_t i = count, fib_2n0, fib_2n1, mask; i-- > 0;) {
> fib_2n0 = fib_n0 * ((fib_n1 << 1) - fib_n0);
> fib_2n1 = fib_n0 * fib_n0 + fib_n1 * fib_n1;
>
> mask = -!!(target & (1UL << i));
> fib_n0 = (fib_2n0 & ~mask) + (fib_2n1 & mask);
> fib_n1 = (fib_2n0 & mask) + fib_2n1;
> }
> return fib_n0;
> }
> ```
`(target & (1 << i))` 的意思為檢查某位元是否為 `1`,前面加上 `!!` 表取得真或假 (0 ro 1),加負號表取得 `0xFFFFFFFFFFFFFFFF`。
:::spoiler 順帶一提,類似的技巧也出現在無分支取絕對值中。
```c=
int absWithoutBranching(int number) {
// 對 number > 0 來說,mask = 0x00000000 (0)
// 對負數來說, mask = 0xffffffff (-1)
const int mask = number >> 31;
// 二補數的「負數取正」為「減一後位元反轉」
// 對負數來說
// 加上 mask 相當於 -1,相當於「減一」
// 對 -1 = 0xffffffff 取 XOR 相當於「位元反轉」得到負數取正
// 對正數來說
// 加上 mask = 0 等於沒做事
// 對 0 取 XOR 相當於沒做事,保持原數
return (mask + number) ^ mask;
}
```
:::
最後根據此 mask 過濾是否採用 `fib_2n0`, `fib_2n1` 與否,十分精妙。
最後我採納此技巧,將原本的 branching 調整成 branchless,不過還是保留第三階段的 branchless,以達到效能的最大提升。以下展示原 branching 調整的部分。
```diff
while (n) { // exists "1" bit
long long f2n = fn * ((fnp1 << 1) - fn), f2np1 = fn * fn + fnp1 * fnp1;
+ long long is_one = -!!(n & 0x8000000000000000ll);
+ fn = (f2np1 & is_one) + (f2n & ~is_one);
+ fnp1 = f2np1 + (f2n & is_one);
- if (n & 0x8000000000000000ll) {
- fn = f2np1, fnp1 = f2np1 + f2n;
- } else {
- fn = f2n, fnp1 = f2np1;
- }
n <<= 1;
}
```
## Count leading zeros 實作
雖然 `__builtin_clzll` 和 `__builtin_ctzll` 好用且高效,但本方法的使用受限於 gcc 與特定型別 (支援 `unsigned int`, `unsigned long` 和 `unsigned long long`);且參考 [GCC 官方文件](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html),當輸入為 `0` 時屬於 undefined behavior,可見略有不足之處。於是我受 bitwise 操作教學文件的啟發,設計巨集來生成 `clz` 和 `ctz` 函式,支援所有長度在 64 以內的型別。
原本教學文件中的程式碼如下。
```c=
int clz (int x){
if (x == 0) return 32;
int n = 0;
if ((x & 0xFFFF0000) == 0) {n += 16; x <<= 16;}
if ((x & 0xFF000000) == 0) {n += 8; x <<= 8;}
if ((x & 0xF0000000) == 0) {n += 4; x <<= 4;}
if ((x & 0xC0000000) == 0) {n += 2; x <<= 2;}
if ((x & 0x80000000) == 0) n += 1;
return n;
}
```
要改成 `long long` 很簡單,簡單調整一下即可,如下。
```diff
int clz(long long x){
if (x == 0) return 64;
int n = 0;
+ if ((x & 0xFFFFFFFF00000000) == 0) {n += 32; x <<= 32;}
if ((x & 0xFFFF000000000000) == 0) {n += 16; x <<= 16;}
if ((x & 0xFF00000000000000) == 0) {n += 8; x <<= 8;}
if ((x & 0xF000000000000000) == 0) {n += 4; x <<= 4;}
if ((x & 0xC000000000000000) == 0) {n += 2; x <<= 2;}
if ((x & 0x8000000000000000) == 0) n += 1;
return n;
}
```
計算時間一樣是確定的 (deterministic),不過實在有失優雅。主要有三點:
* `== 0` 邏輯可以簡化
* 近乎相同的位移、加法邏輯重複寫好幾遍,即 bad smell
* 針對每個不同的型別都要重寫,明明內容都差不多,這又是 bad smell
第一點即採用 `!(x & __)`。第二點的做法可能根據考量點而有所不同,比如[此文提供一個很有趣的想法](https://stackoverflow.com/questions/45221914/number-of-trailing-zeroes),考量最後一步的效能,他利用 bitwise 操作將分支與加法合併:
```diff
- if ((x & 0x8000000000000000) == 0) n += 1;
+ n += (x & 1) ^ 1;
```
若考慮消去 bad smell 的話,採用迴圈可能是更好的做法。雖然效能上可能會稍微降低,不過程式碼會變的簡潔易讀,且容易擴展。搭配前置處理器技巧,想擴展至更多型別便不是問題。
首先將函式調整成迴圈版。
```c=
int clz(long long x){
if (x == 0)
return 64;
int n = 0;
long long mask = 0xFFFFFFFF00000000;
int shift = 32;
while (shift) {
if (!(x & mask)) {
n += shift;
x <<= shift;
}
shift >>= 1;
mask <<= shift;
}
return n;
}
```
可以發現,透過改成迴圈版,我們將與 `long long` 型別有關的變因獨立在迴圈之前,那是不是調整這些變因就可以適用於其他型別呢?沒錯,並且搭配前置處理器,程式碼的前段變成:
```c=
#define __CZ_MAX_ONES 0xFFFFFFFFFFFFFFFF
#define __clz(type, fn_postfix, bit_len, ones) \
static int clz_##fn_postfix(type x) \
{ \
if (!x) \
return bit_len; \
int bits = 0; \
int shift = bit_len >> 1; \
type mask = (type) (ones << shift); \
/* ... */
/**
* Macro to generate "count leading zeros"
* for type which length is less than int64_t
* Named: clz_"fn_postfix"
*/
#define clz(type, fn_postfix) \
__clz(type, fn_postfix, (sizeof(type) * 8), __CZ_MAX_ONES)
```
如此一來,我們可以輕鬆產生長度在 64 bits 內的所有 `clz`,產生方法如下:
```c=
#include <stdint.h>
clz(long long, long_long); // clz_long_long
ctz(long long, long_long); // clz_long_long
clz(int, int); // clz_int
clz(short, short); // clz_short
clz(char, char); // clz_char
clz(int8_t, int8); // clz_int8
clz(int16_t, int16); // clz_int16
clz(int32_t, int32); // clz_int32
clz(int64_t, int64); // clz_int64
clz(uint8_t, uint8); // clz_uint8
clz(uint16_t, uint16); // clz_uint16
clz(uint32_t, uint32); // clz_uint32
clz(uint64_t, uint64); // clz_uint64
```
上百行的函式就這麼樸實無華的產生了。`ctz` 同理,只要進行如下的調整...
:::info
注意到第一個差異使用 `~` 而非反方向位移是因為右移與型別 (`type`) 的符號有關,對於有號數,全一再怎麼 (算數) 右移都是全一。
:::
```diff
#define __ctz(type, fn_postfix, bit_len, ones) \
static int ctz_##fn_postfix(type x) \
{ \
if (!x) \
return bit_len; \
int bits = 0; \
int shift = bit_len >> 1; \
- type mask = (type) ones << shift; \
+ type mask = (type) ~(ones << shift); \
while (shift) { \
if (!(x & mask)) { \
bits += shift; \
- x <<= shift; \
+ x >>= shift; \
} \
shift >>= 1; \
- mask <<= shift; \
+ mask >>= shift; \
} \
return bits; \
}
```
如此,我們又可以使出四兩撥千斤之術生成 `ctz_XXX` 函式們,一勞永逸 :)
:::info
測試的部分,可利用與 `__builtin_c{l,t}z` 來比較輸出,在此省略細節。不過我也是因此注意到 `__builtin_c{l,t}z` 的 undefined behavior。
:::
## 大數運算的實作
> 恩,我不知道自己多久以後才會完善測試與筆記,還是直接放連結吧。
> 至於 Repo 中怎麼沒什麼東西...不好意思等我一...下 QwQ
> [name=Shiritai]
* 筆記:[手刻一套 C 語言大數運算之物件導向函式庫](https://hackmd.io/@Eroiko/impl-big-num)。
* Reposiroty: [BigNum](https://github.com/Shiritai/BigNum)
Repo 的部分...
![](https://i.imgur.com/dFCvJPR.jpg)
## 導讀紀錄
循著作業說明操作的紀錄。
### 環境準備
```bash
ls -l /dev/fibonacci
crw------- 1 root root 510, 0 三 8 23:51 /dev/fibonacci
```
可以看見 `510, 0` 兩數,分別為 device major/minor number。
:::info
看完作業說明影片後發現自己讀+實作/實驗太少了,先回去讀...
:::