--- tags: Linux Kernel --- # 2023q1 Homework3 (fibdrv) contributed by < [`chun61205`](https://github.com/chun61205) > > [K04: fibdrv](https://hackmd.io/@sysprog/linux2022-fibdrv) ## 實驗環境 ```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): 8 On-line CPU(s) list: 0-7 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz CPU family: 6 Model: 126 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 5 CPU max MHz: 3600.0000 CPU min MHz: 400.0000 BogoMIPS: 2380.80 ``` ## 開發紀錄 ### Fast Doubling $F(2k) = F(k)[2F(k+1) - F(k)] \\ F(2k+1) = F(k+1)^2+F(k)^2$ $F(10)$: 10~10~ = 1010~2~ | i | start | 4 | 3 | 2 | 1 | result | |---|-------|----------|----------|----------|---------|--------| | n | - | ==1==010 | 1==0==10 | 10==1==0 | 101==0== | - | |F(m) | F(0) | F(0*2+1) | F(1*2) | F(2*2+1) | F(5*2) | F(10) | | a | 0 | 1 | 1 | 5 | 55 | 55 | | b | 1 | 1 | 2 | 8 | 89 | - | ### 引入 Fast Doubling 原本 fibdrv 採用 Dynamic Programming 實作 `fib_sequence` ,為了減少計算量,我引入 Fast Doubling 並加入 GCC`__uint128_t` 以增加能夠計算的大小。 ```c static __uint128_t fib_sequence(int k) { if (k < 2) return k; __uint128_t a = 0, b = 1, t1, t2; int len = 32 - __builtin_clz(k); for(; len > 0; len--){ t1 = a * (2 * b - a); t2 = b * b + a * a; a = t1; b = t2; if((k >> (len - 1)) & 0x1){ t1 = a + b; a = b; b = t1; } } return a; } ``` 然而,經過測試,上面這段程式碼無法解決 $F(93)$ 會得出錯誤結果的問題。推測原因如下: ```c static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) { return (ssize_t) fib_sequence(*offset); } ``` 這段程式碼會將 `fib_sequence` 的回傳值改成 `ssize_t` ,因此單是更改 `fib_sequence` 的回傳值型態無法解決問題,計畫在研究 [Small/Short String Optimization](http://wdv4758h.github.io/notes/cpp/string-optimization.html) 和 [sysprog21/bignum](https://github.com/sysprog21/bignum) 後改善。 ### F(93) 會計算錯誤的原因 參考 [yanjiew1](https://hackmd.io/v4itYIvpToq0rICnuRAvLQ?both) 透過公式解可直接計算第 $n$ 個費氏數: $$ F(n)= \frac{(\phi^n)-(1-\phi)^n}{\sqrt5} \\ \phi \text{ (golden ratio)} = \frac{1+\sqrt5}{2} \approx 1.61803398875 $$ 當 n 足夠大時,可得到約略值: $$ F(n) \approx \frac{\phi^n}{\sqrt5} \approx 0.4472135955 \times 1.61803398875^n $$ 取得以 2 為底的對數 $$ \log F(n) \approx -1.160964 + n\times0.694242 $$ 這裡的 $logF(n)$ 正好表示需要多少 bits 才能完全表達 $F(n)$ 。 $$ logF(93) \approx 63.4 $$ 因此會造成 overflow 。 若是代入 $logF(n)=128$ ,則 $n\approx186$ 大概可以表示到 $F(186)$ ### 研究大數運算 #### `bn` 資料結構 :::spoiler 完整程式碼 ```c /* number[size - 1] = msb, number[0] = lsb */ typedef struct _bn { unsigned int *number; unsigned int size; int sign; } bn; ``` * `number` - 指向儲存的數值,之後會以 array 的形式來取用 * `size` - 配置的記憶體大小,單位為 sizeof(unsigned int) * `sign` - `0` 為正數、`1` 為負數 #### `bn_to_string` 由於大數沒辦法直接以數值的形式列出,因此需要透過此函數來轉換成字串。 :::spoiler 完整程式碼 ```c /* * output bn to decimal string * Note: the returned string should be freed with kfree() */ char *bn_to_string(bn src) { // log10(x) = log2(x) / log2(10) ~= log2(x) / 3.322 size_t len = (8 * sizeof(int) * src.size) / 3 + 2 + src.sign; char *s = kmalloc(len, GFP_KERNEL); char *p = s; memset(s, '0', len - 1); s[len - 1] = '\0'; for (int i = src.size - 1; i >= 0; i--) { for (unsigned int d = 1U << 31; d; d >>= 1) { /* binary -> decimal string */ int carry = !!(d & src.number[i]); for (int j = len - 2; j >= 0; j--) { s[j] += s[j] - '0' + carry; // double it carry = (s[j] > '9'); if (carry) s[j] -= 10; } } } // skip leading zero while (p[0] == '0' && p[1] != '\0') { p++; } if (src.sign) *(--p) = '-'; memmove(s, p, strlen(p) + 1); return s; } ``` ::: * `kmalloc` 會分配連續的虛擬記憶體,同時也會分配連續的實體記憶體。而 `vmalloc` 則會分配非連續的實體記憶體空間,因此需要映射時,效率較低。 * `for` 迴圈會將 `bn` 轉換成 `string` * 每 $32$ 個 bytes 處理一次 * `carry` 表示是否進位初始值為 ``!!(d & src.number[i])`` * `s[j] += s[j] - '0' + carry` 累加的方式為原本的數值乘以二再加上 `carry` * 最後如果有進位則減去 $10$ * 最後將 leading zero 去掉後返回 ::: info 待確認: 為什麼 `len` 的計算是除以 `3` 而不是 `3.322` 猜測如下: 1. 整數的除法相較浮點數的除法來的快速,因此使用整數 `3` 2. 最後會將 leading zero 去除,因此除數少一點點使得 `len` 多出來的部份會被去除掉 順帶一提, chatgpt 回答如下: ![](https://i.imgur.com/OVy39Bp.png) ::: #### `bn_add` 和 `bn_sub` :::spoiler 完整程式碼 ```c /* c = a + b * Note: work for c == a or c == b */ void bn_add(const bn *a, const bn *b, bn *c) { if (a->sign == b->sign) { // both positive or negative bn_do_add(a, b, c); c->sign = a->sign; } else { // different sign if (a->sign) // let a > 0, b < 0 SWAP(a, b); int cmp = bn_cmp(a, b); if (cmp > 0) { /* |a| > |b| and b < 0, hence c = a - |b| */ bn_do_sub(a, b, c); c->sign = 0; } else if (cmp < 0) { /* |a| < |b| and b < 0, hence c = -(|b| - |a|) */ bn_do_sub(b, a, c); c->sign = 1; } else { /* |a| == |b| */ bn_resize(c, 1); c->number[0] = 0; c->sign = 0; } } } /* c = a - b * Note: work for c == a or c == b */ void bn_sub(const bn *a, const bn *b, bn *c) { /* xor the sign bit of b and let bn_add handle it */ bn tmp = *b; tmp.sign ^= 1; // a - b = a + (-b) bn_add(a, &tmp, c); } ``` ::: * `bn_add` 會先判斷 `a` 和 `b` 的 `sign` 如果是一樣的就做加法,不一樣的就做減法。而 `bn_sub` 則是直接將 `a` 和 `b` 交給 `bn_add` 處理。 #### `bn_do_add` :::spoiler 完整程式碼 ```c /* |c| = |a| + |b| */ static void bn_do_add(const bn *a, const bn *b, bn *c) { // max digits = max(sizeof(a) + sizeof(b)) + 1 int d = MAX(bn_msb(a), bn_msb(b)) + 1; d = DIV_ROUNDUP(d, 32) + !d; bn_resize(c, d); // round up, min size = 1 unsigned long long int carry = 0; for (int i = 0; i < c->size; i++) { unsigned int tmp1 = (i < a->size) ? a->number[i] : 0; unsigned int tmp2 = (i < b->size) ? b->number[i] : 0; carry += (unsigned long long int) tmp1 + tmp2; c->number[i] = carry; carry >>= 32; } if (!c->number[c->size - 1] && c->size > 1) bn_resize(c, c->size - 1); } ``` ::: * `d` 代表總位數 * `MAX(bn_msb(a), bn_msb(b)) + 1` 的 `+1` 是為了避免 `MAX(bn_msb(a), bn_msb(b))` 溢位。 * `DIV_ROUNDUP` 表示向上取整數,會回傳 `d` 是 `32` 的幾倍 * 因為 `d` 的最小值應該為 `1` ,如果 `d = 0` ,則應該加上 `!d` ,也就是 `1` * 利用 `for (int i = 0; i < c->size; i++)` 將各個位數相加。 #### `bn_do_sub` :::spoiler 完整程式碼 ```c /* * |c| = |a| - |b| * Note: |a| > |b| must be true */ static void bn_do_sub(const bn *a, const bn *b, bn *c) { // max digits = max(sizeof(a) + sizeof(b)) int d = MAX(a->size, b->size); bn_resize(c, d); long long int carry = 0; for (int i = 0; i < c->size; i++) { unsigned int tmp1 = (i < a->size) ? a->number[i] : 0; unsigned int tmp2 = (i < b->size) ? b->number[i] : 0; carry = (long long int) tmp1 - tmp2 - carry; if (carry < 0) { c->number[i] = carry + (1LL << 32); carry = 1; } else { c->number[i] = carry; carry = 0; } } d = bn_clz(c) / 32; if (d == c->size) --d; bn_resize(c, c->size - d); } ``` ::: 計算邏輯大部分和 `bn_do_add` 相同,不過如果相減小於 $0$ 則需要先對調 `a` 和 `b` 。 * 這裡的 `carry` 表示借位。 #### `bn_mult` :::spoiler 完整程式碼 ```c /* * c = a x b * Note: work for c == a or c == b * using the simple quadratic-time algorithm (long multiplication) */ void bn_mult(const bn *a, const bn *b, bn *c) { // max digits = sizeof(a) + sizeof(b)) int d = bn_msb(a) + bn_msb(b); d = DIV_ROUNDUP(d, 32) + !d; // round up, min size = 1 bn *tmp; /* make it work properly when c == a or c == b */ if (c == a || c == b) { tmp = c; // save c c = bn_alloc(d); } else { tmp = NULL; for (int i = 0; i < c->size; i++) c->number[i] = 0; bn_resize(c, d); } for (int i = 0; i < a->size; i++) { for (int j = 0; j < b->size; j++) { unsigned long long int carry = 0; carry = (unsigned long long int) a->number[i] * b->number[j]; bn_mult_add(c, i + j, carry); } } c->sign = a->sign ^ b->sign; if (tmp) { bn_swap(tmp, c); // restore c bn_free(c); } } /* c += x, starting from offset */ static void bn_mult_add(bn*c, int offset, unsigned long long int x) { unsigned long long int carry = 0; for (int i = offset; i < c->size; i++) { carry += c->number[i] + (x & 0xFFFFFFFF); c->number[i] = carry; carry >>= 32; x >>= 32; if (!x && ! carry) //done return; } } ``` ::: 此程式碼分別計算每一個位數的乘法,以得到答案。 ```c for (int i = 0; i < a->size; i++) { for (int j = 0; j < b->size; j++) { unsigned long long int carry = 0; carry = (unsigned long long int) a->number[i] * b->number[j]; bn_mult_add(c, i + j, carry); } } ``` #### `bn_lshift` :::spoiler 完整程式碼 ```c /* left bit shift on bn (maximun shift 31) */ void bn_lshift(const bn *src, size_t shift, bn *dest) { size_t z = bn_clz(src); shift %= 32; // only handle shift within 32 bits atm if (!shift) return; if (shift > z) { bn_resize(dest, src->size + 1); } else { bn_resize(dest, src->size); } /* bit shift */ for (int i = src->size - 1; i > 0; i--) dest->number[i] = src->number[i] << shift | src->number[i - 1] >> (32 - shift); dest->number[0] = src->number[0] << shift; } ``` ::: 此程式碼可以將 `src` 向左 shift。 1. 此程式碼最大可 shift 的位元數為 $32$ ```c shift %= 32; ``` 2. 為了處理 left shift 超出的位元,必須將 `src->number[i]` 的最右邊 `32 - shift` 個位元,串接上 `src->number[i-1]` 最左邊的 `shift` 個位元。 ```c dest->number[i] = src->number[i] << shift | src->number[i - 1] >> (32 - shift); ``` #### `bn_clz` :::spoiler 完整程式碼 ```c /* count leading zeros of src*/ static int bn_clz(const bn *src) { int cnt = 0; for (int i = src->size - 1; i >= 0; i--) { if (src->number[i]) { // prevent undefined behavior when src = 0 cnt += __builtin_clz(src->number[i]); return cnt; } else { cnt += 32; } } return cnt; } ``` ::: 此程式碼透過 `__builtin_clz` 計算 leading zero ,如果迴圈遇到非零的數,則代表最高位數在該格中,因此計算完後回傳結果。 ### 使用大數運算來實做 `fib_sequence` :::spoiler 完整程式碼 ```c static void bn_fib_sequence(bn *dest, int k) { bn_resize(dest, 1); if (k < 2) { dest->number[0] = k; return; } /* a: F(k), b: F(k+1) */ bn *a = dest, *b = bn_alloc(1); a->number[0] = 0; b->number[0] = 1; bn *t1 = bn_alloc(1), *t2 = bn_alloc(1); for (unsigned int i = 1U << 31; i; i >>= 1) { /* F(2k) = F(k) * [ 2 * F(k+1) – F(k) ] */ bn_cpy(t1, b); bn_lshift(t1, 1); bn_sub(t1, a, t1); bn_mult(t1, a, t1); /* F(2k+1) = F(k)^2 + F(k+1)^2 */ bn_mult(a, a, a); bn_mult(b, b, b); bn_add(t2, a, b); if (k & i) { bn_cpy(a, t2); bn_cpy(b, t1); bn_add(b, t2, b); } else { bn_cpy(a, t1); bn_cpy(b, t2); } } bn_free(b); bn_free(t1); bn_free(t2); } ``` ::: 並透過 `copy_to_user` 把資料傳送到 user mode 輸出。 ```c bn *fib_time_proxy(long long k) { kt = ktime_get(); bn *dest = bn_alloc(1); fib_fdoubling_bn(dest, k); kt = ktime_sub(ktime_get(), kt); return dest; } static ssize_t fib_read(struct file * file, char *buf, size_t size, loff_t *offset) { bn *dest = fib_time_proxy(*offset); char *result = bn_to_string(dest); int len = strlen(result); copy_to_user(buf, result, len); return (ssize_t) len; } ``` 結果如下: ```shell Reading from /dev/fibonacci at offset 92, returned the sequence 7540113804746346429. utime 33064, ktime 28621 Reading from /dev/fibonacci at offset 93, returned the sequence 12200160415121876738. utime 33287, ktime 28915 Reading from /dev/fibonacci at offset 94, returned the sequence 19740274219868223167. utime 38217, ktime 27735 Reading from /dev/fibonacci at offset 95, returned the sequence 31940434634990099905. utime 37996, ktime 27815 Reading from /dev/fibonacci at offset 96, returned the sequence 51680708854858323072. utime 37179, ktime 27087 Reading from /dev/fibonacci at offset 97, returned the sequence 83621143489848422977. utime 36502, ktime 27363 Reading from /dev/fibonacci at offset 98, returned the sequence 135301852344706746049. utime 35970, ktime 27510 Reading from /dev/fibonacci at offset 99, returned the sequence 218922995834555169026. utime 37317, ktime 28233 Reading from /dev/fibonacci at offset 100, returned the sequence 354224848179261915075. utime 38529, ktime 29498 ``` 結果正確。 ### 量測時間 參考 [shawn5141](https://hackmd.io/@Shawn5141/linux2022q1-hw3#%E9%87%8F%E6%B8%AC%E6%99%82%E9%96%93%E7%9A%84%E6%96%B9%E5%BC%8F) 和 [bakudr18](https://hackmd.io/@bakudr18/HJ363p_Qd#%E6%B8%AC%E9%87%8F-user-space-%E8%88%87-kernel-space-%E6%99%82%E9%96%93) #### 在 user space 下 使用 [clock_gettime](https://linux.die.net/man/2/clock_gettime) 下的 `CLOCK_MONOTONIC` 來計時,`CLOCK_MONOTONIC` 是從特定起始時間單調遞增的計時方式。 ```c // client.c static inline long long elapsed(struct timespec *t1, struct timespec *t2) { return (long long) (t2->tv_sec - t1->tv_sec) * 1e9 + (long long) (t2->tv_nsec - t1->tv_nsec); } for (int i = 0; i <= offset; i++) { clock_gettime(CLOCK_MONOTONIC, &t1); sz = read(fd, buf, 1); clock_gettime(CLOCK_MONOTONIC, &t2); long long utime = elapsed(&t1, &t2); } ``` #### 在 kernel space 下 使用 [hrtimer](https://lwn.net/Articles/167897/) 進行量測。 ```c // fibdrv.c static ktime_t kt; static long long fib_time_proxy(long long k) { kt = ktime_get(); long long result = fib_sequence_dp(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); } ``` ```c // client.c for (int i = 0; i <= offset; i++) { ssize_t kt = write(fd, NULL, 0); } ``` ![](https://i.imgur.com/8RyG7ii.png) ### 研究程式碼的運作方式 程式碼分為 user space 和 kernel space , `client.c` 可以視為 user space ,剩下的則為 kernel space。 #### 使用 kernel space 的 system call 如果 user space 要使用 kernel space 的 system call ,則必須先掛載核心模組。 ```c $ sudo insmod fibdrv.ko ``` #### File Operations 在 `fibdrv.c` 中,定義了 file operations ```c // fibdrv.c const struct file_operations fib_fops = { .owner = THIS_MODULE, .read = fib_read, .write = fib_write, .open = fib_open, .release = fib_release, .llseek = fib_device_lseek, }; ``` 舉例來說,如果在 user space 呼叫 `read` ,就相當於在 kernel space 呼叫 `fib_read` 。 #### `offset` 在一開始看到這段程式碼的時候我非常納悶,明明在 `client.c` 並沒有把 `offset` 作為參數傳入,為什麼能夠在 `fib_read` 中使用。 經過調查後得知,`offset` 是在 linux 核心中已經定義好的變數,指向目前檔案系統讀寫的位置。 #### `lseek` 先解釋 `lseek` 。 `lseek` 這個函式的目的在於指定目前讀寫的位置,透過以下程式碼 ```c // client.c lseek(fd, i, SEEK_SET); ``` `lseek` 能夠將讀寫的位置設定在「從檔案的起頭開始數的第 `i` byte」。 ```c // fibdrv.c static loff_t fib_device_lseek(struct file * file, loff_t offset, int orig) { loff_t new_pos = 0; switch (orig) { case 0: /* SEEK_SET: */ new_pos = offset; break; case 1: /* SEEK_CUR: */ new_pos = file->f_pos + offset; break; case 2: /* SEEK_END: */ new_pos = MAX_LENGTH - offset; break; } if (new_pos > MAX_LENGTH) new_pos = MAX_LENGTH; // max case if (new_pos < 0) new_pos = 0; // min case file->f_pos = new_pos; // This is what we'll use now return new_pos; } ``` 從上面的程式碼可以看出, `lseek` 的回傳值為目前檔案的讀寫位置。 值得注意的是,輸入不同的 `orig` 會有不一樣的結果, linux 核心定義的 Macro 如下: 1. `SEEK_SET`: 將檔案讀寫的位置設成 `offset` 。 2. `SEEK_CUR`: 將檔案讀寫的位置設成「當前的位置+ `offset` 」。 3. `SEEK_END`: 將檔案讀寫的位置設成從檔案的尾端向前數 `offset` 處。 #### `read` 了解了 `lseek` 可以來看看 `read` 。 `fibdrv` 的特別之處在於,這裡的程式碼並不是真的為了從檔案系統讀取檔案,而是透過 `lseek` 移動 `offset` ,並將 `offset` 的值傳入 `fib_sequence` 來達到計算 fibonacci number 的效果。 具體作法如下: 1. 透過 `lseek` 移動 `offset` ```c // client.c for (int i = 0; i <= offset; i++) { lseek(fd, i, SEEK_SET); } ``` 2. `offset` 被設定為 `i` 的值 ```c // fibdrv.c new_pos = offset; return new_pos; ``` 3. 呼叫 `read` ```c for (int i = 0; i <= offset; i++) { sz = read(fd, buf, 1); } ``` 4. `read` 透過 `offset` 來呼叫 `fib_sequence` ```c static ssize_t fib_read(struct file * file, char *buf, size_t size, loff_t *offset) { return (ssize_t) fib_time_proxy(*offset); } ``` ### 將 `bn_to_string` 移動到 user space 分析 `fib_fdoubling_bn` 後可以得到下面的圖。 ![](https://i.imgur.com/gDrRKOr.png) 可以發現,隨著 `n` 增加, `copy_to_user` 的成本越高,這是因為 copy 的資料越大 `copy_to_user` 的執行時間越多。因此可以嘗試將 `bn_to_string` 移動到 user space ,以此來減少 `copy_to_user` 傳輸的時間成本。 ```c // client.c unsigned int *tmp = (unsigned int*)buf; char *s = bn_to_string_user(tmp, len); ```` ```c // fibdrv.c int len = dest->size; copy_to_user(buf, (char*)dest->number, 4 * len); ``` 結果如下,可以發現 `copy_to_user` 的時間不會明顯隨著 `n` 變大而提升。 ![](https://i.imgur.com/pjtuuGr.png) ### 研究 fibdrv 的 mutex #### 觀察 `fibdrv.c` 的 `__init` 和 `__exit` > [Linux 核心模組運作原理](https://hackmd.io/@sysprog/linux-kernel-module#Linux-%E6%A0%B8%E5%BF%83%E6%A8%A1%E7%B5%84%E9%81%8B%E4%BD%9C%E5%8E%9F%E7%90%86) 可以看到 kernel ```c // fibdrv.c module_init(init_fib_dev); module_exit(exit_fib_dev); ``` 這兩段程式碼分別定義兩個函式, `init_fib_dev` 和 `exit_fib_dev` 。 #### 觀察 `fibdrv.c` 中的 mutex 在 `fibdrv.c` 中有使用到 mutex 的函式除了初始化和刪除 mutex 外總共有兩個, `fib_open` 和 `fib_release` 可以看到在 `fib_open` 時 `/dev/fibonacci` 的存取便會被保護住: ```c if (!mutex_trylock(&fib_mutex)) { printk(KERN_ALERT "fibdrv is in use"); return -EBUSY; } ``` 在 `fib_release` 時則會釋放: ```c mutex_unlock(&fib_mutex); ``` #### 撰寫 POSIX Thread 測試程式碼 ```c // client.c #define THREAD_COUNT 2 void run(){ char buf[1000]; int offset = 100; struct timespec t1, t2; FILE *data = fopen("perf/time.txt", "w"); int fd = open(FIB_DEV, O_RDWR); if (fd < 0) { perror("Failed to open character device"); exit(1); } for (int i = 0; i <= offset; i++) { lseek(fd, i, SEEK_SET); int len = read(fd, buf, 1); unsigned int *tmp = (unsigned int *) buf; char *s = bn_to_string_user(tmp, len); printf("Reading from " FIB_DEV " at offset %d, returned the sequence " "%s\n", i, s); fprintf(data, "%d %lld %ld %lld\n", i); } return; } int main(){ pthread_t pt[THREAD_COUNT]; for (int i = 0; i < THREAD_COUNT; i++) { pthread_create(&pt[i], NULL, run, NULL); } for (int i = 0; i < THREAD_COUNT; i++) { pthread_join(pt[i], NULL); } return 0; } ``` ```shell ... Reading from /dev/fibonacci at offset 100, returned the sequence 354224848179261915075 Reading from /dev/fibonacci at offset 97, returned the sequence 83621143489848422977 Reading from /dev/fibonacci at offset 98, returned the sequence 135301852344706746049 Reading from /dev/fibonacci at offset 99, returned the sequence 218922995834555169026 Reading from /dev/fibonacci at offset 100, returned the sequence 354224848179261915075 ``` 可以看到雖然順序不一定,結果則是對的。