sysprog
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Write
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Help
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Write
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    --- title: 2020 年秋季 進階電腦系統理論與實作課程作業 —— dict description: 檢驗學員對 bitwise 操作和記憶體管理的認知 --- # I04: dict > 主講人: [jserv](http://wiki.csie.ncku.edu.tw/User/jserv) / 課程討論區: [2020 年系統軟體課程](https://www.facebook.com/groups/system.software2020/) :mega: 返回「[進階電腦系統理論與實作](http://wiki.csie.ncku.edu.tw/sysprog/schedule)」課程進度表 ## :memo: 預期目標 * 學習 [ternary search tree](https://en.wikipedia.org/wiki/Ternary_search_tree) 作為 [auto-complete](https://en.wikipedia.org/wiki/Autocomplete) 和 prefix search 的實作機制 * 學習 [Bloom filter](https://en.wikipedia.org/wiki/Bloom_filter) 和 [ternary search tree](https://en.wikipedia.org/wiki/Ternary_search_tree) 的整合議題 * bit-wise operation 的實踐 * 設計效能評比 (benchmark) 的程式框架 * 學習 GNU make 和 `Makefile` 撰寫 * 透過 Valgrind 動態程式分析工具及其模組,排除記憶體錯誤和掌握執行時期的記憶體開銷,從而更好掌握記憶體管理 * 學習 [perf](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial) 效能分析工具 :::info :question: [Google Translate](https://translate.google.com/) 一類的線上辭典不僅能夠快速查詢詞彙,還可自動補完和提示相近詞彙,你知道背後的資料結構及數學原理嗎? ::: ## :dart: Trie 和 Ternary search tree Binary Search Tree (BST) 查詢與建立搜尋表的時間複雜度是 O(log~2~n),但是在最糟的情況下時間複雜度可能轉為 O(n),除了要儲存的字串外不須另外分配空間。 [Trie](https://en.wikipedia.org/wiki/Trie) (亦稱 prefix-tree 或字典樹) 的時間複雜度是 O(n),能夠實現 binary search tree 難以做到的 prefix-search,缺點是需要保存大量的空指標,空間開銷較大; [Ternary Search Tree](https://en.wikipedia.org/wiki/Ternary_search_tree) 是種 trie 資料結構,結合 trie 的時間效率和 BST 的空間效率優點。 * 每個節點 (node) 最多有三個子分支,以及一個 Key; * Key 用來記錄 Keys 中的其中一個字元 (包括 EndOfString); * low: 用以指向比目前節點的 Key 值小的節點; * equal: 用來指向與目前節點的 Key 值一樣大的節點; * high: 用來指向比目前節點的 Key 值大的節點; Ternary Search Tree 的應用: * spell check: Ternary Search Tree 可以當作字典來保存所有的單詞。一旦在編輯器中輸入單詞,可在 Ternary Search Tree 中並行搜尋單詞以檢查正確的拼寫方式; * Auto-completion: 使用搜尋引擎進行時,提供自動完成(Auto-complete)功能,讓使用者更容易查詢相關資訊 對於 Web 應用程式來說,自動完成的繁重處理工作絕大部分要交給伺服器。許多時候,自動完成不適宜一下子全都下載到客戶端,TST 是保存在伺服器上,客戶端把使用者已輸入的開頭詞彙提交到伺服器進行查詢,然後伺服器依據 TST 獲取相對應的資料列表,最後把候選的資料列表返回給客戶端。 ![](https://images0.cnblogs.com/blog/183411/201212/30214224-1fdcd259d4fa4b398d2a5a1fa50edb30.png) * [Autocomplete using a ternary search tree](https://github.com/jdrnd/autocomplete-ternary) * [Ternary Search Tree 視覺化](https://www.cs.usfca.edu/~galles/visualization/TST.html) * 以 `ab`, `abs`, `absolute` 等字串逐次輸入並觀察 * 整合案例: [Phonebook](https://github.com/raestio/Phone_book): adds new contacts and effectively search for contacts via ternary search tree ### :tangerine: Trie [trie](https://en.wikipedia.org/wiki/Trie) 與 binary search tree 不同在於,不把整個字串包在同一個節點中 ,而是根據節點的位置決定代表的字串為何。也就是一個看結果(最終節點),一個看過程 (經過的節點)。因此利用 trie 可快速找出有相同 prefix 的字串。以下舉例示範 trie 如何儲存資料。 延伸閱讀: [字典樹 Trie](http://pisces.ck.tp.edu.tw/~peng/index.php?action=showfile&file=f743c2923f8170798f62a585257fdd8436cd73b6d) 以下逐步建構 trie: :::info 一開始,trie tree 起始於一個空的 root: ==◎== ::: ``` ◎ ``` :::info 先插入一個字串 `and` 1. 首先將 and 拆解成 ==a==, ==n==, ==d== 2. 因為 a 第一次出現,所以在 root 建立一個新 children node,內含值為 a 3. 接下來從 node "a" 出發,剩下 n, d 4. n 也是第一次出現,因此在 a 建立一個新 children node,內含值為 n 5. d 比照 4 作法。 ::: ``` ◎ / a / n / d ``` :::info 再插入一個字串 `ant` 1. ant 拆解成 ==a==, ==n==, ==t==,從 root 出發 2. node "a" 已經存在,因此不需要自 root 創造新的 children 3. 再來看 n,node "a" 往 children 看發現 node "n" 已存在,因此也不創造 children 4. 再來看 t 發現 n 的 children 中沒有此 node,因此創造新 children "t" ::: ``` ◎ / a / n / \ d t ``` :::info 隨後插入 ==bear== bea 會產生像這樣子的 tree 1. 有內含數值者就是有字串存在的 node 2. 因此搜尋 "ant" 會返回 0,表示 trie 內有此字串 4. 搜尋 "be" 會返回 NULL,因為在 node "e" 無有效數值 ::: ``` ◎ / \ a b / \ n e / \ \ d t a (3) (0) (1) \ r (2) ``` 以上,我們注意到 trie 中,若有儲存的字串有許多相同的 prefix ,那麼 trie 可以節省大量儲存空間,考慮到 C-style string 的設計,因為 Trie 中每個字串都是經由一個字元接著另一個字元的形式來儲存,所以具有相同 prefix 的部份都會有共同的 node。 相對的,若是資料非常大量,而且幾乎沒有相同的 prefix,將會非常浪費儲存空間。因為 trie 在記憶體的儲存方式其實是像下圖這樣,每個 node 建立時,都必須同時創造 26 個值為 NULL 的子節點。 :::success 下圖的 `universal set = { a, b, c, d, e}`,所以只創造 5 個 ::: ![](https://i.imgur.com/S7VQnPf.png) ### :taurus: Ternary search tree Trie 能夠實現 prefix search 但會有大量空指標 - 每個 node 需要 $|alphabet set|$ 個指標 引入 [Ternary search tree](https://en.wikipedia.org/wiki/Ternary_search_tree) (TST),可望解決上述記體開銷的問題。這種樹的每個節點最多只會有 3 個 children,分別代表小於,等於,大於。在 [dict](https://github.com/sysprog21/dict) 內建的 TST 實作中的 `tst.c` 可看到資料結構如下: ```cpp typedef struct tst_node { char key; /* char key for node (null for node with string) */ unsigned refcnt; /* refcnt tracks occurrence of word (for delete) */ struct tst_node *lokid; /* ternary low child pointer */ struct tst_node *eqkid; /* ternary equal child pointer */ struct tst_node *hikid; /* ternary high child pointer */ } tst_node; ``` 假設目前搜尋到的 prefix = x 被搜尋的 node key = cur 則 - if `x == cur` - x = next prefix - node = node->eqkid - 繼續比對下個 prefix - if `x > cur` - node = node->hikid - 往右搜尋 - if `x < cur` - node = node->lokid - 往左搜尋 而其中 `key` 為 0 時,代表 `struct tst_node *eqkid`,不是指向下個 node,而是該路徑所代表的字串。 以下範例解釋 Ternary search tree 的運作。 :::info 插入一個字串 `and` 與 trie 概念相同,但是在 root 是直接擺 ==a==,而不需要一個 null ::: ``` a | n | d ``` :::info 接著插入單字 `ant` 1. 我們先比對 ==a==,發現 a 和 root 的字母相同(a=a) 2. 因此 pointer 往下,來到 ==n== 節點,發現 n 節點和 ant 的第二個字母相同(n=n) 3. 因此 pointer 再往下,來到 d 節點,發現 d 節點和 ant 的第三個字母不同,而且 **t > d** 4. 因此 d 節點的 right child 成為 ==t==,成功將 ant 放入原本的 TST 中 ::: ``` a | n | d- | t ``` :::info 接著插入單字 `age` 1. 我們先比對 ==a== ,發現 a 和 root 的字母相同(a=a) 2. 因此 pointer 往下,來到 n 節點,發現 n 節點和 age 的第二個字母不同,而且 **g < n** 3. 因此 n 節點的 left child 成為 ==g==,成功將 age 放入原本的 TST 中 ::: ``` a | -n | | g d- | | e t ``` [Ternary Search Tree ](https://www.cs.usfca.edu/~galles/visualization/TST.html) 此網址提供了透過動畫呈現,展示如何加入與刪除給定字串的流程。 我們可發現 TST 幾種特性: 1. 解決 trie tree 中大量佔用記憶體的情況,因為 TST 中,每個 node 最多只會有 3 個 child 2. 但尋找時間在某些狀況中,會比 trie 慢,這是因為 **TST 插入的順序會影響比較的次數**。 考慮以下兩種插入順序: * 前者用 `aa` $\to$ `ac` $\to$ `ae` $\to$ `af` $\to$ `ag` $\to$ `ah` 的順序 * 後者是 `af` $\to$ `ae` $\to$ `ac` $\to$ `aa` $\to$ `ag` $\to$ `ah` 的順序 試想,如果我們要尋找 `ah`,那麼前者要比較 6 次,後者只要比較 3 次。而若是建立 trie ,只需要 2 次。 - [ ] 用 `aa` $\to$ `ac` $\to$ `ae` $\to$ `af` $\to$ `ag` $\to$ `ah` 的順序插入 ![](https://i.imgur.com/2ZZy7ym.png) - [ ] 用 `af` $\to$ `ae` $\to$ `ac` $\to$`aa` $\to$ `ag` $\to$ `ah` 的順序插入 ![](https://i.imgur.com/fPkOILf.png) ## :taurus: [ternary search tree](https://en.wikipedia.org/wiki/Ternary_search_tree) 的實作程式碼 * 取得 [dict](https://github.com/sysprog21/dict) 程式碼,編譯並測試 ```shell $ git clone https://github.com/sysprog21/dict $ cd dict $ make $ ./test_common CPY ``` * 預期會得到以下執行畫面: ``` ternary_tree, loaded 259112 words in 0.151270 sec Commands: a add word to the tree f find word in tree s search words matching prefix d delete word from the tree q quit, freeing all data choice: ``` * 按下 `f` 隨後按下 Enter 鍵,會得到 `find word in tree:` 的提示畫面,輸入 `Taiwan` (記得按 Enter),預期會得到以下訊息: ``` find word in tree: Taiwan found Taiwan in 0.000002 sec. ``` * 當再次回到選單時,按下 `s` 隨後按下 Enter 鍵,會得到 `find words matching prefix (at least 1 char):` 的提示訊息,輸入 `Tain`,預期會得到以下訊息: ``` Tain - searched prefix in 0.000011 sec suggest[0] : Tain, suggest[1] : Tainan, suggest[2] : Taino, suggest[3] : Tainter suggest[4] : Taintrux, ``` * 不難發現 prefix-search 的功能,可找出給定開頭字串對應資料庫裡頭的有效組合,你可以用 `T` 來當作輸入,可得到[世界 9 萬多個城市](https://github.com/sysprog21/dict/blob/master/cities.txt)裡頭,以 `T` 開頭的有效名稱 * 至於選單裡頭的 `a` 和 `d` 就由你去探索具體作用 上述的 `./test_common CPY` 表示 `CPY` 也就是 COPY 機制,與其相對的是 `REF`,即 reference 機制,對應的命令列為 `./test_common REF`。 程式碼中的 copy 與 reference 機制: * copy 機制 (`CPY`): 傳入 `tst_ins_del` 的字串地址所表示的值**隨時會被覆寫**,因此 `eqkid` 需要 `malloc` 新空間將其 **copy** 存起來; * reference 機制 (`REF`): 傳進來的字串地址所表示的值**一直屬於該字串**,以此可將其當作 **reference** 直接存起來,不用透過 `malloc` 新空間。 執行以下命令,比較 copy 與 reference 機制的效率: ```shell $ make bench ``` 參考輸出: ``` COPY mechanism test_common => choice: find words matching prefix (at least 1 char): Tai - searched prefix in 0.000183 sec REFERENCE mechanism test_common => choice: find words matching prefix (at least 1 char): Tai - searched prefix in 0.000262 sec ``` 預設動作是 `s Tai` (搜尋 `Tai` 開頭的字串),可改為搜尋 `T` 開頭的字串,如下: ```shell $ make bench TEST_DATA="s T" ``` 請留意以上機制的行為,後續我們會探討效能表現。 ## 何謂 Bloom Filter Bloom filter 利用 hash function,在不用走訪全部元素的前提,**預測**特定字串是否存於資料結構中。因此時間複雜度是 $O(1)$,而非傳統逐一尋訪的 $O(n)$ 。 * n-bits 的 table * k 個 hash fucntion: h~1~ 、 h~2~ ... h~k~ * 每當要加入新字串 s 時,會將 s 透過這 k 個 hash function 各自轉換為 table index (範圍: `0` 到 `n - 1`) ,所以有 k 個 hash function ,就該有 k 個 index ,然後將該 `table[index]` 上的 bit set * 下次若要檢查該字串 s 是否存在時,只需要再跑一次那 k 個 hash function ,並檢查是否所有的 k 個 bit 都是 set 即可 ![](https://i.imgur.com/qa6qAzi.png) * 動畫: [Cube Drone - Bloom Filters](https://www.youtube.com/watch?v=-SuTGoFYjZs) 但這做法**存在錯誤率**,例如原本沒有在資料結構中的字串 s1 經過 hash 轉換後得出的 bit 的位置和另一個存在於資料結構中的字串 s2 經過 hash 之後的結果相同,如此一來,先前不存在的 s1 便會被認為存在,這就是 [false positive](https://en.wikipedia.org/wiki/False_positives_and_false_negatives) (指進行實用測試後,測試結果有機會不能反映出真正的面貌或狀況)。 Bloom filter 一類手法的應用很常見。例如在社群網站 Facebook,在搜尋欄鍵入名字時,能夠在 [20 億個註冊使用者](https://www.bnext.com.tw/article/45104/facebook-maus-surpasses-2-billion) (2017 年統計資料) 中很快的找到結果,甚至是根據與使用者的關聯度排序。 延伸閱讀: * 影片: [Esoteric Data Structures and Where to Find Them](https://youtu.be/-8UZhDjgeZU?t=607) * [Bloom Filter 影片解說](https://www.youtube.com/watch?v=bEmBh1HtYrw) - [ ] Bloom Filter 實作方式 首先,建立一個 n bits 的 table,並將每個 bit 初始化為 0。 我們將所有的字串構成的集合 (set) 表示為 S = { x~1~, x~2~, x~3~, ... ,x~n~ },Bloom Filter 會使用 k 個不同的 hash function,每個 hash function 轉換後的範圍都是 0 到 n-1 (為了能夠對應上面建立的 n bits table)。而對每個 S 內的 element x~i~,都需要經過 k 個 hash function,一一轉換成 k 個 index。轉換過後,table 上的這 k 個 index 的值就必須設定為 `1`。 > 注意: 可能會有同一個 index 被多次設定為 `1` 的狀況 Bloom Filter 這樣的機制,存在一定的錯誤率。若今天想要找一個字串 x 在不在 S 中,這樣的資料結構只能保證某個 x~1~ 一定不在 S 中,但沒辦法 100% 確定某個 x~2~一定在 S 中。因為會有誤判 (false positive ) 的可能。 此外,**資料只能夠新增,而不能夠刪除**,試想今天有兩個字串 x~1~, x~2~ 經過某個 hash function h~i~ 轉換後的結果 h~i~(x~1~) = h~i~(x~2~),若今天要刪除 x~1~ 而把 table 中 set 的 1 改為 0,豈不是連 x~2~ 都受到影響? - [ ] Bloom Filter 錯誤率計算 首先假設我們的所有字串集合 S 裡面有 n 個字串, hash 總共有 k 個,Bloom Filter 的 table 總共 m bits。我們會判斷一個字串存在於 S 內,是看經過轉換後的每個 bits 都被 set 了,我們就會說可能這個字串在 S 內。但試想若是其實這個字串不在 S 內,但是其他的 a b c 等等字串經過轉換後的 index ,剛好涵蓋了我們的目標字串轉換後的 index,就造成了誤判這個字串在 S 內的情況。 如上述,Bloom Filter 存有錯誤機率,程式開發應顧及回報錯誤機率給使用者,以下分析錯誤率。 當我們把 S 內的所有字串,每一個由 k 個 hash 轉換成 index 並把 `table[index]` 設為 1,而全部轉完後, table 中某個 bits 仍然是 0 的機率是 $(1-\dfrac{1}{m})^{kn}$ 。 其中 $(1-\dfrac{1}{m})$ 是每次 hash 轉換後,table 的某個 bit 仍然是 0 的機率。因為我們把 hash 轉換後到每個 index (而這個 index 會被 set) 的機率視為相等 (每人 $\dfrac{1}{m}$),所以用 1 減掉即為不會被 set 的機率。我們總共需要做 kn 次 hash,所以就得到答案$(1-\dfrac{1}{m})^{kn}$。 由 $(1-\dfrac{1}{m})^{m}≈e^{-1}$ 特性,$(1-\dfrac{1}{m})^{kn}=((1-\dfrac{1}{m})^{m})^{\frac{kn}{m}}≈(e^{-1})^{\frac{kn}{m}}≈e^{-\frac{kn}{m}}$ 以上為全部的字串轉完後某個 bit 還沒有被 set 的機率。 因此誤判的機率等同於全部經由 k 個 hash 轉完後的 k bit 已經被其他人 set 的機率: 轉完後某個 bit 被 set 機率是: $1-e^{-\frac{kn}{m}}$,因此某 k bit 被 set 機率為: ==$(1-e^{-\frac{kn}{m}})^k$== 延伸閱讀: * [Bloom Filter](https://blog.csdn.net/jiaomeng/article/details/1495500) ### 如何選參數值 * k: 多少個不同 hash * m: table size 的大小 **如何選 k 值?** 我們必須讓錯誤率是最小值,因此必須讓 $(1-e^{-\frac{kn}{m}})^k$ 的值是最小。先把原式改寫成 $(e^{k\ ln(1-e^{-\frac{kn}{m}})})$,我們只要使 ${k\ ln(1-e^{-\frac{kn}{m}})}$ 最小,原式就是最小值。可以看出當 $e^{-\frac{kn}{m}}=\dfrac{1}{2}$ 時,會達到最小值。因此 ==$k=ln2\dfrac{m}{n}$== 即能達到最小值。 [Bloom Filter calculator](https://hur.st/bloomfilter/?n=5000&p=3&m=&k=45) 和 [Bloom Filter calculator 2](https://krisives.github.io/bloom-calculator/) 這兩個網站可以透過設定自己要的錯誤率與資料的多寡,得到 `k` 和 `m` 值。 ## Bloom Filter error rate 視覺化 以 hash function的數量、table size 的大小做為橫軸縱軸,以錯誤率做為深淺。而畫面中紫色的線為 $y=ln2\cdot\frac{x}{93827}$,其中93827 為字典中 item 數量。 ![](https://i.imgur.com/6SKmS2x.png) 我們想要最小的空間及執行速度,並符合一定的錯誤率,於是期望的數值在可行域的左下角。 為了讓圖片不明顯的部分,能被看得清楚,將圖片轉為灰階後做直方圖均衡化 ![](https://i.imgur.com/DxibxrA.png) ![](https://i.imgur.com/pXWBJuw.png) 把此圖視為高線圖,可以發現山谷都在 $k=ln2\frac{m}{n}$上,而在這裡實際的意義就是錯誤率最小值發生在 $k=ln2\frac{m}{n}$與推導的結果相符。透過開四次方根把錯誤率之間的差距加大,再製圖,可更明顯看出上述的內容。 ![](https://i.imgur.com/53VgivI.png) ## 在 prefix search 導入 Bloom filter 機制 [參考的字典檔案](https://stackoverflow.com/questions/4456446/dictionary-text-file) 裡面總共有 36922 個詞彙。 經由上述運算得知,當我們容許 5% 錯誤 (=0.05) 時: * table size = 230217 * k = 4 先用原來的 `cities.txt` 檔案做測試,以驗證估程式。 首先,這裡選用了兩種 hash function:`jenkins` 和`djb2`,另外 [murmur3 hash](https://en.wikipedia.org/wiki/MurmurHash) 也是個不錯的選擇,這裡主要是選用夠 uniform 的即可。 ```cpp unsigned int djb2(const void *_str) { const char *str = _str; unsigned int hash = 5381; char c; while ((c = *str++)) hash = ((hash << 5) + hash) + c; return hash; } unsigned int jenkins(const void *_str) { const char *key = _str; unsigned int hash=0; while (*key) { hash += *key; hash += (hash << 10); hash ^= (hash >> 6); key++; } hash += (hash << 3); hash ^= (hash >> 11); hash += (hash << 15); return hash; } ``` 再來宣告我們的 Bloom filter 主體,這裡用一個結構體涵蓋以下: * `func` 是用來把我們的 hash function 做成一個 list 串起來,使用這個方法的用意是方便擴充。若我們今天要加進第三個 hash function,只要串進 list ,後面在實做時就會走訪 list 內的所有 function。 * `bits` 是我們的 filter 主體,也就是 table 。 size 則是 table 的大小。 ```cpp typedef unsigned int (*hash_function)(const void *data); typedef struct bloom_filter *bloom_t; struct bloom_hash { hash_function func; struct bloom_hash *next; }; struct bloom_filter { struct bloom_hash *func; void *bits; size_t size; }; ``` 主要的程式碼如下。 首先在 `bloom_create` 的部份,可以看到我們把兩個 hash function 透過`bloom_add_hash` 加入結構中。 再來 `bloom_add` 裡面是透過對目標字串 `item` 套用 hash ,並把 hash 產生的結果對 `filter->size` 取餘數 (第 19 行),這樣才會符合 table size 。最後第 19 行把產生的 bit 設定為 1 。 最後`bloom_test`就是去檢驗使用者輸入的 `item` 是不是在結構當中。與 add大致上都相等,只是把 bit 設定為 1 的部份改為「檢驗是不是 1」,若是任一個 bit 不是 1 ,我們就可以說這個結構裡面沒有這個字串。 這樣的實做,讓我們可以在 bloom_create 初始化完,接下來要加入字串就呼叫 bloom_add ,要檢驗字串就呼叫 bloom_test,不需要其他的操作。 ```cpp= bloom_t bloom_create(size_t size) { bloom_t res = calloc(1, sizeof(struct bloom_filter)); res->size = size; res->bits = malloc(size); // 加入 hash function bloom_add_hash(res, djb2); bloom_add_hash(res, jenkins); return res; } void bloom_add(bloom_t filter, const void *item) { struct bloom_hash *h = filter->func; uint8_t *bits = filter->bits; while (h) { unsigned int hash = h->func(item); hash %= filter->size; bits[hash] = 1; h = h->next; } } bool bloom_test(bloom_t filter, const void *item) { struct bloom_hash *h = filter->func; uint8_t *bits = filter->bits; while (h) { unsigned int hash = h->func(item); hash %= filter->size; if (!(bits[hash])) { return false; } h = h->next; } return true; } ``` 再來是 `main` 改的部份,這裡修改 `test_common.c` 檔案。需要更動的地方是 add 和 find 功能。 首先 find 功能修改: ```cpp= t1 = tvgetf(); // 用 bloom filter 判斷是否在 tree 內 if (bloom_test(bloom, word) == 1) { // version1: bloom filter 偵測到在裡面就不走下去 t2 = tvgetf(); printf(" Bloomfilter found %s in %.6f sec.\n", word, t2 - t1); printf(" Probability of false positives:%lf\n", pow(1 - exp(-(double)HashNumber / (double) ((double) TableSize /(double) idx)), HashNumber)); // version2: loom filter 偵測到在裡面,就去走 tree (防止偵測錯誤) t1 = tvgetf(); res = tst_search(root, word); t2 = tvgetf(); if (res) printf(" ----------\n Tree found %s in %.6f sec.\n", (char *) res, t2 - t1); else printf(" ----------\n %s not found by tree.\n", word); } else printf(" %s not found by bloom filter.\n", word); break; ``` 由以上可見,第 3 行會先用 bloom filter 去判斷是否在結構內,若不存在的話就不用走訪。如果在的話,程式印出 bloom filter 找到的時間,以及錯誤機率,隨後走訪節點,判斷是否在 tree 裡面。 上述參數的配置為: * TableSize = 5000000 (=五百萬) * hashNumber = 2 (總共兩個 hash) * 用原來的 `cities.txt` 資料量為 206849 計算出理論錯誤率約為 `0.009693`,也就是不到 `1%`。 以下是 add 的部份: ```cpp if (bloom_test(bloom, Top) == 1) // 已被 Bloom filter 偵測存在,不要走 tree res = NULL; else { // 否則就走訪 tree,並加入 bloom filter bloom_add(bloom,Top); res = tst_ins_del(&root, &p, INS, REF); } t2 = tvgetf(); if (res) { idx++; Top += (strlen(Top) + 1); printf(" %s - inserted in %.10f sec. (%d words in tree)\n",(char *) res, t2 - t1, idx); } else printf(" %s - already exists in list.\n", (char *) res); if (argc > 1 && strcmp(argv[1], "--bench") == 0) // a for auto goto quit; break; ``` 以上程式碼可以看到,第 1 行的部份會先偵測要加入的字串是否在結構中,若是回傳 1 表示 bloom filter 偵測在結構中,已經重複了。因此會把 res 設為 null,這樣下方第 9 行的判斷自然就不會成立。如果偵測字串不在結構中,會跑進第 3 行的 else,加入結構還有 Bloom filter 當中。 ### 情境 1: 搜尋字串全部在字典內 > 以 `cities.txt` 作為輸入搜尋 ![](https://i.imgur.com/PxjAxi5.png) 從結果中可以發現: 1. tst_search 重複測試 600 次的搜尋時間大致上相同 2. 因為全部的搜尋字串都在字典內,因此 bloom filter 進行的預測時間是浪費的,隨著 table size、雜湊函式數量提高,浪費的時間愈多 ### 情境 2: 搜尋字串在第一個字元即可判斷不在字典內 ![](https://i.imgur.com/eJiaj8f.png) 1. 加上 bloom filter 後的執行時間為 L 型: 在這個情境中,若 bloom filter 預測錯誤,進行一個搜尋所花的時間為 bloom filter + tst_search 的時間;若是預測正確則是只需要花 bloom filter 的時間,因此預測的錯誤率愈低搜尋所需要的時間愈短。 已知: ==bloom filter 預測的錯誤率計算公式為 $(1-e^{-\frac{kn}{m}})^k$== **其中 k為 hash function 數量、m 為 table size、n 為字串數量** 依據上述公式,當 k 固定時,m 增加錯誤率會逐漸下降,繪製出的圖形也是呈現 L 形與輸出結果相符。 2. 把這張圖 bloom filter 的部分與 bloom filter錯誤率的 heat map 一起看,由於錯誤率與執行時間成正比,這張圖 bloom filter 的部分,正是一列一列拆開的 heat map。 3. 因為在第一個字元就可以判斷出字串不在字典中,tst_search 進行搜尋的時間反而少於進行 hash 的時間。 ### 情境 3: 搜尋到最後一個字元才發現字串不在字典內 ![](https://i.imgur.com/7OTUiPM.png) 1. 第三個 L 之後加入 bloom filter 的搜尋時間全在不加 bloom filter 之上,這與==搜尋的字串長度==有關,雖然搜尋字串全部不在字典中且需要到最後一個字元才能發現,但因為字串長度不長,當雜湊函式的數量大於 3 個之後,進行雜湊運算與預測的時間反而大於 tst_search,因此在輸入的搜尋字串長度與本次情境相近時,bloom filter 的雜湊函式數量小於 3 個才可以使效率提高。 ## Bloom filter 空間浪費改善 原本整個 table 都把 byte 當 bit 來用,浪費 $\dfrac{7}{8}$ 的 table 空間 ```cpp void bloom_add(bloom_t filter, const void *item) { struct bloom_hash *h = filter->func; uint8_t *bits = filter->bits; while (h) { unsigned int hash = h->func(item); hash %= filter->size; bits[hash] = 1; // 每個 uint8_t 不是 1 就是 0 h = h->next; } } ``` 做以下改善: ```cpp bloom_t bloom_create(size_t size) { bloom_t res = calloc(1, sizeof(struct bloom_filter)); res->size = size; //res->bits = malloc(size + 7); res->bits = calloc((size + 7) >> 3, 1); bloom_add_hash(res, djb2); bloom_add_hash(res, jenkins); return res; } ``` 為了改善空間浪費,在 `bloom_create` 內做以上更改 - 因為從 byte-level 改成 bit-level 所以 res->bits 只需要 malloc 1/8 的空間 - 其中 `res->bits = calloc((size + 7) >> 3, 1)` 先對齊 1-byte (round up) 再 shift 3 bits - 這邊不用 `(size + 7) & -8` 因為會 shift 掉 - `res->bits = calloc((size + 7) >> 3, 1)` 改成 `calloc` 因為要確保原本 memory content 為 0 ```cpp void bloom_add(bloom_t filter, const void *item) { struct bloom_hash *h = filter->func; uint8_t *bits = filter->bits; while (h) { unsigned int hash = h->func(item); hash %= filter->size; //bits[hash] = 1; bits[hash >> 3] |= 0x40 >> (hash & 7); h = h->next; } } ``` ```cpp bool bloom_test(bloom_t filter, const void *item) { struct bloom_hash *h = filter->func; uint8_t *bits = filter->bits; while (h) { unsigned int hash = h->func(item); hash %= filter->size; if (!(bits[hash >> 3] & (0x40 >> (hash & 7)))) { return false; } h = h->next; } return true; } ``` 在以上二個函式中,因為 table 的儲存已經改成 bit-level 所以 - `bits[hash >> 3]` 先找到存放位置在第幾個 byte - `0x40 >> (hash & 7)` 再找到要放在第幾個 bit - 在 little-endian 往右 shift 會往低位址移動,但是不影響結果 - 也可寫成 `1 << (hash & 7)`,只是前者邏輯上較直觀 ### 分飾多角的 eqkid 資料結構: ```cpp typedef struct tst_node { char key; /* char key for node (null for node with string) */ unsigned refcnt; /* refcnt tracks occurrence of word (for delete) */ struct tst_node *lokid; /* ternary low child pointer */ struct tst_node *eqkid; /* ternary equal child pointer */ /** 其實它分飾多角 **/ struct tst_node *hikid; /* ternary high child pointer */ } tst_node; ``` `tst_ins_del` 實作: ```cpp void *tst_ins_del(tst_node **root, const char *s, const int del, const int cpy) { ... while ((curr = *pcurr)) { /* iterate to insertion node */ ... return (void *) curr->eqkid; /* pointer to word */ ... pcurr = &(curr->eqkid); /* get next eqkid pointer address */ ... } for (;;) { ... curr->eqkid = (tst_node *) s; return (void *) s; ... } } ``` 乍看之下一頭霧水,為何有時將 `eqkid` 傳出去當 word 起始位置,有時又用它來取得下個 node,有時將傳進來的 `const char *s` 轉型成 `tst_node *` 呢?其實從 `tst_suggest` 可以看出些端倪: ```cpp void tst_suggest( ... ) { ... if (p->key) tst_suggest(p->eqkid, c, nchr, a, n, max); else if (*(((char *) p->eqkid) + nchr - 1) == c) a[(*n)++] = (char *) p->eqkid; ... } ``` 若 `key` 非 0(`key` 爲 0 的 node 的存在是爲了表示 parent node 的結尾),則將 `eqkid` 當 node 指標進行 recursive call,否則當作字串使用。 這裡需要記錄字串是因爲走訪到 node 之後要得知該 node 代表的字串是有點麻煩的事情(若有記錄 parent,可以往上透過一些規則找回),但這裡很巧妙的利用了 `eqkid` 當作 `char *` 指向該 node 表示的字串。 關於程式碼的 copy (`CPY`) 與 reference (`REF`) 機制,前者是傳入 `tst_ins_del` 的字串地址所表示的值**隨時會被覆寫**,因此 `eqkid` 需要 `malloc` 新空間將其 **copy** 存起來,而後者是傳進來的字串地址所表示的值**一直屬於該字串**,以此可將其當作 **reference** 直接存起來,不用 `malloc` 新空間。 承接上面,`eqkid` 分飾多角,沒有需要重複使用的時候嗎? |Insert XY|Insert XYZ|Insert XY and XYZ |--|--|--| | ![](https://i.imgur.com/Sfb5JEz.png) | ![](https://i.imgur.com/CgpvAKU.png) | ![](https://i.imgur.com/QbPSMk8.png) | 如上面第三種情況,node Z 的 `eqkid` 該指向 ``"XY"`` 表示該字串結束,還是指向下一個 `key` 爲 0 的 node 表示 `"XYZ"` 的結束呢? 再仔細觀察及思考會發現 node Z 的 key 爲 `'z'`,已無法表示字串 `"XY"` 的結束,`tst_node` 中也沒有類似綠色的屬性,所以 tst.c 的實作實際上是這樣的: |Insert XY and XYZ in order|Insert XYZ and XY in order| |--|--| |![](https://i.imgur.com/KU6JPaA.png)|![](https://i.imgur.com/vTpNRJd.png)| > 注意:上圖(兩張都)請將 N 當作小綠空圓(表示字串 `"XY"` 的結束)並無視它下面的小綠空圓,而左圖 `>N` 爲 `>0` 可見用來表示結束的 node 永遠不會有 ternary equal child, 因此 `eqkid` 就不會分身乏術。 ## 資料檔案的分析 檔案 `cities.txt` 提供超過 9 萬筆城市和所屬的國家/地區。城市名後面跟着逗號 (`,`),由於國情和文化落差,我們務必要留意到這些城市的命名行為及特例。我們可運用 [regular expression](https://en.wikipedia.org/wiki/Regular_expression) 來分析。 * Pattern `,(.*)?,(.*)?,(.*)?,`: 有 4 個逗號的資料,共有 1 個結果 > 命令: `$ egrep ",(.*)?,(.*)?,(.*)?," cities.txt` * `Villa Presidente Frei, Ñuñoa, Santiago, Chile, Chile`, 依序為 `建築物名稱, 城鎮名, 城市名, 國名`。其中城市名的部份為 `Santiago, Chile`,後者為 "Santiago de Chile" 的縮寫 * Pattern `,(.*)?,(.*)?,`: 有 3 個逗號的資料,共有 3 個結果 > 命令: `$ egrep ",(.*)?,(.*)?," cities.txt` * `Oliver-Valdefierro, Oliver, Valdefierro, Spain`, 依序為 `城市名, 區名, 區名, 國名`。 其中 Oliver 和 Valdefierro 是兩個相同層級的行政區 * `Earth, TX, Texas, United States`, 依序為 `城市名, 州名縮寫, 州名, 國名`。這是例外,因為除此之外的所有美國的地名都是遵循 `城市名, 州名, 國名` 的格式。 * Pattern `,(.*)?,`: 有 2 個逗號的資料,共有 19191 個結果 > 命令: `$ egrep ",(.*)?," cities.txt` * 大部份為美國、加拿大、墨西哥、澳洲等有州層級的行政區規劃的國家,可粗略推測這種情況的資料,中間的部份都是州 * 剩下的就是只有一個逗號的國家,格式為 `城市名, 國名` 為了解析的便利,我們可將上述有三個以上逗號的資料取代如下: * `Villa Presidente Frei, Ñuñoa, Santiago, Chile, Chile` $\to$ `Ñuñoa, Chile` * 因為智利其他的地名都是遵循 `城市名, 國名` 的格式,而且 `Santiago, Chile` 已存在 * `Oliver-Valdefierro, Oliver, Valdefierro, Spain` $\to$ `Oliver-Valdefierro, Spain` * 因為就西班牙的行政區分類,這兩個地名本來就被視為是[同一個區](https://es.wikipedia.org/wiki/Oliver-Valdefierro),且大部份西班牙的地名都是遵循 `城市名, 國名` 的格式 * `Earth, TX, Texas, United States` -> `Earth, Texas, United States` * 除此之外,所有美國的地名都是遵循 `城市名, 州名, 國名` 的格式 還有個例外是 `Xeraco,Jaraco, Spain`,該城市名的逗號後方沒有空白鍵(僅此一筆例外)。這因 Xeraco 和 Jaraco 實際指同一個地方,只是拼法不同,因此保留西班牙本地常用的 Xeraco 即可。 如此一來,這份資料就只剩下「有一個逗號的資料」跟「有兩個逗號的資料」了,在我們假設「有兩個逗號的資料」都是`城市名, 州名/省名, 國名`的情況下,我們可以用以下程式碼讀取輸入(此方法會將逗號和換行符都排除): ```cpp char line[WRDMAX]; while (fgets(line, WRDMAX, fp) != NULL) { char city[WRDMAX] = "", province[WRDMAX] = "", nation[WRDMAX] = ""; sscanf(line, "%[^,^\n], %[^,^\n], %[^,^\n]", city, province, nation); /* Do what you want */ } ``` ## 使用 perf 分析程式效能 Perf 全名是 Performance Event,是在 Linux 2.6.31 以後內建的系統效能分析工具,它隨著核心一併釋出。藉由 perf,應用程式可以利用 PMU (Performance Monitoring Unit), tracepoint 和核心內部的特殊計數器 (counter) 來進行統計,另外還能同時分析運行中的核心程式碼,從而更全面了解應用程式中的效能瓶頸。 參考 [使用 perf_events 分析程式效能](https://zh-blog.logan.tw/2019/07/10/analyze-program-performance-with-perf-events/) 和 [簡介 perf_events 與 Call Graph](https://zh-blog.logan.tw/2019/10/06/intro-to-perf-events-and-call-graph/) 這兩份中文介紹,事先安裝好 `perf` 並熟悉其使用方式。 * ==在自己的 Linux 開發環境實際操作== * 再次提醒:你的 GNU/Linux「==不能==」也「==不該==」透過虛擬機器 (如 VMware, VirtualBox, QEMU 等等) 安裝 * 請務必依據 [GNU/Linux 開發工具共筆](https://hackmd.io/@sysprog/gnu-linux-dev/) 指示,將 Linux 安裝於實體硬碟並設定好多重開機 CPY 機制在 prefix-search 時效率較好的原因,可見 `tst.c` 的 `tst_ins_del`: ```cpp void *tst_ins_del(tst_node **root, const char *s, const int del, const int cpy) { ... if (*p++ == 0) { if (cpy) { /* allocate storage for 's' */ const char *eqdata = strdup(s); if (!eqdata) return NULL; curr->eqkid = (tst_node *) eqdata; return (void *) eqdata; } else { /* save pointer to 's' (allocated elsewhere) */ curr->eqkid = (tst_node *) s; return (void *) s; } } ... } ``` CPY 機制在 insert 時,建立 leaf 後會間接呼叫 `malloc` 來保存字串,在這之前的最後一次 `malloc` 是爲了配置該 leaf 的空間。一直 `malloc` 時,heap 也會形同 stack 一般地增長,所以像給定測試條件很規律的狀況下,字串地址與 leaf 的地址很可能都很靠近。 換言之,CPY 機制在一開始`p->eqkid` 後,字串就很大可能在 cache 裡面了,因此 cache miss 機率較低,這就是它相對來說在 prefix-search 上效率比較好的原因。 CPY 機制與 REF 機制在建 tree 的效率比較 + CPY 機制 + 讀檔時每次將地名暫時存入同一字串 `word` :+1: 只要 cache 沒被覆蓋就不會發生 cache miss + ternary search tree 的 terminal node 用 `strdup` 將地名存起來 :-1: 需要複製字串(影響小) :-1: 間接呼叫 `malloc`,可能涉及大量系統呼叫 + REF 機制 + 讀檔時每次將地名永久(直到 memory pool 被 free)依序存入 memory pool :-1: cache miss 次數 $\ge$ 使用的 memory pool 大小 / cache 一次載入的大小 + ternary search tree 的 terminal node 直接存 reference(指向 memory pool 的某個位置) :+1: 沒有明顯額外 overhead ## :penguin: 作業要求 * 在 GitHub 上 fork [dict](https://github.com/sysprog21/dict),視覺化 ternary search tree + bloom filter 的效能表現並分析。注意,需要設計實驗,允許不同的字串輸入並探討 TST 回應的統計分佈 * :warning: 若你的 GitHub 帳號在 2020 年 9 月 24 日已自 [dict](https://github.com/sysprog21/dict) fork,請在 GitHub 上重新 fork * 要考慮到 prefix search 的特性還有詞彙的涵蓋率 * 限用 gnuplot 來製圖 * 以 perf 分析 TST 程式碼的執行時期行為並且充分解讀 * 考慮到 CPY 和 REF 這兩種實作機制,思考效能落差的緣故,並且提出後續效能改善機制 * 充分量化並以現代處理器架構的行為解讀 * 如何兼顧執行時間和空間運用效率? * 研讀 [Xor Filters: Faster and Smaller Than Bloom and Cuckoo Filters](https://arxiv.org/abs/1912.08258) 論文並參閱對應程式碼實作 [xor_singleheader](https://github.com/FastFilter/xor_singleheader),引入上述 [dict](https://github.com/sysprog21/dict) 程式碼中,需要比較原有的 bloom filter 和 xor filter 在錯誤率及記憶體開銷 * 善用 Valgrind 和 [Massif](https://valgrind.org/docs/manual/ms-manual.html),使用方式參見 [I01: lab0](https://hackmd.io/@sysprog/2020-lab0) * 如何縮減頻繁 malloc 帶來的效能衝擊呢?提示: [memory pool](https://en.wikipedia.org/wiki/Memory_pool) * 儲存字串用的 memory pool 在 REF 版本中已實作,但在 `tst_ins_del()` 當中頻繁以 calloc() 配置節點空間造成的效能衝擊,應許以改進。 * REF 版本中的 pool 只會一直增添新的資料,並沒有從中移除資料的操作發生,因此可以用更進階的手段來管理 * tst 則會有移除節點的操作,我們無法確定要被刪除的節點他使用的記憶體落在 memory pool 的哪個地方,如何管理 memory pool, 使之不會有記憶體碎片產生,是這個問題的關鍵。 * 在現有的開放原始碼專案中,找出使用 bloom filter 的專案,並解析其導入的考量 ## 繳交方式 * 編輯 [Homework 3 作業區共筆](https://hackmd.io/@sysprog/2020-homework3),將你的觀察、上述要求的解說、應用場合探討,以及各式效能改善過程,善用 gnuplot 製圖,紀錄於新建立的共筆 ## 截止日期 * Oct 7, 2020 (含) 之前進行,不該截止日前一天才動手 * 越早在 GitHub 上有動態、越早接受 code review,評分越高

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully