2019q1 Homework7 (skiplist) === contributed by < `Laurora` > --- ### 測驗 `1` 給定一個 circular linked list 實作如下: (檔案 `list.h`) ```cpp #ifndef INTERNAL_LIST_H #define INTERNAL_LIST_H /* circular doubly-linked list */ typedef struct __list_t { struct __list_t *prev, *next; } list_t; /* * Initialize a list to empty. Because these are circular lists, an "empty" * list is an entry where both links point to itself. This makes insertion * and removal simpler because they do not need any branches. */ static inline void list_init(list_t *list) { list->prev = list; list->next = list; } /* * Append the provided entry to the end of the list. This assumes the entry * is not in a list already because it overwrites the linked list pointers. */ static inline void list_push(list_t *list, list_t *entry) { list_t *prev = list->prev; entry->prev = prev; entry->next = list; prev->next = entry; list->prev = entry; } /* * Remove the provided entry from whichever list it is currently in. This * assumes that the entry is in a list. You do not need to provide the list * because the lists are circular, so the list's pointers will automatically * be updated if the first or last entries are removed. */ static inline void list_remove(list_t *entry) { list_t *prev = entry->prev; list_t *next = entry->next; prev->next = next; next->prev = prev; } /* * Remove and return the first entry in the list or NULL if the list is empty. */ static inline list_t *list_pop(list_t *list) { list_t *foo = list->prev; if (foo == list) return NULL; LL1 } #endif ``` 請參照上方程式註解,補完對應程式碼。 要求補完的程式碼是 `list_pop` ,由於選項是要利用 `list_remove` 實作 `list_pop` ,因此我們先看 `list_remove` 是怎麼運作的 ```clike /* * Remove the provided entry from whichever list it is currently in. This * assumes that the entry is in a list. You do not need to provide the list * because the lists are circular, so the list's pointers will automatically * be updated if the first or last entries are removed. */ static inline void list_remove(list_t *entry) { list_t *prev = entry->prev; list_t *next = entry->next; prev->next = next; next->prev = prev; } ``` * 在 list 的 `entry->prev` 新增 `*prev` ,如下圖 ```clike list_t *prev = entry->prev; ``` ![](https://i.imgur.com/srr2j3i.png) * 在 list 的 `entry->next` 新增 `*next` ```clike list_t *next = entry->next; ``` ![](https://i.imgur.com/KdGkOTn.png) * 將 `prev->next` 指向 `next` ```clike prev->next = next; ``` ![](https://i.imgur.com/YdulCts.png) * 將 `next->prev` 指向 `prev` ```clike prev->next = next; ``` ![](https://i.imgur.com/oH2pFOV.png) 這樣就完成一次 remove entry ,不過這邊似乎沒有將 entry 所指向的 `prev` 和 `next` 指向 `NULL`,實作的時候需要注意這點。 再來看 `list_pop` ```clike /* * Remove and return the first entry in the list or NULL if the list is empty. */ static inline list_t *list_pop(list_t *list) { list_t *foo = list->prev; if (foo == list) return NULL; LL1 } ``` 註解的要求是要將 list 的 first entry 移除並回傳。 `list_t *foo = list->prev;` 這裡的 `*foo` 就是 list 的 first entry,一次 `list_remove(foo)` 後就可以移除,之後 `return foo;` 即可。 ![](https://i.imgur.com/qmDJ3DX.png) 因此答案選 (e) `list_remove(foo); return foo;` --- ### 延伸問題 #### 1.研讀 2019q3 Week11 的 cache-oblivious data structures 參考資料,記錄所見所聞 [**Maximize Cache Performance with this One Weird Trick: An Introduction to Cache-Oblivious Data Structures**](https://rcoh.me/posts/cache-oblivious-datastructures/) 一般在學習演算法時,Big-O 的概念大多側重於整個程式運算的次數,是因為有一個大前提:假設從記憶體存取的時間成本與讀取的位元組 (byte) 成正比。但是實際上計算機的存取單位是資料段 (block) ,為了更貼近計算機存取的實際情況,於是就有了這個考慮硬體存取成本的模型。 * 假設在記憶體有 2 個分層: 小快速層(通常為 cache)、大慢速層(通常為 disk) * 慢速層記憶體無限大;快速層大小為 $M$ ,且無限快 * 記憶體只能以大小為 $B$ 的資料段從慢速層移動到快速層 * $M$ 和 $B$ 可為任意值 * 只分析最慢層到次慢層的傳遞,其他視為更快的傳遞,不影響結果 ![](https://rcoh.me/images/cache-oblivious-graph.png) 上圖可以發現,不同演算法在樹的大小越大而分歧越大。當樹很小,三者還看不出分歧時,表示這棵樹可以很完美的存在一個資料段中;圖中線段還有比平坦的部分在特定的 cache 大小時會發生;而樹越來越大, Cache-Oblivious 演算法的優勢就越明顯。 **藍色線段: Leveled (BFS)** ![](https://rcoh.me/images/breadthfirst.svg) * 樹中的樹字指的是元素在字組中的位置,橙色框為記憶體中的分組 * 以記憶體的角度是一個很糟糕的演算法,從根部到葉走訪,一旦一列所含元素太多,會造成每一列都會在不同的資料段中,而每一個資料段僅讀取一個元素,最後會需要讀取 $\log_2N$ 個資料段。 **橙色線段: Preorder (DFS)** ![](https://rcoh.me/images/depthfirst.svg) * 最佳情況:沿最左邊走訪(橘色圈部分)比 BFS 好,僅需讀取 1 個資料段 * 最差情況:沿最右邊走訪仍需讀取 $\log_2N$ 個資料段。 **綠色線段: Recursive Block Approach** ![](https://rcoh.me/images/van-layout.svg) * 綠色框為 block,橙色框為 superblock * 所有的 block 之間==連續==,所有的 superblock 之間也==連續== * 當讀取的 block 越大,相較之前演算法,讀取的階層會少的越多 * 例:若讀取一個大小為 $B$ 的 block,原來預期會讀取 $\log_2B$ 層才能走訪根到葉,而整個樹有 $\log_2N$ 層,透過 Recursive Block Approach 只需要存取 $\log_BN$ 個 block。 * $\frac{\log_2N}{\log_2B}=\log_BN$ [**Memory Hierarchy Models** ](https://www.youtube.com/watch?v=V3omVLzI0WE) **extended-memory model (I/O model, Disk Access Model, cache-aware model)** ![](https://i.imgur.com/Jc5CErB.png) 1. 記憶體階層的記憶體大小 (size) 與傳遞時間 (latency) 通常隨深度(階層距離 cpu 越遠)呈指數成長 2. 計算 latency: 最後一層與倒數第二層之間的 memory transfer 即可,在這裡是 disk 和 cache 之間的傳遞 (假設存取 cache 為無限快) 3. $\frac{ number\ of\ cell\ probes}{B}\leqq$ number of memory transfer $\leqq$ RAM running time > RAM running time: 在 RAM 中每一次運算都當作一次 memory transfer > number of cell probes: 在一次運算中需要讀取幾個字組 4. 計算 memory transfer - [Scanning](https://youtu.be/V3omVLzI0WE?t=500): $O(\lceil\frac{N}{B}\rceil)$ - [Search trees](https://youtu.be/V3omVLzI0WE?t=553) (B-tree): $O(\log_{B+1}N)$、$\Omega(\log_{B+1}N)$ - [Sorting](https://youtu.be/V3omVLzI0WE?t=920): $O(\frac{N}{B}\log_{\frac{M}{B}}\frac{N}{B})$ > 一般 sorting 的最佳解通常是$O(Nlog_2N)$ 在這個模型可以看到明顯的進步 - [Permuting](https://youtu.be/V3omVLzI0WE?t=1041):$O(min\{N,\frac{N}{B}\log_{\frac{N}{B}}\frac{N}{B}\})$ - [Buffer tree](https://youtu.be/V3omVLzI0WE?t=1174): $O(\frac{1}{B}\log_{\frac{M}{B}}\frac{N}{B})$、尋找最小值: $O(\phi)$ > 平攤 memory transfer,用於 delayed queries 和 batch update,此時尋找最小值: $O(\phi)$ **cache-oblivious model** 較新的 Memory Hierarchy Model ,與 extended-memory model 最大的區別是,這個模型的 $M$(cache size) 和 $B$(block size) 未知,因此不能手動計算要讀、寫的 block ,cache 滿的時候也不能決定哪一個 block 要優先被踢出去 1. 演算法 - 與 RAM 演算法相似 - cache 替換可利用 LRU(least resent used)、FIFO(first-in-first-out)等算法可接近最佳解 - 必須適用於各種大小的 $B$、$M$,也就是在每一個記憶體階層間的 memory transfer 都要找的到最佳解 2. 計算 memory transfer * [Scanning](https://youtu.be/V3omVLzI0WE?t=1849): $O(\lceil\frac{N}{B}\rceil)$ - [Search trees](https://youtu.be/V3omVLzI0WE?t=1867) (B-tree): $O(\log_{B+1}N)$、$\Omega(\log_{B+1}N)$ - [Sorting](https://youtu.be/V3omVLzI0WE?t=1906): $O(\frac{N}{B}\log_{\frac{M}{B}}\frac{N}{B})$ - [Permuting](https://youtu.be/V3omVLzI0WE?t=1041): 找不到最小值。因為無法定義 B 和 M 的關係。 - [Priority Queue](https://youtu.be/V3omVLzI0WE?t=2067): $O(\frac{1}{B}\log_{\frac{M}{B}}\frac{N}{B})$ > tall cache assumption: cache 存取了 M 個物件,其中 $M=\Omega(B^{1+\epsilon})$ 3. cache-oblibious static search trees * 應用平衡二元搜尋樹 * [van Emde Boas 排序](https://youtu.be/V3omVLzI0WE?t=2388) * 將樹從正中間的階層切一半 ![](https://i.imgur.com/UI6XbvN.png) * 用遞迴的方式將$\sqrt N$ 的三角形分離出來 (Divide) * 再將每個三角形連接起來 (Conquer) * 分析:在知道 B 的情況 ![](https://i.imgur.com/r9cvvv8.png) * 遞迴條件設定為三角形尺寸 $\lt B$ 就停止 * 切割完後可以保證存取每個三角形最多只需要 $2$ 次 memeory transfer * 小三角形的高度為 $\theta(\lg B)\in (\frac{1}{2}\lg B, \lg B)$ * 從 root 到 leaf 最多需要經過 $\frac{\lg N}{\frac{1}{2}\lg B}$ 個小三角形 * 所以memory transfer 次數 $\leq 4\log_{B+1}N$ [**Skip list**](https://en.wikipedia.org/wiki/Skip_list) 一般的 linked list 搜尋時必須從第一個開始,按照順序一個一個搜尋,因此不論是插入、刪除、搜尋的時間複雜度皆為 $O(\log N)$,而 skip list 可以讓 n 個元素的平均時間複雜度收斂到 $O(\log N)$ **建立 skip list** 1. 最底層 (level 1) 是包含所有元素的 linked list 2. 保留第一個和最後一個元素,從其他元素中隨機選出一些元素形成新的一層 linked list 3. 為剛選出的每個元素新增指標,指向下一層和自己相等的元素 4. 重複 2、3 直到不再能選擇出除了第一個和最後一個元素以外的元素 **特徵** * 第一層包含所有的元素 * 每一層都是一個有序的 linked list * skip list 是隨機生成,所以即使元素完全相同的 linked list ,最終的生成的 skip list 有可能不一樣 * 如果元素 x 出現在第 i 層,則所有比 i 小的層都包含 x **在 skip list 插入元素** ![](https://upload.wikimedia.org/wikipedia/commons/2/2c/Skip_list_add_element-en.gif) --- #### 2.依據之前給定的 Linux 核心 linked list 實作,開發 cache-oblivious 的版本,並證實其效益; --- #### 3.實作 Skip list,並且比照之前的 unit test 提供對應的測試、效能分析,還有改善機制