2018q1 Homework3 (list) === contributed by <`rwe0214`> ###### tags: `Linux`, `rwe0214`, `NCKU` ## 作業說明 * [D05: list](https://hackmd.io/s/HkxZbJzif#) ## 生日悖論 ### 如何透過近似的運算式,估算上述機率計算呢?附上有效的 LaTeX 算式 * 假設 1 年有 365 天 * n 個人生日不重複的事件為 $C_{n}^{365} \times\ n!$ * 樣本數為 $365^{n}$ * 機率 $p$ 為 $\dfrac{C_{n}^{365} \times\ n!}{365^{n}}$ * 則 n 個人生日重複的機率 $p'$ 為 $1-\dfrac{C_{n}^{365} \times\ n!}{365^{n}}$ * $C_{n}^{365}$ 怎麼計算? * 將 $p$ 展開可得 $\frac{365}{365} \times \frac{364}{365} \times \frac{363}{365} \times\ ... \times\ \frac{365-n+1}{365}= 1 \times (1 - \frac{1}{365}) \times (1 - \frac{2}{365}) \times (1 - \frac{3}{365}) \times\ ...\times\ (1 - \frac{n-1}{365})$ * 引入泰勒級數且因為 $x<<1$,所以 $e^{x} ≈ 1 - x$ * 原式變成 $1 \times e^{\frac{-1}{365}} \times e^{\frac{-2}{365}} \times e^{\frac{-3}{365}} \times\ ...\times\ e^{\frac{-n}{365}} = e^\frac {-n(n-1)/2}{365} = e^\frac {-n(n-1)}{730}$ * $p' = 1 - e^\frac {-n(n-1)}{730}$ * 將 n = 22 代入 $p' ≈ 1 - e^\frac {-462}{730} = 1 - e^\frac {-231}{365} = 1 - e^{-0.6328} ≈ 1 - 0.531 = 0.469$ * 將 n = 23 代入 $p' ≈ 1 - e^\frac {-506}{730} = 1 - e^\frac {-253}{365} = 1 - e^{-0.69315} ≈ 1 - 0.4999 = 0.5001$,機率為 $50.01%$ ### [Birthday problem](https://en.wikipedia.org/wiki/Birthday_problem) 對資訊安全的影響在哪些層面 * [生日攻擊](http://blog.renren.com/share/250153535/16381267487/0) 即利用在 hash function 中的 collision 現象做攻擊。 * 一個設計良好的 hash function 不容易利用 hash value 反推回原本的明文 $t_1$,但是若是這個 hash value $h(t_1)$ 的 bit 數不夠多( 即值域不夠大 ),那麼我們可以透過生日悖論的想法,去枚舉找到另一份明文 $t_2$ 其 $h(t_2) = h(t_1)$ * 也許枚舉這個動作聽來不怎麼可怕,但是將生日悖論求得的數學式代入會發現 * $p = e^\frac {-m(m-1)/2}{2^n} ≈ e^\frac {m^2}{2^n}$ * $p' ≈ 1 - e^\frac {m^2}{2^n}$ * 若 $p' > 0.5$ 則 $m ≈ 2^{n/2}$ * 換句話說,我只需要枚舉 $2^{n/2}$ 的 $t_2$ 就有超過一半的機率使得 $h(t_2) = h(t_1)$ ### 像是 [Random Number Generator](http://stattrek.com/statistics/random-number-generator.aspx) 這樣的產生器,是否能產生「夠亂」的亂數呢? * 在這個亂數產生器中的 FAQ 有寫到一點: ``` Although no computer algorithm can produce numbers that are truly random, Stat Trek's Random Number Generator produces numbers that are nearly random. ``` * 換句話說這個產生器也只能產生接近真的亂數的偽亂數而已 * 那要如何產生真正的亂數呢? * 在這個產生器裡,亂數產生是根據使用者輸入的 seed 所 "計算" 出來的。 * 所以我們只要在每次產生亂數的時候,隨機的輸入 seed 的就行了! * ??? * 產生亂數的時候要先隨機輸入 seed,這不就是蛋生雞還是雞生蛋一樣的問題了嗎! * 參考資料 * [隨機亂數生成演算法獲突破](https://www.inside.com.tw/2016/05/28/new-technique-produces-real-randomness) * [你的程式夠「亂」嗎?](https://www.ithome.com.tw/voice/110007) ## Linux 風格的 linked list ### 為何 Linux 採用 macro 來實作 linked list?一般的 function call 有何成本? * function call 會在程式呼叫時將 function 所需 argument 和 pc 返回地址 `push` 進 stack,並且在 function 結束時從 stack `pop` 出正確的 pc 返回地址。 * 而 macro 則是會在程式呼叫的地方直接展開插入程式碼,而省略了 push 和 pop 的動作。 ### Linux 應用 linked list 在哪些場合?舉三個案例並附上對應程式碼,需要解說,以及揣摩對應的考量 * 在老師的講座中有提到,linux 在排程管理上使用了 linked list 來實做,所以我便以這個方向去查詢。 1. [linux/kernel/sched/sched.h](https://github.com/torvalds/linux/blob/master/kernel/sched/sched.h) ```clike= /* * This is the priority-queue data structure of the RT scheduling class: */ struct rt_prio_array { DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */ struct list_head queue[MAX_RT_PRIO]; }; ``` * 功能:process 管理 * 依據註解的說明這個 struct 是 real-time schedule 的 priority-queue,而這個 queue 是以 linked list 來實做 2. [linux/mm/slab.h](https://github.com/torvalds/linux/blob/master/mm/slab.h) ```clike= /* * Common fields provided in kmem_cache by all slab allocators * This struct is either used directly by the allocator (SLOB) * or the allocator must include definitions for all fields * provided in kmem_cache_common in their definition of kmem_cache. * * Once we can do anonymous structs (C11 standard) we could put a * anonymous struct definition in these allocators so that the * separate allocations in the kmem_cache structure of SLAB and * SLUB is no longer needed. */ struct kmem_cache { unsigned int object_size;/* The original size of the object */ unsigned int size; /* The aligned/padded/added on size */ unsigned int align; /* Alignment as calculated */ slab_flags_t flags; /* Active flags on the slab */ unsigned int useroffset;/* Usercopy region offset */ unsigned int usersize; /* Usercopy region size */ const char *name; /* Slab name for sysfs */ int refcount; /* Use counter */ void (*ctor)(void *); /* Called on object slot creation */ struct list_head list; /* List of all slab caches on the system */ }; ``` * 功能:kernel memory 的管理 * 由註解中可以看到在這個 struct 中也是以 linked list 穿接起系統中所有的 slab cache * What is slab cache ? * Slab 算是 Kernel 裡最早的一種演算法,事實上,除了 Slab 之外,Linux Kernel 已經實作出數種演算法,以優化記憶體的配置機制。前面提到,Kernel 的底層是以 page 為記憶體配置的單位,因此,哪怕只是需要 1 Byte 的空間,都會佔用一個 page,所以良好的 Slab 演算法,才能盡可能避免記憶體浪費和破碎的問題。 * Slab Allocator 實作了一個 cache 的架構(這裡的 cache 並非意指快取),對系統程式下的記憶體需求群組化,以達到管理的目的。一般的程式可以藉由向 Slab 註冊 cache,以得到記憶體的配置。 * 參考資料:[Linux Kernel 記憶體管理機制之美](http://fred-zone.blogspot.tw/2009/03/linux-kernel.html) 3. [linux/mm/swap.c](https://github.com/torvalds/linux/blob/master/mm/swap.c) ```clike= /** * put_pages_list() - release a list of pages * @pages: list of pages threaded on page->lru * * Release a list of pages which are strung together on page.lru. Currently * used by read_cache_pages() and related error recovery code. */ void put_pages_list(struct list_head *pages) { while (!list_empty(pages)) { struct page *victim; victim = list_entry(pages->prev, struct page, lru); list_del(&victim->lru); put_page(victim); } } ``` * 功能:釋放一連串的 page ( 以 lru 法則挑選 ) * 不僅行程排班是以 linked list 實做,連記憶體的 page 結構也是以 linked list 來完成。 ### GNU extension 的 typeof 有何作用?在程式碼中扮演什麼角色? * 回傳一個 type,e.g. `typeof(a) b` 就會以 a 的型態宣告一個變數 b * 可以幫助我們以更高層次的角度去封裝程式碼 * 參考資料:[Using the GNU Compiler Collection (GCC): Typeof](https://gcc.gnu.org/onlinedocs/gcc/Typeof.html) ### 解釋以下巨集的原理 ```Clike #define container_of(ptr, type, member) __extension__({ const __typeof__(((type *) 0)->member) *__pmember = (ptr); (type *) ((char *) __pmember - offsetof(type, member)); }) ``` * 這個 macro 定義了 `ptr` 去指向 `member` 這個結構成員的地址,這個地方也使用 typeof 來讓 `ptr` 可以是使用者所指定的 type。 * 最後再減掉這個 member 在原本 `struct` 中的記憶體位移量來算出 `struct` 的記憶體起始位置。 ### 除了你熟悉的 add 和 delete 操作,list.h 還定義一系列操作,為什麼呢?這些有什麼益處? * [linux/inlude/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) * 一打開便在開頭發現了註解 ``` /* * Simple doubly linked list implementation. * * Some of the internal functions ("__xxx") are useful when * manipulating whole lists rather than single entries, as * sometimes we already know the next/prev entries and we can * generate better code by using them directly rather than * using the generic single-entry routines. */ ``` * 說明在某些已經知道 pre/next 節點的情況下,使 __xxx 版本的 linked list 操作會更 useful。 ### `LIST_POISONING` 這樣的設計有何意義? * [linux/include/poison.h](https://github.com/torvalds/linux/blob/master/include/linux/poison.h) ```clike= /* * These are non-NULL pointers that will result in page faults * under normal circumstances, used to verify that nobody uses * non-initialized list entries. */ #define LIST_POISON1 ((void *) 0x100 + POISON_POINTER_DELTA) #define LIST_POISON2 ((void *) 0x200 + POISON_POINTER_DELTA) ``` * 當程式 access 到 `LIST_POISONING` 時便會發出 page fault 以便確保沒有人會使用到沒有初始化過得 list entry ( 被刪除掉的 ) * 而在 `list_del()` 中可發現被刪除的 list entry 都會指向 `LIST_POISONING` ```clike= static inline void list_del(struct list_head *entry) { __list_del_entry(entry); entry->next = LIST_POISON1; entry->prev = LIST_POISON2; } ``` ### linked list 採用環狀是基於哪些考量? * 因為 linux 使用 doubly linked list 使用 circular 可以省掉總是需要兩個指標去指向頭和尾的空間,並在操作上也省去這些步驟。 * ### `list_for_each_safe` 和 `list_for_each` 的差異在哪?"safe" 在執行時期的影響為何? * 參考 [`LIST_POISONING` 這樣的設計有何意義?](#LIST_POISONING-%E9%80%99%E6%A8%A3%E7%9A%84%E8%A8%AD%E8%A8%88%E6%9C%89%E4%BD%95%E6%84%8F%E7%BE%A9%EF%BC%9F) * 在 `list_for_each_safe` 中多了一個變數儲存 pos 的地址,這樣可以確保如果在迴圈中使用 `list_del` 刪除節點後,程式指標不會發生找不到 head 而無限迴圈的現象。 ```clike= #define list_for_each(pos, head) for (pos = (head)->next; pos != (head); pos = pos->next) #define list_for_each_safe(pos, n, head) for (pos = (head)->next, n = pos->next; pos != (head); pos = n, n = pos->next) ``` ### for_each 風格的開發方式對程式開發者的影響為何? * 參考資料:[Foreach loop | Programmer's Wiki | FANDOM powered by Wikia](http://code.wikia.com/wiki/Foreach_loop) * 以程式開發者來說 for 已經相當方便並且可以自由的制定範圍,但是若只是單純想要走訪一個資料結構的全部資料,使用 for 迴圈可能需要撰寫額外的程式碼,但是若使用 for_each 可以使程式碼更簡潔。 * ### 程式註解裡頭大量存在 `@` 符號,這有何意義?你能否應用在後續的程式開發呢? * 參考資料:[Doxygen Manual: Special Commands](https://www.stack.nl/~dimitri/doxygen/manual/commands.html) * Doxyygen 是一個可以自動根據程式碼的註解內容產生說明文件的產生器,如果依照其規定的格式撰寫註解,這樣就可以省下額外多寫一份說明文件的步驟。 * 而 `@` 符號是 Doxygen 裡規定的註解開頭。 ### `tests/` 目錄底下的 unit test 的作用為何?就軟體工程來說的精神為何? ``` :~/linux-list/tests$ ls common.h list_add_tail.ok list_del.ok list_for_each_entry.o list_for_each_safe.ok list_init-local.ok list_move.ok list_splice.ok containerof list_cut_position list_entry list_for_each_entry.o.d list_init-explicit list_is_singular list_move_tail list_splice_tail containerof.c list_cut_position.c list_entry.c list_for_each_entry.ok list_init-explicit.c list_is_singular.c list_move_tail.c list_splice_tail.c containerof.o list_cut_position.o list_entry.o list_for_each_entry_safe list_init-explicit.o list_is_singular.o list_move_tail.o list_splice_tail_init containerof.o.d list_cut_position.o.d list_entry.o.d list_for_each_entry_safe.c list_init-explicit.o.d list_is_singular.o.d list_move_tail.o.d list_splice_tail_init.c containerof.ok list_cut_position.ok list_entry.ok list_for_each_entry_safe.o list_init-explicit.ok list_is_singular.ok list_move_tail.ok list_splice_tail_init.o list_add list_del list_first_entry list_for_each_entry_safe.o.d list_init-global list_last_entry list_splice list_splice_tail_init.o.d list_add.c list_del.c list_first_entry.c list_for_each_entry_safe.ok list_init-global.c list_last_entry.c list_splice.c list_splice_tail_init.ok list_add.o list_del_init list_first_entry.o list_for_each.o list_init-global.o list_last_entry.o list_splice_init list_splice_tail.o list_add.o.d list_del_init.c list_first_entry.o.d list_for_each.o.d list_init-global.o.d list_last_entry.o.d list_splice_init.c list_splice_tail.o.d list_add.ok list_del_init.o list_first_entry.ok list_for_each.ok list_init-global.ok list_last_entry.ok list_splice_init.o list_splice_tail.ok list_add_tail list_del_init.o.d list_for_each list_for_each_safe list_init-local list_move list_splice_init.o.d list_add_tail.c list_del_init.ok list_for_each.c list_for_each_safe.c list_init-local.c list_move.c list_splice_init.ok list_add_tail.o list_del.o list_for_each_entry list_for_each_safe.o list_init-local.o list_move.o list_splice.o list_add_tail.o.d list_del.o.d list_for_each_entry.c list_for_each_safe.o.d list_init-local.o.d list_move.o.d list_splice.o.d ``` * 可以看到在 test/ 目錄底下有許多小程式,且幾乎每個裡面皆有使用 `assert()` ,這時查看 `MAKEFILE` 發現在 make 的時候便會執行所有的小程式,並測試這些程式能不能正確執行完畢。 * 這些小程式的目的即檢測 `list.h` 內的 function 能不能正確執行。 * 當我們在撰寫大型專案的時候,常常會寫許多工具函式,如果沒有先確保這些工具函式的正確性,往後向上層設計整個系統的時候便會不知道是在何處出錯,是使用的工具函式的邏輯錯誤還是整個工具函式有問題 * 如果我們能夠先行利用 `tests` 的手法確認函式的正確性,更能增進往後撰寫程式的效率。 ### `tests/` 目錄的 unit test 可如何持續精進和改善呢? * 參考 `rex662624` * 可以增加變異性,並測試一些 boundary condition。或是故意測試一些會出錯的值,看程式碼是否能夠抓到並印出 warning。 ## 針對 linked list 的排序演算法 ### 對 linked list 進行排序的應用場合為何? * 因 linux 行程管理是使用 linked list 串接,所以假設我們需要查看現在是哪些行程佔用了 cpu,並比較何行程佔的比例最大,就會使用到 linked list 的排序。 ### 考慮到 linked list 在某些場合幾乎都是幾乎排序完成,這樣貿然套用 quick sort 會有什麼問題?如何確保在平均和最差的時間複雜度呢? * quick-sort 的時間複雜度,取決於一開始的 pivot 值能否有效的把資料有效切割。因為一般情形下 quick-sort 的 pivot 值通常是取第一個或是最後一個 element,所以如果對已經排序過的資料做 quick-sort,取出的 pivot 就會是最大值或最小值,此情形即為 quick-sort 的 worst-case。 * 如果能在取 pivot 的過程動一些些手腳,使 pivot 不為最大或最小,即能提昇時間複雜度,例如使用 medium-of-mediums 的方法。 ### 能否找出 Linux 核心原始程式碼裡頭,對 linked list 排序的案例? * 參閱 `rex662624` 的[工具](https://livegrep.com/search/linux)可以直接搜尋 linux 的程式碼 * 輸入指令 `path: list_sort.h` 可以看到所有使用 list_sort.h 函式庫的程式。 * 總共 `28 matches found` ### 透過 [skiplist](https://en.wikipedia.org/wiki/Skip_list) 這樣的變形,能否加速排序? ### 研讀 [How to find the kth largest element in an unsorted array of length n in O(n)?](https://stackoverflow.com/questions/251781/how-to-find-the-kth-largest-element-in-an-unsorted-array-of-length-n-in-on),比照上方連結,是否能設計出針對 linked list 有效找出第 k 大 (或小) 元素的演算法? ### [linux-list](https://github.com/sysprog21/linux-list) 裡頭 `examples` 裡頭有兩種排序實作 (insertion 和 quick sort),請簡述其原理 * insert-sort 是將資料從第一個 element 插入進一個新的 linked list ( head ),而新的 list 中是已經排序過的資料,並從頭比對資料大小做插入排序,而因為這個過程須走訪每個節點,所以時間複雜度為 $O(n^2)$。 * quick-sort 是利用 pivot 值將原本的資料序列做分割,然後分割後的 list 再遞迴呼叫 quick-sort 排序。 * 分割過程為將資料和 pivot 比大小,將比 pivot 小的是值搬到 list 頭,將比 pivot 小的值搬到 list 尾。 * 此過程是要確保在 pivot 之前資料都是小於 pivot 的,反之在 pivot 後的資料皆大於 pivot。 * 平均情形: $O(nlogn)$ * $T(n) = \frac{1}{n}\sum_{s=1}^n(T(s) + T(n-s)) + cn$ * $T(n) = \frac{1}{n}{[T(1)+T(n-1)]+[T(2)+T(n-2)]+ ... +[T(n-1)+T(1)]+[T(n)+T(0)]} + cn$ * $T(n) = \frac{2}{n}[T(1)+...+T(n-1)] + \frac{T(n)}{n} + cn$ * $nT(n) = 2[T(1)+...+T(n-1)] + T(n) + cn^2$ --------- ① * $(n-1)T(n-1) = 2[T(1)+...+T(n-2)] + T(n-1) + c(n-1)^2$ --------- ② * ① - ② 可得 $(n-1)T(n) = nT(n-1) + c(2n-1)$ * $\dfrac{T(n)}{n} = \dfrac{T(n-1)}{n-1} + c(\frac{1}{n} + \frac{1}{n-1})$ * $\dfrac{T(n)}{n} = c(\frac{1}{n} + \frac{1}{n-1} + ... + \frac{1}{2}) + c(\frac{1}{n-1} + \frac{1}{n-2} + ... + \frac{1}{1})$ * $\dfrac{T(n)}{n} = c(H_n-1) + c(H_n - \frac{1}{n})$ * $T(n) = cn(H_n-1) + c(nH_n - 1)$ * $T(n) = 2cnH_n - cn -c$ * $T(n) = 2cnlogn - cn -c$ * $T(n) = O(nlogn)$ ## 實作測試程式,批次建立包含若干大小的 linked list,隨機放入數值為介於 1 到 366 之間 (包含) 的節點 (代表 [day number](https://www.epochconverter.com/daynumbers)) ,至少應該涵蓋 10^1^, 10^2^, 10^3^, ... 10^6^ 等數量,透過前述的 insert, quick, merge sort 進行排序,分析執行時間並解讀其行為 * [檢驗程式碼](https://github.com/rwe0214/linux-list/blob/master/examples/sort_analyze.c) * 程式執行結果: ``` $ ./analyze size insert-sort quick-sort merge-sort 10^1 0.0000020971 0.0000040781 0.0000070296 10^2 0.0000390438 0.0000373689 0.0000674257 10^3 0.0018955263 0.0003176225 0.0005918828 10^4 0.2778701301 0.0058945494 0.0080722889 10^5 59.8833043724 0.2961517506 0.1274873439 ``` 可以看到 insert-sort 在時間上耗費相當的大,所以之後的比較省略 insert-sort。 ``` $ ./analyze size quick-sort merge-sort which is fast 2^1 0.0000004582 0.0000008401 quick-sort (1.83 X) 2^2 0.0000011706 0.0000018430 quick-sort (1.57 X) 2^3 0.0000029139 0.0000042060 quick-sort (1.44 X) 2^4 0.0000065810 0.0000112332 quick-sort (1.71 X) 2^5 0.0000103636 0.0000182269 quick-sort (1.76 X) 2^6 0.0000133417 0.0000234677 quick-sort (1.76 X) 2^7 0.0000292101 0.0000531666 quick-sort (1.82 X) 2^8 0.0000681951 0.0001245088 quick-sort (1.83 X) 2^9 0.0001520672 0.0002769880 quick-sort (1.82 X) 2^10 0.0003306429 0.0005956724 quick-sort (1.80 X) 2^11 0.0007398336 0.0013145460 quick-sort (1.78 X) 2^12 0.0017331422 0.0028466553 quick-sort (1.64 X) 2^13 0.0043816538 0.0063244468 quick-sort (1.44 X) 2^14 0.0121712947 0.0136517630 quick-sort (1.12 X) 2^15 0.0380133697 0.0310422692 merge-sort (1.22 X) 2^16 0.1327709013 0.0761860735 merge-sort (1.74 X) ``` 可以發現 merge-sort 在資料量大的時候表現比 quick-sort 優異許多。 ## 設計 deduplication 程式,將上述輸入中的數值挑出重複的節點並與刪除,透過統計分析,解讀執行時間分佈,以及探討 [data deduplication](https://en.wikipedia.org/wiki/Data_deduplication) 的效益