---
# System prepended metadata

title: fibdrv

---

# Fibdrv
contributed by < [`ericlinqq`](https://github.com/ericlinqq) >
## 實驗環境
```
$ fastfetch
                   ##  **                     eric@eric-macbookair
                *####****.                    --------------------
                  ###,                        OS: Fedora Linux Asahi Remix 42 (Forty Two [Adams]) aarch64
               ...,/#,,,..                    Host: Apple MacBook Air (13-inch, M2, 2022)
          /*,,,,,,,,*,........,,              Kernel: Linux 6.16.4-402.asahi.fc42.aarch64+16k
        ,((((((//*,,,,,,,,,......             Shell: zsh 5.9
       ((((((((((((((%............            Display (eDP-1): 2560x1600 @ 60 Hz (as 1598x999) in 13" [Built-in]
     ,(((((((((((((((@@(............          DE: KDE Plasma 6.4.4
    (((((((((((((((((@@@@/............        WM: KWin (Wayland)
  ,((((((((((((((((((@@@@@&*...........       Terminal: ghostty 1.2.0-2.fc42
 ((((((((((((((((((((@@@@@@@&,...........     CPU: Apple M2 (8) @ 3.20 GHz
(((((((((((((((((((((@@@&%&@@@%,..........    GPU: Apple M2 (10) @ 1.40 GHz [Integrated]
 /(((((((((((((((((((@@@&%%&@@@@(........     Memory: 9.58 GiB / 15.27 GiB (63%)
    ,((((((((((((((((@@@&&@@&/&@@@/..
        /((((((((((((@@@@@@/.../&&
           .(((((((((@@@@(....
               /(((((@@#...
                  .((&,
```
```
$ lscpu
Architecture:                aarch64
  CPU op-mode(s):            64-bit
  Byte Order:                Little Endian
CPU(s):                      8
  On-line CPU(s) list:       0-7
Vendor ID:                   Apple
  Model name:                Blizzard-M2
    Model:                   0
    Thread(s) per core:      1
    Core(s) per socket:      4
    Socket(s):               1
    Stepping:                0x1
    Frequency boost:         disabled
    CPU(s) scaling MHz:      72%
    CPU max MHz:             2424.0000
    CPU min MHz:             600.0000
    BogoMIPS:                48.00
    Flags:                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 asimddp sha512 asimdf
                             hm dit uscat ilrcpc flagm ssbs sb paca pacg dcpodp flagm2 frint i8mm bf16 bti ecv
  Model name:                Avalanche-M2
    Model:                   0
    Thread(s) per core:      1
    Core(s) per socket:      4
    Socket(s):               1
    Stepping:                0x1
    CPU(s) scaling MHz:      100%
    CPU max MHz:             3204.0000
    CPU min MHz:             660.0000
    BogoMIPS:                48.00
    Flags:                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 asimddp sha512 asimdf
                             hm dit uscat ilrcpc flagm ssbs sb paca pacg dcpodp flagm2 frint i8mm bf16 bti ecv
Caches (sum of all):         
  L1d:                       768 KiB (8 instances)
  L1i:                       1.3 MiB (8 instances)
  L2:                        20 MiB (2 instances)
NUMA:                        
  NUMA node(s):              1
  NUMA node0 CPU(s):         0-7

```

```bash
$ gcc --version
gcc (GCC) 15.2.1 20250808 (Red Hat 15.2.1-1)
```

安裝 `kernel-16k-devel` 套件 (對應到作業說明中的 `linux-headers` 套件)
```bash
$ sudo dnf install kernel-16k-devel
```


## 排除干擾效能分析的因素
### 查詢 CPU 資訊
Apple M2 包含 Avalanche (Performance Core) 以及 Blizzard (Efficiency Core) 兩種 Core，這邊選擇使用 P Core 來作測試
藉由以下指令可以看到 CPU 0-3 為 E Core，而 CPU 4 - 7 為 P Core
```
$ lscpu -e
CPU NODE SOCKET CORE L1d:L1i:L2 ONLINE    MAXMHZ   MINMHZ       MHZ
  0    0      0    0 0:0:0         yes 2424.0000 600.0000 1752.0000
  1    0      0    1 1:1:0         yes 2424.0000 600.0000 1752.0000
  2    0      0    2 2:2:0         yes 2424.0000 600.0000 1752.0000
  3    0      0    3 3:3:0         yes 2424.0000 600.0000 1752.0000
  4    0      0    0 4:4:1         yes 3204.0000 660.0000  660.0000
  5    0      0    1 5:5:1         yes 3204.0000 660.0000  660.0000
  6    0      0    2 6:6:1         yes 3204.0000 660.0000  660.0000
  7    0      0    3 7:7:1         yes 3204.0000 660.0000  660.0000
```
### 相關 kernel 開機參數
參考 [Ubuntu documentation: How to configures CPUs for real-time processing](https://documentation.ubuntu.com/real-time/latest/how-to/cpu-boot-configs)
以下操作皆需要在 `/etc/default/grub` 中的`GRUB_CMDLINE_LINUX_DEFAULT` 新增 kernel 開機參數，並利用 
```
$ sudo grub2-mkconfig -o /etc/grub2.cfg
``` 
來更新 GRUB 的設定，重新開機後即生效
#### 保留特定 CPU 避免被排程
加入`isolcpus=7`

可以用以下指令來確認是否成功
```
$ cat /sys/devices/system/cpu/isolated
7
```
```
$ taskset -cp 1
pid 1's current affinity list: 0-6
```

#### 避免特定 CPU 被指派處理 IRQ
加入 `irqaffinity=7`
可以用以下指令來確認
```
$ find /proc/irq/ -name smp_affinity_list -print -exec cat '{}' ';'
/proc/irq/1/smp_affinity_list
0-6
/proc/irq/2/smp_affinity_list
0-6
/proc/irq/3/smp_affinity_list
0-6
/proc/irq/4/smp_affinity_list
0-6
/proc/irq/5/smp_affinity_list
0-6
/proc/irq/6/smp_affinity_list
0-6
...
```

#### 避免特定 CPU 被排程器時鐘中斷
加入 `nohz=on nohz_full=7`
可以用以下指令確認
```
$ cat /sys/devices/system/cpu/nohz_full
7
```

#### 避免特定 CPU 處理 RCU callback
加入 `rcu_nocbs=7`
可以用以下指令確認
```
$ sudo dmesg | grep rcu
[    0.000000] rcu:     Offload RCU callbacks from CPUs: 7.
```


### 以特定 CPU 執行程式
```
$ taskset -c 7 ./client
```

### 抑制 ASLR
```
$ sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space"
```

### 設定 scaling_governor 為 performance
```
$ sudo bash -c "echo performance > /sys/devices/system/cpu/cpu7/cpufreq/scaling_governor"
```
 ### 設定 scaling_min_freq
```
$ MAX_FREQ=`cat /sys/devices/system/cpu/cpu7/cpufreq/scaling_max_freq`
$ sudo bash -c "echo $MAX_FREQ > /sys/devices/system/cpu/cpu7/cpufreq/scaling_min_freq"
```

## 時間量測方式
### 利用 write operation 測試執行時間
使用 `ktime` 系列 API 來量測執行時間，並利用 `fib_time_proxy()` 函式作為 wrapper function，用以進行量測，並返回計算結果
```c
static ssize_t fib_write(struct file *file,
                         const char *buf,
                         size_t size,
                         loff_t *offset)
{
    int k = *offset;
    switch (size) {
    case 0:
        fib_time_proxy(fib_sequence, k);
        break;
    case 1:
        fib_time_proxy(fib_sequence_fdoubling, k);
        break;
    case 2:
        fib_time_proxy(fib_sequence_fdoubling_clz, k);
        break;
    default:
        return 1;
    }

    return (ssize_t) ktime_to_ns(kt);
}
```

`write(fd, buf, 0)` 為測試 naive 
`write(fd, buf, 1)` 為測試 fast doubling
`write(fd, buf, 2)` 為測試 fast doubling w/ clz


```c
static ktime_t kt;
typedef long long (*fib_ft)(long long);

static long long fib_time_proxy(fib_ft fib, long long k) {
    kt = ktime_get();
    long long res = fib(k);
    kt = ktime_sub(ktime_get(), kt);
    return res;
}
```


### 執行時間
直接輸出執行一次的時間
![runtime_ns](https://hackmd.io/_uploads/BkScygYjeg.png)
可以觀察到上圖中有兩個問題：
1. Naive 方法的執行時間在某幾個 Fibonacci number 會比其他高出很多，在圖上呈現出細尖的形狀
2. Fast doubling 的方法似乎只有兩種執行時間，不是 0 就是 40 左右

以下將針對這兩點分別進行討論

### Naive 方法部份 F(n) 執行時間過長
從上圖可以觀察到當 `n = 0, 3, 7, 11, 15, 23, 31, 63` 時會有相比其他數相差非常巨大的執行時間
#### 查看 Naive 方法的實作方式：
```c
static long long fib_sequence(long long k) {
    long long *f = kmalloc(sizeof(*f) * (k + 2), GFP_KERNEL);
    ...
    kfree(f);
}
```
發現會使用 `kmalloc` 來動態配置記憶體，並且會依照傳入的 `k` 數值配置一個 `sizeof(long long) * (k + 2)` 空間，依照環境 LP64 data model，即為 `8 * (k + 2)` Bytes，我們將上述提到的數字代入，


|      k       |  0  |  3  |  7  | 11  | 15  | 23  | 31  | 63  |
|:------------:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| Bytes needed | 16  | 40  | 72  | 104 | 136 | 200 | 264 | 520 |

#### 查看 `kmalloc` 會使用到的通用 slab cache
```
$ sudo cat /proc/slabinfo
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
...
kmalloc-32k            0      0  32768    4    8 : tunables    0    0    0 : slabdata      0      0      0
kmalloc-16k            0      0  16384    8    8 : tunables    0    0    0 : slabdata      0      0      0
kmalloc-8k            96     96   8192   16    8 : tunables    0    0    0 : slabdata      6      6      0
kmalloc-4k           224    288   4096   32    8 : tunables    0    0    0 : slabdata      9      9      0
kmalloc-2k           256    256   2048   32    4 : tunables    0    0    0 : slabdata      8      8      0
kmalloc-1k           288    288   1024   32    2 : tunables    0    0    0 : slabdata      9      9      0
kmalloc-512          384    384    512   32    1 : tunables    0    0    0 : slabdata     12     12      0
kmalloc-256          448    448    256   64    1 : tunables    0    0    0 : slabdata      7      7      0
kmalloc-128          896    896    128  128    1 : tunables    0    0    0 : slabdata      7      7      0
kmalloc-64          2048   2048     64  256    1 : tunables    0    0    0 : slabdata      8      8      0
kmalloc-32          3584   3584     32  512    1 : tunables    0    0    0 : slabdata      7      7      0
kmalloc-16          7168   7168     16 1024    1 : tunables    0    0    0 : slabdata      7      7      0
kmalloc-8          14336  14336      8 2048    1 : tunables    0    0    0 : slabdata      7      7      0
kmalloc-192          680    680    192   85    1 : tunables    0    0    0 : slabdata      8      8      0
kmalloc-96          1190   1190     96  170    1 : tunables    0    0    0 : slabdata      7      7      0
```

可以看到配置的記憶體都是比所要求空間還要來得大的 slab cache
|        k        |  0  |  3  |  7  | 11  | 15  | 23  | 31  |  63  |
|:---------------:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:----:|
|  Bytes　needed  | 16  | 40  | 72  | 104 | 136 | 200 | 264 | 520  |
| Bytes allocated | 16  | 64  | 96  | 128 | 192 | 256 | 512 | 1024 |

接著再觀察 `k - 1` 時所配置到的記憶體大小，剛好就是每個 slab cache 的大小
|       k - 1       |  2  |  6  | 10  | 14  | 22  | 30  | 62  |
|:---------------:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|  Bytes　needed  | 32  | 64  | 96  | 128 | 192 | 256 | 512 |
| Bytes allocated | 32  | 64  | 96  | 128 | 192 | 256 | 512 |

這些多餘的時間主要花費在要求配置一個新的 slab cache，而無法復用先前剛 `kfree` 掉的空間。
後續會使用統計方法來重複取樣，以消除掉這些異常值，另外也會嘗試改寫避免動態配置記憶體。


### Fast doubling 方法執行時間為 0
觀察上圖，執行時間只有 0 或 40 幾，因此懷疑或許是執行時間小於時鐘精度，造成前後 Cycle count 相等，因此就看到執行時間為 0 的情況
#### 查看 Clock Source
```
$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
arch_sys_counter
```
```
$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
arch_sys_counter
```
此為 ARMv8-A 架構的 [System counter](https://developer.arm.com/documentation/102379/0100/System-Counter?lang=en)，我們可以通過讀取[CNTFRQ_EL0](https://developer.arm.com/documentation/ddi0595/2020-12/AArch64-Registers/CNTFRQ-EL0--Counter-timer-Frequency-register?lang=en) 暫存器來得到它的頻率，讀取 [CNTPCT_EL0](https://developer.arm.com/documentation/ddi0601/2025-06/AArch64-Registers/CNTPCT-EL0--Counter-timer-Physical-Count-Register) 得到目前計數值
:::warning
若在 Userspace (EL0) 讀取上述兩個暫存器可能會發生 Illegal Instrucion，須手動設置 [CNTKCTL_EL1](https://developer.arm.com/documentation/ddi0601/2020-12/AArch64-Registers/CNTKCTL-EL1--Counter-timer-Kernel-Control-register) 暫存器之`EL0PCTEN, BIT[0]` 為 1 後才能夠成功讀取。
:::
#### 利用 System counter 查看執行 cycle 數
撰寫以下函式來讀取 `CNTPCT_EL0` 暫存器
```c
static unsigned long read_cntpct(void)
{
    unsigned long cnt;
    asm volatile ("mrs %0, CNTPCT_EL0": "=r" (cnt)::);
    return cnt;
}      
```
改寫 `fib_time_proxy()` 利用執行前後數值相差來計算執行所需 Cycle 數
```c
    kt = read_cntpct();
    long long res = fib(k);
    kt = read_cntpct() - kt;
```
![runtime_cycle](https://hackmd.io/_uploads/r1zpSBqilx.png)
果然可以看到 Fast doubling 的 Cycle 數只有 0 和 1 兩種數值

#### 確認 System conter 的頻率
從 kernel log 中確認
```
$ sudo dmesg | less
[    0.000000] arch_timer: cp15 timer(s) running at 24.00MHz (phys).
[    0.000000] clocksource: arch_sys_counter: mask: 0xffffffffffffff max_cycles: 0x588fe9dc0, max_idle_ns: 440795202592 ns
[    0.000000] sched_clock: 56 bits at 24MHz, resolution 41ns, wraps every 4398046511097ns
```
撰寫以下函式來讀取 `CNTFRQ_EL0` 暫存器
```c
static unsigned long read_cntfreq(void)
{
    unsigned long freq;
    asm volatile ("mrs %0, CNTFRQ_EL0": "=r" (freq)::);
    return freq;
}      
```
利用 `printk` 輸出資訊
```c
printk(KERN_INFO "CNTFRQ_EL0: %lu\n", read_cntfreq());
```
查看 kernel log
```
$ sudo dmesg | less
[ 9007.220132] CNTFRQ_EL0: 24000000
```
System counter 頻率為 24MHz，也就是一個 Cycle 約為 41 ns，因此前面我們只能看到 0 和 42 ns 兩種執行時間

### 改為使用 PMU 進行分析
我們可改用 Performance Monitoring Unit (PMU)，ARM 架構下提供暫存器 Performance Monitor Cycle Counter (PMCC) 用於紀錄處理器 Cycle 的數量，根據 ARMv8-A Manual [PMCCNTR_EL0](https://developer.arm.com/documentation/ddi0595/2021-03/AArch64-Registers/PMCCNTR-EL0--Performance-Monitors-Cycle-Count-Register) 暫存器可以用來讀取 Cycle 的計數值，不過當中也有提醒：
> This register is present only when FEAT_PMUv3 is implemented. Otherwise, direct accesses to PMCCNTR_EL0 are UNDEFINED.
```
$ sudo dmesg | grep PMU
[    0.000000] CPU features: detected: Apple IMPDEF PMUv3 Traps
[    0.668723] hw perfevents: enabled with apple_blizzard_pmu PMU driver, 10 (0,000003ff) counters available
[    0.668817] hw perfevents: enabled with apple_avalanche_pmu PMU driver, 10 (0,000003ff) counters available
```
```
$ ls /sys/bus/event_source/devices | grep pmu
apple_avalanche_pmu
apple_blizzard_pmu
```

```
$ cat /sys/bus/event_source/devices/apple_avalanche_pmu/cpus
4-7
```
可惜的是，Apple M2 上並沒有使用 armv8-pmuv3，而是使用自家專有的 PMU，因此直接讀取 `PMCCNTR_EL0` 暫存器會發生 Illegal Instruction Exception

不過我們還是可以利用 `perf_events` 來進行效能量測

### 利用 `perf_events` 在 kernel module 內進行效能量測
#### 利用 open operation 建立 `perf_events`
在 open 時，建立 perf_event 監測 CPU cycle count
利用 [work_on_cpu()](https://github.com/torvalds/linux/blob/cec1e6e5d1ab33403b809f79cd20d6aff124ccfe/include/linux/workqueue.h#L843) 在目標 CPU 上利用函式
[perf_event_create_kerenl_count()](https://github.com/torvalds/linux/blob/07e27ad16399afcd693be20211b0dfae63e0615f/kernel/events/core.c#L13771C1-L13771C34)　建立 `perf_event`，參數設定為`cpu` 設為目標 CPU，`task`　設為目前的 task (task mode)，並將 `fib_ctx` 存入 `file->private_data`　中，方便在對 `/dev/fibonacci` 操作時得到相關執行參數與結果

```c
struct fib_ctx {
    struct perf_event *pe;
    int cpu;
    fib_ft func;
    long long k;
    u64 cycle;
    long long result;
};

static int fib_open(struct inode *inode, struct file *file) {
    ...
    struct fib_ctx *fc = kzalloc(sizeof(*fc), GFP_KERNEL);
    if (!fc)
        return -ENOMEM;

    fc->cpu = smp_processor_id();

    if (!cpu_online(fc->cpu)) {
        kfree(fc);
        return -EINVAL;
    }

    long ret = work_on_cpu(fc->cpu, fib_create_pe_oncpu, fc);

    if (ret) {
        kfree(fc);
        return ret;
    }

    file->private_data = fc;
    return 0;
}
```
如前面所述，`fib_create_pe_oncpu()`　被放入指定 CPU 執行，並在內部呼叫 [perf_event_create_kernel_counter](https://github.com/torvalds/linux/blob/07e27ad16399afcd693be20211b0dfae63e0615f/kernel/events/core.c#L13771C1-L13771C34) 建立　`perf_event`
```c
static long fib_create_pe_oncpu(void *data)
{
    struct fib_ctx *fc = data;

    fc->pe =
        perf_event_create_kernel_counter(&attr, fc->cpu, current, NULL, NULL);
    if (IS_ERR(fc->pe))
        return PTR_ERR(fc->pe);
    return 0;
}
```
設定 `perf_event_attr` 為監控 CPU Cycle，並且獨占 PMU
```c
struct perf_event_attr attr = {.type = PERF_TYPE_HARDWARE,
                               .size = sizeof(attr),
                               .config = PERF_COUNT_HW_CPU_CYCLES,
                               .disabled = 0,
                               .exclude_hv = 1,
                               .pinned = 1,
                               .exclusive = 1,
                               .exclude_kernel = 0,
                               .read_format = 0};
```


#### 利用 write operation 讀取執行待測函式前後的 cycle count
修改先前的 `fib_time_proxy()`，讓其在目標 CPU 上執行測試函式
```c
static long long fib_time_proxy(struct fib_ctx *fc)
{
    work_on_cpu(fc->cpu, fib_cycle_delta, fc);
    return fc->result;
}
```
透過 [perf_event_read_value()](https://github.com/torvalds/linux/blob/07e27ad16399afcd693be20211b0dfae63e0615f/kernel/events/core.c#L5909) 讀取 `perf_event`　所監測之　Cycle count;
```c
static long fib_cycle_delta(void *data)
{
    struct fib_ctx *fc = data;
    u64 v0 = 0, en0 = 0, run0 = 0;
    u64 v1 = 0, en1 = 0, run1 = 0;
    v0 = perf_event_read_value(fc->pe, &en0, &run0);

    fc->result = fc->func(fc->k);

    v1 = perf_event_read_value(fc->pe, &en1, &run1);
    
    fc->cycle = v1 - v0;
    return 0;
}
```
![runtime_perf](https://hackmd.io/_uploads/BJi_SBjigg.png)
終於我們得以看到更細粒度的執行時間，並且不同方法執行時間對比更加明顯

### 改進 Naive 方法
因為使用 `kmalloc` 造成執行時間較長且不穩定，考慮這邊我們並沒有紀錄並重複使用 fibonacci number，因此可以直接用變數暫存結果即可，這樣在與 `fast doubling` 方法比較時也會比較公平
```c
static long long fib_sequence2(long long k)
{
    long long a = 0;  // f(0)
    long long b = 1;  // f(1)
    for (int i = 0; i < k / 2; i++) {
        a += b;  // f(i)
        b += a;  // f(i + 1)
    }
    return (k & 1) ? b : a;
}
```
![improve_naive](https://hackmd.io/_uploads/r1y5RnIhle.png)

### 以統計方法消除極端值
對每個 Fibonacci number 的計算皆重複執行 1000 次，並根據 95% 信賴區間將兩個標準差以外之樣本排除，取剩餘樣本之平均作為最終執行時間。
可以將原先 naive 方法的 `kmalloc` 所造成的極端值去除。
![runtime_stat](https://hackmd.io/_uploads/r1AiyTL3el.png)

### User Space 和 Kernel Space 傳遞時間
以原先的 Naive 方法進行測試，同樣使用上節之統計方法，Kernel Module 中使用 `ktime` ， User Space 使用 `clock_gettime`
![syscall_plot](https://hackmd.io/_uploads/ryM6u3Jhge.png)
可以看到 kernel time 和 user time 皆隨著輸入線性增加，system call overhead 則是保持固定