--- tags: linux2023 --- # 2023q1 Homework3 (fibdrv) contributed by < `YSRossi` > ## 開發環境 ```shell $ 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: 48 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 8 On-line CPU(s) list: 0-7 Vendor ID: AuthenticAMD Model name: AMD FX-8320E Eight-Core Processor CPU family: 21 Model: 2 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 0 Frequency boost: enabled CPU max MHz: 3200.0000 CPU min MHz: 1400.0000 BogoMIPS: 6429.64 ``` ## 開發紀錄 ### 改成 Fast Doubling 版本(成功計算到 F~92~) 參考作業說明所提供 Fast Doubling 的虛擬碼,搭配 bitwise 實作。 - `n` 的型態為 `long long`,使用 `sizeof(long long) << 3` 計算 `long long` 所佔有的位元數,減去 `n` 的 `leading 0-bits` 數量,得到 `n` 實際上有用到的位元數。其中 `n` 的 `leading 0-bits` 數量,使用 `__builtin_clzll(n)` 獲得。 - 在迴圈中,目前的位元以 `n & (1 << (i - 1))` 表示,將 1 左移 `i - 1` 把對應目前位元的位置設成 1 ,與 `n` 做 `and` 判斷目前位元是否為 1 。 ``` 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 版本與原本方法在 kernel space 所花費的時間(還未排除干擾效能分析的因素) ![](https://i.imgur.com/q3k5Dkd.png) :::warning TODO : 嘗試解決在 GRUB 的設定檔中加入 ```isolcpus=7``` 重新開機後仍無法起作用的問題。 ``` GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=7" ``` > 已解決,使用 ```sudo update-grub``` 更新 GRUB 設定檔 ::: - 實驗比較 Fast Doubling 與原始方法在 kernel space 所花費的時間(已排除干擾效能分析的因素) ![](https://i.imgur.com/KrXov9P.png) ```c= static long long fib_sequence_fast_doubling(long long k) { long long a = 0, b = 1; for (int i = 63; i >= 0; i--) { long long t1 = a * ((2 * b) - a); long long t2 = b * b + a * a; a = t1; b = t2; if ((k >> i) & 1) { t1 = a + b; a = b; b = t1; } } return a; } ``` - 若將上方程式碼的第 5 行改成下方程式碼,計算 fib(0) 在 user space 會花費大量時間。 ```c for (int i = 63 - __builtin_clzll(k); i >= 0; i--) { ``` `write` 回傳 Fast Doubling 版本的 kernel space time `read` 回傳 Fast Doubling 版本計算的 Fibonacci 值 ```c /* client.c */ for (int i = 0; i <= offset; i++) { struct timespec start, end; lseek(fd, i, SEEK_SET); clock_gettime(CLOCK_MONOTONIC, &start); sz = read(fd, buf, 1); clock_gettime(CLOCK_MONOTONIC, &end); printf("Reading from " FIB_DEV " at offset %d, returned the sequence ", i); printf("%lld.\n", sz); kernel_time = write(fd, write_buf, 0); user_time = (long long)(end.tv_sec * 1e9 + end.tv_nsec) - (start.tv_sec * 1e9 + start.tv_nsec); } ``` ![](https://i.imgur.com/3ay7ITQ.png) - 在計算 fib(0) 會有異常的原因是 [__builtin_clzll](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html#Other-Builtins) 的參數不可以為0,因此在計算 fib(0) 時,直接回傳 0 即可解決問題。 > Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined. ![](https://i.imgur.com/SRHPxGy.png) ### 排除干擾效能分析的因素 * 限定 CPU 給特定的程式使用 使用 ```isolcpus``` Linux 核心起始參數,讓特定的 CPU 核在開機時就被保留下來,設定完成後使用 ```sudo update-grub``` 更新 grub 設定檔 ```shell GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=7" ``` * 設定 [NO_HZ](https://www.kernel.org/doc/Documentation/timers/NO_HZ.txt) 減少影響程式執行的中斷,設定完成後使用 ```sudo update-grub``` 更新 grub 設定檔 ```shell GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=7 nohz_full=7" ``` * 抑制 [address space layout randomization](https://en.wikipedia.org/wiki/Address_space_layout_randomization) (ASLR) ```shell $ sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space" ``` * 設定 scaling_governor 為 performance。準備以下 shell script,檔名為 `performance.sh`: ```shell for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor do echo performance > ${i} done ``` 之後再用 `$ sudo sh performance.sh` 執行 * 關閉 [Processor boosting](https://www.kernel.org/doc/Documentation/cpu-freq/boost.txt) ```shell sudo bash -c "echo 0 > /sys/devices/system/cpu/cpufreq/boost" ``` * 設定 [SMP IRQ Affinity](https://www.kernel.org/doc/html/next/core-api/irq/irq-affinity.html) ,減少目標 cpu 處理 interrupt request ,使用 cpu7 來做此實驗,所以 mask 為 0x7f ```shell sudo bash -c "echo 7f > /proc/irq/default_smp_affinity" ``` 設定完成後,雖然還是有些浮動,但可以測量到相對穩定的實驗結果 ![](https://i.imgur.com/fbYb6GR.png) ### 支援大數運算(成功計算到 F~186~) #### 大數資料結構 `uppper` 紀錄大數較高的 `64` 位元,`lower` 紀錄大數較低的 `64` 位元 ```c struct BigN { unsigned long long upper, lower; }; ``` 依照前面實作, Fast-doubling 需要加法、減法、乘法、左移一位運算,其中大數乘法運算還會使用右移一位運算。 ##### `bn_add` 加法 兩數 `upper` 與 `lower` 分別相加,判斷 `lower` 相加後是否溢位,溢位的話將 `upper` 加一。 ```c struct BigN bn_add(struct BigN a, struct BigN b) { struct BigN result; result.lower = a.lower + b.lower; result.upper = a.upper + b.upper; if (a.lower > ~b.lower) result.upper++; return result; } ``` ##### `bn_sub` 減法 兩數 `upper` 與 `lower` 分別相減,若 `a.lower < b.lower`,代表需要跟 `upper` 借位,所以 `upper` 減一。 ```c struct BigN bn_sub(struct BigN a, struct BigN b) { struct BigN result; result.upper = a.upper - b.upper; if (a.lower < b.lower) result.upper--; result.lower = a.lower - b.lower; return result; } ``` ##### `bn_lshift` 左移一位 / `bn_rshift` 右移一位 實作左移一位,將兩數 `upper` 與 `lower` 分別左移一位,若 `lower` 在位移前的最高有效位元為 `1`,則 `upper` 的最低有效位元設為 `1`。實作右移一位與左移相似,兩數 `upper` 與 `lower` 分別右移一位,若 `upper` 在位移前的最低有效位元為 `1`,則 `lower` 的最高有效位元設為 `1`。 ```c struct BigN bn_lshift(struct BigN a) { a.upper <<= 1; if (a.lower & MSB_MASK) a.upper |= 1; a.lower <<= 1; return a; } struct BigN bn_rshift(struct BigN a) { a.lower >>= 1; if (a.upper & 1) a.lower |= MSB_MASK; a.upper >>= 1; return a; } ``` ##### `bn_mul` 乘法 每次將 `a` 左移一位,並將 `b` 右移一位,找出 `b` 中值為 `1` 的位元,若找到為 `1` 的位元,則將目前 `a` 的值累加到 `result` ,達到類似乘法直式的運算方式。 例如 : a = 0b1010, b = 0b101 a * b = 0b1010 * 0b101 = 0b1010 + 0b101000 = 0b110010 ```c struct BigN bn_mul(struct BigN a, struct BigN b) { struct BigN result = bn_init(0, 0); while (b.upper || b.lower) { if (b.lower & 1) result = bn_add(result, a); a = bn_lshift(a); b = bn_rshift(b); } return result; } ``` ##### `bn_to_str` 將大數轉成字串 `input` 為要轉換成字串的大數, `num` 為最靠近且小於等於 `input` 的 `10` 的冪,將 `input` 不停減掉 `num` 直到 `input < num` ,減去的次數即目前 `input` 最高位的十進位數。 例如 : `input = 3021`, 最靠近且小於等於 `input` 的 `10` 的冪為 `num = 1000` , `input` 需要減掉 `num` `3` 次才會滿足 `input < num` 的條件,因此得到最高位的十進位數為 `3` ,後面的位數以此類推。 ```c int bn_to_str(char *buf, char *str) { struct BigN input = bn_init(*(unsigned long long *) buf, *((unsigned long long *) buf + 1)); struct BigN num = bn_init(0, 1); struct BigN ten = bn_init(0, 10); /* maximum value of BigN is 340,282,366,920,938,463,463,374,607,431,768,211,455 (39 digits)*/ int pos = 39; /* highest possible digit index*/ int dig_idx = 0; int dig_num; while (pos--) { for (dig_num = 0, num = bn_init(0, 1); dig_num < pos; dig_num++) num = bn_mul(num, ten); if (dig_idx || !bn_less(input, num) || !pos) { for (dig_num = 0; !bn_less(input, num); dig_num++) input = bn_sub(input, num); str[dig_idx++] = dig_num + '0'; } } str[dig_idx] = 0; return dig_idx; } ``` #### 實驗結果 ![](https://i.imgur.com/NHtJIg3.png) ### 支援大數運算(可動態改變大數長度) #### 大數資料結構 `digits` 紀錄大數的數值,`size` 紀錄大數使用幾個 uint64_t,`alloc` 配置幾個 uint64_t 空間。 ```c typedef u64 bn_digit; typedef u64 bn_size; struct { bn_digit *digits; bn_size size; bn_size alloc; } ``` ##### 重新配置空間 若 `size` 超過 `alloc` 時,代表配置的空間不夠,需要 realloc,`BN_MIN_ALLOC(n, s)` 將大數 `n` 重新配置成 `s` 個 uint64_t 的大小,並確保配置的大小為 4 的倍數個 uint64_t。 `BN_SIZE(n, s)` 的功能類似,會額外將大數 `n` 的 `size` 設為 `s`。 ```c #define BN_MIN_ALLOC(n, s) \ do { \ bn *const __n = (n); \ const bn_size __s = (s); \ if (__n->alloc < __s) { \ __n->digits = \ bn_resize(__n->digits, __n->alloc = ((__s + 3) & ~3U)); \ } \ } while (0) #define BN_SIZE(n, s) \ do { \ bn *const __n = (n); \ __n->size = (s); \ if (__n->alloc < __n->size) { \ __n->digits = \ bn_resize(__n->digits, __n->alloc = ((__n->size + 3) & ~3U)); \ } \ } while (0) ``` ##### `bn_add` 加法 將大數 `a` 與大數 `b` 對應的位數相加,若有進位會累加到下一位。為了確保 a = c 與 b = c 時能正確運算不改到 `a` 或 `b` 的值,使用 `tmp` 紀錄結果,最後再將 `tmp` 複製到 `c`。 ```c /* a + b = c */ void bn_add(bn *a, bn *b, bn *c) { /* a + 0 or 0 + b */ if (a->size == 0) { if (b->size == 0) c->size = 0; else bn_set(c, b); return; } else if (b->size == 0) { bn_set(c, a); return; } /* a + b = 2 * a */ if (a == b) { bn_lshift(a, c); return; } bn_size min_size, max_size; bn_digit *tmp; bn *max_bn; if (a->size <= b->size) { min_size = a->size; max_size = b->size; max_bn = b; } else { min_size = b->size; max_size = a->size; max_bn = a; } tmp = kmalloc((max_size + 1) * BN_DIGITS_SIZE, GFP_KERNEL); for (int i = 0; i < max_size + 1; i++) tmp[i] = 0; bn_digit cy = 0; for (int i = 0; i < min_size; i++) { bn_digit u = a->digits[i]; bn_digit v = b->digits[i]; cy = (u += cy) < cy; tmp[i] = u + v; cy += tmp[i] < v; } if (cy) { for (int i = min_size; i < max_size; i++) { bn_digit u = max_bn->digits[i]; cy = (u += cy) < cy; tmp[i] = u; } if (cy) tmp[max_size++] = cy; } else bn_copy(max_bn->digits + min_size, max_size - min_size, tmp + min_size); BN_SIZE(c, max_size); bn_copy(tmp, max_size, c->digits); kfree(tmp); } ``` ##### `bn_lshift` 左移一位 從低位到高位尋訪大數的 `digits`,每次左移 1 位,藉由將 `digits` 右移 63 位獲取該 `digits` 的最高位,並與下一個 `digits` 做 OR 運算,將下一個 `digits` 的最低位設成目前 `digits` 的最高位。 ```c /* c = a << 1 */ void bn_lshift(bn *a, bn *c) { if (a->size == 0) { bn_set(c, a); return; } if(a != c) BN_SIZE(c, a->size); const unsigned int subp = 63; bn_digit cy = 0; bn_size size = a->size; for (int i = 0; i < size; i++) { bn_digit p = *(a->digits + i); *(c->digits + i) = (p << 1) | cy; cy = p >> subp; } if (cy) { BN_SIZE(c, c->size + 1); c->digits[c->size - 1] = cy; } } ``` #### `bn_mul` 乘法 大數 `a` 的每個位數與大數 `b` 的每個位數相乘,將結果紀錄在 `tmp` ,二個 64-bit 整數相乘會需要 128-bit ,所以使用 GCC Extension `__uint128_t`,透過右移 64 位,將較高的 64-bit 存在 `tmp` 目前位數的下一位,最後再將 `tmp` 複製到 `c`。 ```c void bn_mul(bn *a, bn *b, bn *c) { if (a->size == 0 || b->size == 0) c->size = 0; bn_size csize = a->size + b->size; bn_digit *tmp = kmalloc(csize * BN_DIGITS_SIZE); for (int i = 0; i < csize; i++) tmp[i] = 0; for (int i = 0; i < a->size; i++) { for (int j = 0; j < b->size; j++) { __int128 product = (__int128)a->digits[i] * (__int128)b->digits[j]; bn_size low = product; bn_size high = product >> BN_DIGITS_BITS; tmp[i + j] += low; uint32_t cy = tmp[i + j] < low; tmp[i + j + 1] += high + cy; if (cy) cy = tmp[i + j + 1] <= high; else cy = tmp[i + j + 1] < high; for (int k = i + j + 2; cy; k++) { tmp[k] += cy; cy = tmp[k] < cy; } } } while (csize > 0 && tmp[csize - 1] == 0) csize--; BN_SIZE(c, csize); bn_copy(tmp, csize, c->digits); kfree(tmp); } ``` ##### `bn_to_str` 將大數轉成字串 基本想法為對大數除以 10 取餘數,反覆對其商除以 10 取餘數,將取得的餘數逐一串接起來,最後所得到的就是該數的十進位表示法。 ``` 123 / 10 = 12...3 <- 12 / 10 = 1...2 <- 1 / 10 = 0...1 <- ``` - 方法一(對應到圖中的 Not divided by max_radix) 每次迴圈將大數除以 10 取餘數,這個餘數就是該數的十進位最低位的數值,保留商到下一次迴圈再除以 10,直到商等於 0 時停止迴圈。 方法一對大數一次只除以 10,需要跑很多次迴圈才可以讓商等於 0,因此有了方法二來加速。 - 方法二(對應到圖中 Divided by max_radix) `max_radix` 為 10^19^,相當於 uint64_t 可表示值的範圍中,最大的 10 的冪的值,`max_power` 為 19,紀錄 10 的冪次。 每次迴圈藉由將大數除以 `max_radix`,獲得一個 uint64_t 可表示的餘數,再用方法一來取得最終的餘數,重複上述操作直到商等於 0 時停止迴圈。 ```c static char *bn_to_str(const bn_digit *u, bn_size size, char *out) { while (size && !u[size -1]) /* 取得 u 的有效 size(byte) */ --size; /* u = 0 ~ 9 */ if (size == 0 || (size == 1 && u[0] < 10)) { if (!out) out = malloc(2); out[0] = size ? radix_chars[u[0]] : '0'; out[1] = '\0'; return out; } const bn_digit max_radix = radix_table.max_radix; const unsigned int max_power = radix_table.max_power; if (!out) out = malloc(string_size(size) + 1); char *outp = out; bn_digit *tmp = BN_TMP_COPY(u, size); bn_size tsize = size; do { bn_digit r = bn_ddivi(tmp, tsize, max_radix); tsize -= (tmp[tsize - 1] == 0); unsigned int i = 0; do { bn_digit rq = r / 10; bn_digit rr = r % 10; *outp++ = radix_chars[rr]; r = rq; if (tsize == 0 && r == 0) break; } while (++i < max_power); } while (tsize != 0); free(tmp); char *f = outp - 1; while (*f == '0') --f; f[1] = '\0'; for (char *s = out; s < f; ++s, --f) SWAP(*s, *f); return out; } ``` ![](https://i.imgur.com/AQRj4IG.png) #### 降低 realloc 呼叫次數 在 [sysprog21/bignum](https://github.com/sysprog21/bignum) 中,初始化大數所 malloc 的大小皆相同,在計算 fib(k) 時,k 越大,所需的空間越多,realloc 的次數也越多。 透過預估 fib(k) 所需的位數,在初始化時就分配足夠的空間,降低 realloc 次數。 根據[作業說明](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-b),當 $n$ 為足夠大的正整數時, $$ F(n)\approx0.4472135955\times1.61803398875^n $$ 將近似值取 2 為底的對數,可得到需要使用的位元數 $$ log_2(0.4472135955\times1.61803398875^n)\approx-1.160964 + n\times0.694242 $$ 除以 64 可得到需要使用的 `uint64` 數量 $$ (-1.160964 + n\times0.694242)\div64 = -0.0181400625 + n\times0.01084753125 $$ $$\lfloor\frac{-181400625 + n\times108475313}{10^{10}}\rfloor+1 $$ ```c int bn_fib_digits_n(int n) { if (n < 2) return 1; int digits = ((long)n * 108475313 - 181400625) / 10000000000 + 1; return digits; } ``` 由下圖能觀察到隨著數字增加,預先分配足夠空間的效果越明顯。 ![](https://i.imgur.com/CIyg97D.png) ### 關於 [sysprog21/bignum](https://github.com/sysprog21/bignum) 的問題 :::info 在 [sysprog21/bignum/bignum.c](https://github.com/sysprog21/bignum/blob/master/bignum.c) 中,macro 使用 do while(0) 將多行內容框起來,我想原因是減少語法錯誤,像是下方在 if 中呼叫 macro,且沒有用大括號括住的情形依舊能正確執行,若 macro 沒有 do while(0) 則會出錯。 在 macro 中不用 if(1) { ... } 而是使用 do while(0) 將內容框起來的原因是單純 do while(0) 後可以加分號,因此使用該 macro 時可以較直觀的在後面加分號,還是有其他考量? ```c #define BN_MIN_ALLOC(n, s) \ do { \ ... \ } while (0) if (1) BN_MIN_ALLOC(n, s); ``` ::: :::info 在 [sysprog21/bignum/format.c](https://github.com/sysprog21/bignum/blob/master/format.c) 中,為什麼當 radix 不是 2 的冪時,結果需要額外加 2,而不是加 1? ```c static size_t apm_string_size(apm_size size, unsigned int radix) { ASSERT(radix >= 2); ASSERT(radix <= 36); if ((radix & (radix - 1)) == 0) { unsigned int lg = apm_digit_lsb_shift(radix); return ((size * APM_DIGIT_BITS + lg - 1) / lg) + 1; } /* round up to second next largest integer */ return (size_t)(radix_sizes[radix] * (size * APM_DIGIT_SIZE)) + 2; } ``` :::