--- tags: jserv --- # quiz8 實作紀錄 ## Floating Add * 題目缺陷 * 發現題目只考慮 normalized 和 normalized 相加條件 * 先完成 normalized 和 normalized 相加實作細節 + 抓出 floating point 內,各部分 signed bit、 exponent 和 significand + 因為兩 operand 皆為 normalized 故在 mantissa 前補 1 + 對齊 exponent + 根據正負號決定是否轉為 two's complement 再相加 + 發現[**缺陷**](https://github.com/posutsai/EmbSysProject/blob/master/proj1/FloatAdd/FloatAdd.c#L114) + 在更改前為 `if (sig_Z & 0x01000000)` 只檢查第 25 個 bit,若是兩者 exp 沒對齊的情況下,就不會進位到 25 bit,故應檢查第 24 和 25 位 + 以 `FloatAdd(1.5, 5.5)` 為例 + 1.5 經過右移的 mantissa 為 `0b 00000000001100000000000000000000` + 5.5 經過右移的 mantissa 為 `0b 00000000101100000000000000000000` + 從這個例子就可以知道相加後的 mantissa 並不會進位到第 25 bit + 甚至若其中一個數值為負值,還需要判斷是否 exp 全不為 0 且全不為 1,根據 mantissa 以 while loop 調整 exponent +  把相加後的各部分填入 `Z` ,因為 Z 也是 normalized 要去掉小數點前的 1 +  轉為 int 回傳 ```c= // 缺陷 條件 if (sig_Z & 0x1800000) { sig_Z = sig_Z >> 1; exp_Z += 1; if (exp_Z & 0b100000000) { if (sign_Z > 0) { return 1.0 / 0.0; return -1.0 / 0.0; } } while ((sig_Z & 0b100000000000000000000000) == 0) { sig_Z = sig_Z << 1; exp_Z -= 1; if (exp_Z == 0) return 0; } sig_Z &= 0x7fffff; unsigned int Z = 0; Z |= sign_Z << 31; Z |= exp_Z << 23; Z |= sig_Z; return int_to_float(Z); } ``` * 在處理 IEEE 754 定義的 special number 根據題目原始實作並沒有定義,故在取出 exp_X 及 exp_Y 後判斷二者是否為 `0xFF`,若是則回傳 `-nan` 或 `nan` ## Merge Sort Linked List 一開始先初始化需要排序的資料 ```C= struct mylist *temp; struct list_head *new_head; temp = malloc(sizeof *temp); init_list_head(&temp->list); temp->data = 700; temp->c = 'a'; new_head = &temp->list; for (int i = 1; i < 4; i++) { temp = malloc(sizeof *temp); init_list_head(&temp->list); temp->data = rand() % 1000; temp->c = 'a' + i; tail_add(&temp->list, new_head); } ``` linked-list traversal ```C= struct list_head *curr; curr = new_head; do { printf("%d(%c) ", list_entry(curr, struct mylist, list)->data, list_entry(curr, struct mylist, list)->c); curr = curr->next; } while(curr != new_head); ``` ```shell= $ ./a.out 700(a) 383(b) 886(c) 777(d) ``` 透過以上的程式碼可以建立下圖的結構 ![](https://i.imgur.com/U9W3Kva.png) 先做 divide ```C= struct list_head *merge_sort(struct list_head *head) { if (head->next == head) { return head; } int c = COUNT(head); struct list_head *last_few = SLICE_BW(c / 2, head); struct list_head *first_few = SLICE(c % 2 ? c / 2 + 1 : c / 2, head); return merge(merge_sort(first_few), merge_sort(last_few), predicate); } ``` ![](https://i.imgur.com/Vp7N0fV.png) ![](https://i.imgur.com/2LPSt84.png) 接著做 conquer ```C= static struct list_head *merge(struct list_head *left, struct list_head *right, bool (*pred)(struct list_head *, struct list_head *)) { if (left == NULL) return right; if (right == NULL) return left; struct list_head *head, *tail; /* Fill your solution here */ if(pred(left, right)) { head = left; left = right; right = head; } else { head = right; } tail = head_remove(right); return head_add(head, merge(left, tail, pred)); } ``` `tail = head_remove(right);` 這行的作用是移除 min(left->head, right->head),所以在上面利用 pred 判斷時,如果 left->head 比 right->head 小,就必須 left/right 對調。 conquer 的過程如下 ![](https://i.imgur.com/SoX6oNU.png) ![](https://i.imgur.com/JdxfMz4.png) ```shell= $ ./a.out 700(a) 383(b) 886(c) 777(d) 383(b) 700(a) 777(d) 886(c) ``` * 題目缺陷 在 merge sort 的演算法中會遞迴的呼叫 merge 以及 merge_sort 這兩個函式,且每次的遞迴都需要 push 東西進到 stack 裡面,因此如果需要排序的資料太長的話,會造成遞迴的深度太深,進而形成 stack overflow,先做個簡單的測試: ```shell= ## n = 1000000 $ ./a.out Segmentation fault (core dumped) ``` 的確在長度過長的情況下會因為 stack 被塞滿而造成程式停止。 在進行其他實驗前,考量到初始序列排列的不同會造成不同深度的遞迴,因此在初始任何長度的序列時都按照 temp->data = n - i - 1 的規則來設定順序,例如 n = 8 時,初始序列為 8, 7, 6, 5, 4, 3, 2, 1。 ```shell= $ ulimit -a core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 128026 max locked memory (kbytes, -l) 64 max memory size (kbytes, -m) unlimited open files (-n) 102400 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 128026 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited ``` 在我們所用的機器上 stack size 為 8192 kbytes。 為了計算遞迴過程中所累加的 stack frame size,我們在 merge 以及 merge_sort 加入計算的程式碼。 ```C= static struct list_head *merge(struct list_head *left, struct list_head *right, bool (*pred)(struct list_head *, struct list_head *)) { stack_size += (char *)__builtin_frame_address(1) - (char *)__builtin_frame_address(0); max = max > stack_size ? max : stack_size; if (left == NULL) { stack_size -= (char *)__builtin_frame_address(1) - (char *)__builtin_frame_address(0); return right; } if (right == NULL) { stack_size -= (char *)__builtin_frame_address(1) - (char *)__builtin_frame_address(0); return left; } struct list_head *head, *tail; /* Fill your solution here */ if(pred(left, right)) { head = left; left = right; right = head; } else { head = right; } tail = head_remove(right); struct list_head *tmp_head_add = head_add(head, merge(left, tail, pred)); stack_size -= (char *)__builtin_frame_address(1) - (char *)__builtin_frame_address(0); return tmp_head_add; } struct list_head *merge_sort(struct list_head *head) { stack_size += (char *)__builtin_frame_address(1) - (char *)__builtin_frame_address(0); max = max > stack_size ? max : stack_size; if (head->next == head) { stack_size -= (char *)__builtin_frame_address(1) - (char *)__builtin_frame_address(0); return head; } int c = COUNT(head); struct list_head *last_few = SLICE_BW(c / 2, head); struct list_head *first_few = SLICE(c % 2 ? c / 2 + 1 : c / 2, head); struct list_head *tmp; tmp = merge(merge_sort(first_few), merge_sort(last_few), predicate); stack_size -= (char *)__builtin_frame_address(1) - (char *)__builtin_frame_address(0); return tmp; } ``` :::info 以 `(char *)__builtin_frame_address(1) - (char *)__builtin_frame_address(0);` 量測單一 stack frame 的大小,GCC extension 有提供 [__buitin_frame_address](https://gcc.gnu.org/onlinedocs/gcc/Return-Address.html) 這個函數存取 frame 所在記憶體 stack 中的位置,若丟入參數 `1` 會取得 caller 的位置,反之,若丟入 `0` 則會獲得 callee 的位址,故相減就可得 frame 大小。 ::: stack_size 紀錄現階段因為 merge 及 merge_sort 所造成遞迴的 stack size (因為大部分的遞迴都在這兩個地方造成),max 則記錄整個過程中最大的 stack_size,期望若 max < 8192 kbytes 時會順利的跑完, ```shell= ## n = 209400 $ ./a.out max: 8376176 ## n = 209500 $ ./a.out Segmentation fault (core dumped) ``` 計算在 n = 209400 時,max = 8376176 bytes = 8179 kbytes,這個數字很接近 8192 了,所以當我們稍微增加序列長度,就會造成 stack overflow,為了檢驗這種計算當下使用的 stack size 的方法是不是可行的,我們想要調整系統 stack size 的大小來驗證,看是否在不同 stack size 下,我們算出的 max 會很接近。 ![](https://i.imgur.com/OR9wcT0.png) 由於我們所使用的機器是公用的,因此只能利用 docker 來達成這件事。為了能改變 stack size 的大小,在設定 container 時需要 --privileged 這個參數, ```shell $ docker run --privileged ``` 接著測試在不同的 n 會需要多大的 stack size ```shell= ## n = 32 $ ulimit -s 8 $ ./a.out max: 1456 ## 1456 bytes = 1.42 kbytes ## n = 128 $ ulimit -s 12 $ ./a.out max: 5296 ## 5296 bytes = 5.17 kbytes ## n = 1024 $ ulimit -s 46 $ ./a.out max: 41136 ## 41136 bytes = 40 kbytes ## n = 32768 $ ulimit -s 1287 $ ./a.out max: 1310896 ## 1310896 bytes = 1280 kbytes ``` 可以發現我們算的 max 都會比實際上用到的大小少了 6-7 個 bytes,這可能是因為裏頭其他 function 所造成的 overhead。 ## todo 1. 以更不具侵略性的測量方式,量測 frame size 以目前的做法,在量測 frame size 的同時也增加了 frame 本身的大小,我想比較好的方法會是用老師那天在直播中談到的 eBPF 在 kernel 內做量測,我在 Brenden gregg 的網站有看到類似的[文章](http://www.brendangregg.com/blog/2016-01-18/ebpf-stack-trace-hack.html?fbclid=IwAR2MW_dJ83TN5AaSyHQ1PGn0U79XsdzaEOTaQNUzv5DsBXo4RlI2IHLlS9s),或許可以試試。 :::info 善用 GDB ::: 2. 在實驗中,實際觀察到的數據並不是每一次的 stack max depth 都會一樣,我們找不到合理的解釋,以及實際上 stack 使用的最大深度總是會和我們的量測數據有常數 7k byte 的落差,我們找不到合理的解釋。