contributed by < paulpeng-popo
>
Github: paulpeng-popo
$ 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
Byte Order: Little Endian
Address sizes: 39 bits physical, 48 bits virtual
CPU(s): 12
On-line CPU(s) list: 0-11
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 158
Model name: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
Stepping: 10
CPU max MHz: 4600.0000
CPU min MHz: 800.0000
BogoMIPS: 6399.96
Virtualization: VT-x
L1d: 192 KiB (6 instances)
L1i: 192 KiB (6 instances)
L2: 1.5 MiB (6 instances)
L3: 12 MiB (1 instance)
NUMA node0 CPU(s): 0-11
假設成年兔子為 a
,幼年兔子為 b
,且兔子不死亡
一般遞迴
矩陣運算 Q-matrix
又可改寫成
Fast Doubling
其中
最後得到
作業說明中的虛擬碼
Fast_Fib(n)
a = 0; b = 1; // m = 0
for i = (number of binary digit in n) to 1
t1 = a*(2*b - a);
t2 = b^2 + a^2;
a = t1; b = t2; // m *= 2
if (current binary digit == 1)
t1 = a + b; // m++
a = b; b = t1;
return a;
這裡 Fast Doubling 利用 比
參考:Calculating Fibonacci Numbers by Fast Doubling
/* FILE: fibdrv.c */
static long long fib_fastd(long long n)
{
if (n < 2)
return n;
long long a = 0;
long long b = 1;
for (int j = 63 - 1; j >= 0; j--) {
long long c = a * ((b << 1) - a);
long long d = a * a + b * b;
a = c;
b = d;
if ((n >> j) & 1LL) {
a = d;
b = c + d;
}
}
return a;
}
n 小於 2,直接回傳,不會經過迴圈
從 n 的 MSB 開始,依序計算該數的費氏數,直到 n 的 LSB 或剩下之 bit 皆為 0,則提前跳出迴圈
先用 h
記錄實際的數字共有幾個 bit (不含 leading 0s),接著從 MSB 開始計算首項費氏數,再藉由迴圈依次增加 n >> j
的 bit 數,以計算後續的費氏數
複雜度分析
解釋 fast doubling 為什麼用 2k 跟 2k + 1
例如:長度為 n 個 bit 的數字
剛好就是 binary number 的進位規則
使用 __builtin_clzll
function 做硬體上的計算加速
clz
(Count Leading Zero) 可以計算數字 n
前面有幾個 0,加速取得 h
的速度
- for (int j = 63 - 1; j >= 0; j--) {
+ long long mask = 1LL << (63 - __builtin_clzll(n));
+ for (; mask; mask >>= 1) {
- if ((n >> j) & 1LL) {
+ if (mask & n) {
Linux kernel 不支援 VLA,因此使用 Dynamic Programming 與空間重複利用的技巧,避免動態陣列的使用,同時將 space complexity 從
Linux 核心限制使用 VLA 的因素
在 The Linux Kernel is now VLA-Free 有提到 Linux kernel 不再允許 VLA 的使用C99 具有 VLA (Variable-Length Array) 的特性,允許執行時期再決定陣列佔用的空間大小,但可能會造成 stack overflow 等安全疑慮
根據教材 你所不知道的 C 語言:函式呼叫篇,攻擊者可以利用 stack-based-buffer-overflow 來執行惡意程式碼,從而對系統造成威脅此外,Linus Torvalds 也說過:『USING VLA'S IS ACTIVELY STUPID! It generates much more code, and much slower code (and more fragile code), than just using a fixed key size would have done.』
/* FILE: fibdrv.c */
static long long fib_sequence(long long k)
{
long long f[2] = {0, 1};
if (k < 2)
return f[k];
for (int i = 2; i <= k; i++) {
f[i & 1] = f[0] + f[1];
}
return f[k & 1];
}
在 Linux kernel 中具有兩種計時方式
jiffies 是 Linux kernel 較早期存在的計時方法,使用作業系統從開機以來的計時器中斷發生的次數作為計時的依據,此全域變數稱作 ticks
數。計時器中斷為週期性的中斷事件,以固定的時間間隔觸發,利用間隔固定的特性便能達到測量時間的目的。
通常情況下,jiffies 的精度為毫秒級別,具體的精度取決於 tick rate 的大小。例如 1000Hz 表示每秒觸發 1000 次的 timer interrupt,因此 jiffies 的增加速度為每毫秒一次。
jiffies 轉換為秒的公式為
(unsigned long long)(jiffies - INITIAL_JIFFIES) / HZ;
由以上程式碼可知, 假如系統原先設定的 tick rate (HZ)
不大於
hrtimer 是在 2.6.16 開始有的新的計時機制,裡面使用了 ktime_t
這個資料結構來進行計時。書中提到,原本基於 jiffies
的計時機制 timer wheel
在許多層面上有許多缺失,並幾乎無法再優化/整合了,包括:
- The forced handling of low-resolution and high-resolution timers in the same way leads to a lot of compromises, macro magic and #ifdef mess.
- The unpredictable [O(N)] overhead of cascading leads to delays which necessitate a more complex handling of high resolution timers, which in turn decreases robustness.
- The timer wheel data structure is too rigid for high-res timers.
因此實作 hrtimer
的設計考量包括
ktime_t 結構定義如下
union ktime {
s64 tv64;
};
typedef union ktime ktime_t;
其結構有一型態為 64 位元的有號整數 tv64,單位為奈秒 (ns)
透過調用 Linux kernel API ktime_get()
,可以取得相對於系統啟動時間的偏移量,因此藉由計算出兩次 ktime_get()
的時間差異,便可知其之間的程式碼執行時間花費
/* FILE: fibdrv.c */
static ktime_t kt;
static long long fib_time_proxy(long long k)
{
kt = ktime_get();
long long result = fib_sequence(k);
kt = ktime_sub(ktime_get(), kt);
return result;
}
static ssize_t fib_read(struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
return (ssize_t) fib_time_proxy(*offset);
}
static ssize_t fib_write(struct file *file,
const char *buf,
size_t size,
loff_t *offset)
{
return ktime_to_ns(kt);
}
在 read 的時候,計算 fib,並同時將所花時間儲存在 kt
中
接著 write 的時候,將 kt
的值轉換成 nanoseconds 再回傳給 client
client
中順序調用 read
和 write
,即可取得所花費的 kernel time
/* FILE: client.c */
for (int i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
sz = read(fd, buf, 1);
kt = write(fd, write_buf, 1);
}
執行 sudo ./client
後,成功輸出計算費氏數時間,輸出如下
Reading from /dev/fibonacci at offset 0, returned the sequence 0.
Time: 331 (ns).
Reading from /dev/fibonacci at offset 1, returned the sequence 1.
Time: 197 (ns).
Reading from /dev/fibonacci at offset 2, returned the sequence 1.
Time: 183 (ns).
Reading from /dev/fibonacci at offset 3, returned the sequence 2.
Time: 113 (ns).
Reading from /dev/fibonacci at offset 4, returned the sequence 3.
Time: 123 (ns).
Reading from /dev/fibonacci at offset 5, returned the sequence 5.
Time: 120 (ns).
Reading from /dev/fibonacci at offset 6, returned the sequence 8.
Time: 81 (ns).
Reading from /dev/fibonacci at offset 7, returned the sequence 13.
Time: 78 (ns).
Reading from /dev/fibonacci at offset 8, returned the sequence 21.
...
參考 yanjiew1 的做法,利用
read
傳入size
作為使用不同演算法的條件
/* FILE: fibdrv.c */
switch (size) {
case 1:
result = fib_sequence(k);
break;
case 2:
result = fib_fastd(k);
break;
case 3:
result = fib_fastd_clz(k);
break;
default:
return -EINVAL;
}
使用 gnuplot 製圖,確認 fast doubling 確實加速了費氏數的計算,同時發現 __builtin_clzll 大幅降低了時間開銷
NOTE: 此處時間為 kernel mode 中計算的時間,還未加入 user space 的時間開銷比較
使用 taskset
預留 cpu 給效能測試使用
$ sudo vim /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=5"
$ sudo update-grub
$ sudo init 6
將作業說明提到的方法寫成一個 script
#!/bin/sh
CPUID=5
MASK=$((1 << $CPUID))
MASK=$((0xfff & ~$MASK))
MASK=`printf "%x" $MASK`
ORIG_ASLR=`cat /proc/sys/kernel/randomize_va_space`
ORIG_GOV=`cat /sys/devices/system/cpu/cpu$CPUID/cpufreq/scaling_governor`
ORIG_TURBO=`cat /sys/devices/system/cpu/intel_pstate/no_turbo`
ORIG_IRQ=`cat /proc/irq/$CPUID/smp_affinity`
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo"
sudo sh -c "echo performance > /sys/devices/system/cpu/cpu$CPUID/cpufreq/scaling_governor"
for file in /proc/irq/*/smp_affinity; do
var=0x$(cat "$file")
var=$((var & 0xfdf))
var=`printf "%x" $var`
sudo sh -c "echo $var > $file" 2> /dev/null
done
sudo sh -c "echo $MASK > /proc/irq/$CPUID/smp_affinity"
# Load the module and run the client
make unload
make load
python3 scripts/statisic_plot.py -cpu=$CPUID
gnuplot -e "filename='scripts/data.txt'" scripts/draw.gp
make unload
# Restore original settings
sudo sh -c "echo $ORIG_ASLR > /proc/sys/kernel/randomize_va_space"
sudo sh -c "echo $ORIG_GOV > /sys/devices/system/cpu/cpu$CPUID/cpufreq/scaling_governor"
sudo sh -c "echo $ORIG_TURBO > /sys/devices/system/cpu/intel_pstate/no_turbo"
sudo sh -c "echo $ORIG_IRQ > /proc/irq/$CPUID/smp_affinity"
最後使用 taskset -c 5 ./client
指定 client 使用 cpu 5 進行費氏數計算
參考 時間測量與效能分析
計算 50 次費氏數的平均,並將離散值做處理,用以得到較平滑的效能比較圖
程式碼 4b6a40c