--- tags: Linux --- # quiz6B - hash table contributed by < [`Chialiang86`](https://github.com/Chialiang86) > > [GitHub](https://github.com/Chialiang86/Hash-Table-C) ###### tags: `linux2021` > [作業說明 - hash table](https://hackmd.io/@sysprog/linux2021-quiz6) > [2021期末專題說明 - hash table](https://hackmd.io/@sysprog/linux2021-projects#quiz6B---hash-table) ## Todo - [x] 整理其他學員資料 - [x] 看 [hash.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/hash.h), [hashtable.h](https://github.com/torvalds/linux/blob/master/include/linux/hashtable.h) - [x] 看文章 [How does the kernel implements Hashtables?](https://kernelnewbies.org/FAQ/Hashtables) - [ ] 改寫更有效率的作法 - [x] 加上新功能(delete, collision count) - [x] 嘗試別的 hash funciton - [ ] 改善效能 - [ ] 參考 linux RCU 機制 - [ ] Hash table 應用 - [ ] sentence to NLP vector by hashing - [ ] Process management - [ ] Scheduler ## Hash Table 程式碼運作原理 ### 資料結構 ```c typedef struct { int bits; struct hlist_head *ht; } map_t; struct hlist_node { struct hlist_node *next, **pprev; }; struct hlist_head { struct hlist_node *first; }; struct hash_key { int key; void *data; struct hlist_node node; }; ``` `hash table` 主要是由一個 `hlist_head` 的動態陣列所構成,每個 entry 指向一個由 `struct hlist_node` 構成的非環狀 doubly linked list ,hash table 長度依照 `bits` 給定,可知大小為 2 的冪。 :::warning power of 2 的翻譯是「2 的冪」,而非「冪次方」 :notes: jserv ::: 而可以發現 `struct hlist_head` 只有一個 `struct hlist_node *` 的成員; 而 `struct hlist_node` 型態卻包含了一個 `struct hlist_node *` 及 `struct hlist_node **` ,其中一個原因為 hash table 指向的為非環狀的 linked list ,因此只需指向 list 的一端點,若單純使用 `struct hlist_node` 當作 head ,無用的 "pprev" 會造成大量的記憶體空間浪費,因此將 head 與 node 分開來實做。 而 `struct hlist_node` 中的 pprev 為何使用「指標的指標」而非「指標」? 回答這個問題前可以先參考 Linux 原始碼中 [type.h](https://gist.github.com/ian910297/d03edc271105854a0cc3fcf68c1cb527) : ```c=75 struct list_head { struct list_head *next, *prev; }; struct hlist_head { struct hlist_node *first; }; struct hlist_node { struct hlist_node *next, **pprev; }; ``` 可知在 type.h 中有兩種 list 的結構: #### 1. struct list_head 此結構在 linux 中常實做成環狀 doubly linked list,且可以在行程管理 (process scheduling) 的相關實做上看到,如 [sched.h](https://github.com/torvalds/linux/blob/master/include/linux/sched.h) 中有近 20 處使用到此結構,因為可以快速存取到頭以及尾的 node ( 時間複雜度 O(1) ) 故有較好的效能,適用於行程管理這種對時間要求嚴謹的部分做使用。 引用自 linux [sched.h](https://github.com/torvalds/linux/blob/master/include/linux/sched.h) ```c=461 struct sched_entity { /* For load-balancing: */ struct load_weight load; struct rb_node run_node; struct list_head group_node; unsigned int on_rq; ... } ``` #### 2. struct hlist_head 搭配 hlist_node 在 linux 中專門為了 hash table 而使用,`hlist_head` 的設計也省去了 list 起始端 `pprev` 的存放空間、在初始狀態就省去了一半的記憶體容量。而且同時 hash table 不會特別需要存取到 list 的尾端,並且走訪 list 相對沒那麼講求效率(因為 hash 的設計理念就是講求 hash collision rate 要低、因此一個 list 若太長比較需要改進的為 hash function 的設計而非改進整個資料結構)。綜合上述所說單向 list 已滿足 hash table 的需求。 此外,回答前面對於 **`pprev` 為何是「指標的指標」** 的問題:若和 `list_head` 一樣使用單純的指標( `hlist_node *`),則考慮到 list 有方向性,delete node 時需要額外檢查其是否為 list 的 head 或是 NULL 等等,有較冗餘的程式碼必須實做,因此使用 `hlist_node **pprev` 直接存取上一個 node 所在的位址 (`pprev = &node->next`) 。Linux 為求程式碼簡潔故以 pointer to pointer 的方式用 pprev 直接指向前一個元素的記憶體位址本身。 以下範例用 hlist 刪除 list 中第一個元素搭配程式碼與圖示來解釋 (先忽略記憶體釋放的程式碼) 範例中值得注意的是,如果把 `hlist_node **pprev` 變成了 `hlist_node *prev` ,可以觀察到 `first->prev` 是沒有指向任何東西的,此時範例中 step2 直接將 `pprev` 指向 `first->pprev` 是會出錯的,需要額外的程式碼來處理刪除第一個 node 的狀況。 - 若 `prev` 為 `hlist_node *` (注意紅色箭頭): ```graphviz digraph hash_list { node [shape=record]; rankdir=LR; map_t [label = "map|<f0>bits|<f1>ht*"] table [label = "<f0>ht[0]|<f1>ht[1]|<f2>...|<fn>ht[size - 1]"] first [label = "first"] hash_key1 [label = "<name>entry|{<pp>prev|<n>next}"] hash_key2 [label = "<name>entry_next|{<pp>prev|<n>next}"] null [shape=plaintext label="NULL"] blank [shape=plaintext label="NULL"] map_t:f1->table:f0 table:f1->first first->hash_key1:name hash_key1:pp->blank[color=red] hash_key1:n->hash_key2:name hash_key2:pp->hash_key1:name[color=red] hash_key2:n->null } ``` - 若 `pprev` 為 `hlist_node **` (注意紅色箭頭): ```graphviz digraph hash_list { node [shape=record]; rankdir=LR; map_t [label = "map|<f0>bits|<f1>ht*"] table [label = "<f0>ht[0]|<f1>ht[1]|<f2>...|<fn>ht[size - 1]"] hash_key1 [label = "<name>entry|{<pp>pprev|<n>next}"] hash_key2 [label = "<name>entry_next|{<pp>pprev|<n>next}"] null [shape=plaintext label="NULL"] map_t:f1->table:f0 table:f1->first first->hash_key1:name hash_key1:pp->first[color=red] hash_key1:n->hash_key2:name hash_key2:pp->hash_key1:n[color=red] hash_key2:n->null } ``` 假設要刪除 list 的第一個元素 (first 指向的 entry) ```c // deletes entry from hlist void __hlist_del(struct hlist_node* entry) { struct hlist_node *next = entry->next; // step1 struct hlist_node **pprev = entry->pprev; //step2 *pprev = next; //step3 if (next) next->pprev = pprev; //step4 } ``` step1: `next` 指向 `entry->next` (也就是 `entry_next`) ```graphviz digraph hash_list { node [shape=record]; rankdir=LR; map_t [label = "map|<f0>bits|<f1>ht*"] table [label = "<f0>ht[0]|<f1>ht[1]|<f2>...|<fn>ht[size - 1]"] hash_key1 [label = "<name>entry|{<pp>pprev|<n>next}"] hash_key2 [label = "<name>entry_next|{<pp>pprev|<n>next}"] null [shape=plaintext label="NULL"] map_t:f1->table:f0 table:f1->first first->hash_key1:name hash_key1:pp->first hash_key1:n->hash_key2:name hash_key2:pp->hash_key1:n hash_key2:n->null next->hash_key2:s[color=red] } ``` step2: `pprev` 指向 `entry->pprev` ( 也就是 `&ht[1]->first`) ```graphviz digraph hash_list { node [shape=record]; rankdir=LR; map_t [label = "map|<f0>bits|<f1>ht*"] table [label = "<f0>ht[0]|<f1>ht[1]|<f2>...|<fn>ht[size - 1]"] hash_key1 [label = "<name>entry|{<pp>pprev|<n>next}"] hash_key2 [label = "<name>entry_next|{<pp>pprev|<n>next}"] null [shape=plaintext label="NULL"] map_t:f1->table:f0 table:f1->first first->hash_key1:name hash_key1:pp->first hash_key1:n->hash_key2:name hash_key2:pp->hash_key1 hash_key2:n->null next->hash_key2:s[color=red] pprev->first:s[color=red] } ``` step3: `*pprev` (即 `&ht[1]->first`) 指向「 `next` 指向的 `entry_next` 」 ```graphviz digraph hash_list { node [shape=record]; rankdir=LR; map_t [label = "map|<f0>bits|<f1>ht*"] table [label = "<f0>ht[0]|<f1>ht[1]|<f2>...|<fn>ht[size - 1]"] hash_key1 [label = "<name>entry|{<pp>pprev|<n>next}"] hash_key2 [label = "<name>entry_next|{<pp>pprev|<next>next}"] null [shape=plaintext label="NULL"] map_t:f1->table:f0 table:f1->first first->hash_key2:name[color=red] hash_key2:pp->hash_key1:n hash_key2:n->null next->hash_key2:s[color=red] pprev->first:s[color=red] } ``` step4: 當 `next` 指向的位址不為 `NULL` 則 `next->pprev` 指向 `entry_next->pprev` ```graphviz digraph hash_list { node [shape=record]; rankdir=LR; map_t [label = "map|<f0>bits|<f1>ht*"] table [label = "<f0>ht[0]|<f1>ht[1]|<f2>...|<fn>ht[size - 1]"] hash_key1 [label = "<name>entry|{<pp>pprev|<n>next}"] hash_key2 [label = "<name>entry_next|{<pp>pprev|<next>next}"] null [shape=plaintext label="NULL"] map_t:f1->table:f0 table:f1->first first->hash_key2:name[color=red] hash_key2:pp->hash_key1:n[color=none] hash_key2:pp->first[color=red] hash_key2:n->null next->hash_key2:s[color=red] pprev->first:s[color=red] } ``` 此時 `entry` 已不被任何指標指向、可以釋放其記憶體空間(略),由於以上範例在刪除非 first 元素時行為也相同所以在這邊也忽略。 #### Hash Function 此處僅先列出 hash 程式碼,[下一章節](#Hash-Function1) 會針對此處做詳細說明包含公式由來、相關出處、比較效能 (collision rate, scalability) 以及運算速度等。 ```c #define GOLDEN_RATIO_32 0x61C88647 static inline unsigned int hash(unsigned int val, unsigned int bits) { /* High bits are more random, so use them. */ return (val * GOLDEN_RATIO_32) >> (32 - bits); } ``` #### container_of 此結構常出現在課程以及 linux 核心程式碼中,主要是藉由某個 struct 中成員的記憶體位址回推該 struct 所在的記憶體位址。 ```c #define container_of(ptr, type, member) \ ({ \ void *__mptr = (void *) (ptr); \ ((type *) (__mptr - offsetof(type, member))); \ }) ``` ```c struct hash_key *kn = (struct hash_key *)malloc(sizeof(struct hash_key)); kn->key = key, kn->data = data; struct hlist_node *p = &kn->node; // 注意是取 kn->node 的位址給 p struct hash_key *kn = container_of(p, struct hash_key, node); ``` 例如以上例子, `container_of(p, struct hash_key, node)` 就是藉由 `p` 的位址回推儲存 `p` 的容器 `kn` 的位址,顯然可以用計算 `node` 這個成員在 `struct hash_key` 這個型態的相對記憶體位址來回推,假設 `p` 位址為 `0xff0c`, 而 `node` 這個成員在 `struct hash_key` 的第 12(`0x000c`) 個 byte 的位置,而 `p` 所在的容器位址可計算出為 `0xff0c - 0x000c = 0xff00`。 #### 重要細節 ```c struct hash_key { int key; void *data; struct hlist_node node; }; ``` `node` 在 `struct hash_key` 中不是「 pointer 」是「 一般變數 」,因為若是 pointer (`struct hlist_node*`) 的話,`container_of` 是算出 (「 pointer 所指的空間」 - 「成員在 struct 的相對位置」)、顯然這樣算會得出一個不合法的記憶體位址,值得注意。 ### 其他 hash table API 實作細節 #### map_init 先藉由 `MAP_HASH_SIZE(map->bits)` 將 hash table 的 bucket size 設定為 2 的 `map->bits` 次方,並將每個 bucket 指出去的 list 初始化為 `NULL` ,若有任何記憶體配置錯誤則初始失敗回傳 NULL ```c map_t *map_init(int bits) { map_t *map = (map_t*)malloc(sizeof(map_t)); if (!map) return NULL; map->bits = bits; map->ht = (struct hlist_head *)malloc(sizeof(struct hlist_head) * MAP_HASH_SIZE(map->bits)); if (map->ht) { for (int i = 0; i < MAP_HASH_SIZE(map->bits); i++) (map->ht)[i].first = NULL; } else { free(map); map = NULL; } return map; } ``` #### map_add 將 `key` 以及其對應的 `data` 加入 hash table 中。若 `find_key` 函數得出此 `key` 已經存在 hash table 中,則直接回傳,否則藉由 hash function 算出要存入 table 中的哪個 bucket 並將其加入 bucket 的 list 中。 ```c void map_add(map_t *map, int key, void *data) { struct hash_key *kn = find_key(map, key); if (kn) return; // step 1 kn = (struct hash_key *)malloc(sizeof(struct hash_key)); kn->key = key, kn->data = data; // step 2 struct hlist_head *h = &map->ht[hash(key, map->bits)]; struct hlist_node *n = &kn->node, *first = h->first; n->next = first; //step 3 if (first) first->pprev = &n->next; //step 4 h->first = n; //step 5 n->pprev = &h->first; //step 6 } ``` #### find_key 根據 `key` 找到對應的 hash bucket 中 list 的 `head`,走訪該 list 來尋找存放此 key 的結構 `struct hash_key`,並回傳此結構的 pointer,沒有比對到 key 則回傳 `NULL`。 因為整個 list 只有 `struct hlist_node` 去參與並不是整個 `struct hash_key`,故要藉由 node 得到整個 hash_key 的結構必須用 `container_of` 去計算相對記憶體位址。 ```c static struct hash_key *find_key(map_t *map, int key) { struct hlist_head *head = &(map->ht)[hash(key, map->bits)]; for (struct hlist_node *p = head->first; p; p = p->next) { struct hash_key *kn = container_of(p, struct hash_key, node); if (kn->key == key) return kn; } return NULL; } ``` #### map_deinit 針對每個非空的 bucket 走訪所有節點釋放記憶體空間。 ```c void map_deinit(map_t *map) { if (!map) return; for (int i = 0; i < MAP_HASH_SIZE(map->bits); i++) { struct hlist_head *head = &map->ht[i]; for (struct hlist_node *p = head->first; p;) { struct hash_key *kn = container_of(p, struct hash_key, node); struct hlist_node *n = p; p = p->next; if (!n->pprev) /* unhashed */ goto bail; struct hlist_node *next = n->next, **pprev = n->pprev; *pprev = next; if (next) next->pprev = pprev; n->next = NULL, n->pprev = NULL; bail: free(kn->data); free(kn); } } free(map); } ``` ## Hash Function 程式碼中的 hash function 如下,可以看到其使用一個 magic number 稱為 `GOLDEN_RATIO` ,並且直接與 `val` 相乘並留下較高位元的 bits,此外 `bits` 變數為 hash 最終的值使用的 bit size ,實做中使用 `bits = 10` ,也就是 hash 出來的值介於 0 ~ 1023 之間。 ```c #define GOLDEN_RATIO_32 0x61C88647 static inline unsigned int hash(unsigned int val, unsigned int bits) { /* High bits are more random, so use them. */ return (val * GOLDEN_RATIO_32) >> (32 - bits); } ``` hash function 可以從以下幾個觀點考量: - hash collision 的機率(雜湊值的隨機性), 影響桶內的鏈表的長度 => 走訪的效率 - bucket 的使用率 => 空間使用率 - hash function 本身的計算效率 - 無法從雜湊值回推原值(加解密用, 應該不是 hash table 的考量點) 從 The Art of Computer Programming vol3. 6.4, Dr.Knuth 提到兩種 hash function - based on division - based on multiplication(multiplicative hashing) 書中描述 multiplicative hashing, 可以等價於 mod, 且在多數的 CPU 乘法都比除法快( 特例是 mod (power of 2) 可以轉爲位元運算), 此外對於非隨機性的值像是 "1 2 3 ..." 可以減少碰撞機率。 在 [hash.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/hash.h) 註解中出現的論文 [Linux Kernel Hash Table Behavior: Analysis and Improvements](http://www.citi.umich.edu/techreports/reports/citi-tr-00-1.pdf) 也提到 > Our multiplicative hash functions were derived from [Knuth], p. 513ff. The theory posits that machine multiplication by a large number that is likely to cause overflow is the same as finding the modulus by a different number. 利用 golden ratio 做 multiplicative hashing 的稱為 Fibonacci hashing(也是書中提出來的例子), 被上述的論文驗證過 :::danger 務必清楚標註,到底「根據 ref 裡面一篇討論」是什麼 "ref" 呢?又,討論在哪? :notes: jserv ::: 而根據 ref 裡面一篇討論 Fibonacci hashing 的 blog, 作者認為 power of two binary and 雖然可以有較快的速度, 但是由於會捨棄掉 low bits , 在沒有留意的狀況可能會造成較高 hash collision,後面會用實驗來檢視此說法在各個情況的結果。 而 [stackoverflow](https://stackoverflow.com/questions/38994306/what-is-the-meaning-of-0x61c88647-constant-in-threadlocal-java/38994612) 也有提到 > 0x61c88647 = 1640531527 ≈ 2 ^ 32 * (1 - 1 / φ), φ = (√5 + 1) ÷ 2, it is an another Golden ratio Num of 32 bits. > If you convert 0x61c88647 into decimal, you'll get 1640531527 which is nonsensical until you realise that in 32 bits, it's the signed version of 2654435769. Again this number seems a bit odd until you realise that it is 2 ^ 32 ÷ φ where φ is the golden ratio (√5+1)÷2. 再跟 [hash.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/hash.h) 註解做比對 (phi 為黃金比例) >These are the negative, (1 - phi) = phi ^ 2 = (3 - sqrt(5))/2, which is very slightly easier to multiply by and makes no difference to the hash distribution. ### 整理上述資訊 一般來說我們常聽到的 Golden Ratio : \begin{equation} Golden Ratio = \phi = \frac{1 + \sqrt{5}}{2} \end{equation} 而將註解中的公式 ` (3 - sqrt(5))/2` 進行推導如下: \begin{equation} \frac{3 - \sqrt{5}}{2} = 1 + \frac{1 - \sqrt{5}}{2} = 1 - \frac{2}{1 + \sqrt{5}} = 1 - \frac{1}{\frac{1 + \sqrt{5}}{2}} = 1 - \frac{1}{\phi} \end{equation} 由推導及引文的整理可知程式碼中的 Golden Ratio 實際上是將 `2 ^ 32 * (1 - 1 / phi)` 得到的,是 一種 32 bit 的 Golden Ratio 表達方式,而 32 bit 版本又有兩種: - 第一種公式相當單純: \begin{equation} Golden Ratio = \frac{2 ^ {32}}{\phi} = \texttt{2654435769.5} \approx \texttt {0x9E3779BA} \end{equation} - 第二種稍複雜一些,也是 `hash.h` 使用的版本 \begin{equation} Golden Ratio = 2 ^ {32} \times (1 - \frac{1}{\phi}) = \texttt{1640531526.5} \approx \texttt {0x61C88647} \end{equation} 至於為什麼沒用第一種公式 `hash.h` 註解也有提到,主要是相乘上較為容易、並且表現跟第一種是一樣的。以下實際執行四個實驗來驗證其說法的正確性,包含比較 collision rate 、乘法執行時間比較等。也會進而延伸探討 scalability 、 使用不同 bits 下當作 hash 結果等面向。 ### 實驗一 : 32-bit 下不同 Golden Ratio 的 collision rate #### 動機及方法 檢驗兩種 Golden Ratio 對於 hash 的表現,驗證註解提到的 hash distribution 是否真的相同。以下提供兩種 hash function 對 1~100 當作 key 去 hash 的結果比較圖 :::spoiler <span style="color:rgb(100, 155, 225)">實驗程式碼</span> hash.h 的 head 新增欄位 `cnt` 欄位紀錄 list 中有多少 key ; `map_add` 中新增一行 : `map->ht[hash(key, map->bits)].cnt += 1`,當然刪除 node 也必須有 `-= 1` 的操作。 ```cpp ... struct hlist_head { int cnt; struct hlist_node *first; }; ... void map_add(map_t *map, int key, void *data) { struct hash_key *kn = find_key(map, key); if (kn) return; kn = (struct hash_key*)malloc(sizeof(struct hash_key)); kn->key = key, kn->data = data; map->ht[hash(key, map->bits)].cnt += 1; struct hlist_head *h = &map->ht[hash(key, map->bits)]; struct hlist_node *n = &kn->node, *first = h->first; n->next = first; if (first) first->pprev = &n->next; h->first = n; n->pprev = &h->first; } ``` generate.c ```c= #include "hash.h" #include <stdio.h> #include <stdlib.h> struct timespec time_diff(struct timespec start, struct timespec end); int main(int argc, char *argv[]) { // hash major code if (argc < 3) { printf("should input bits and basic_size.\n"); exit(0); } int bits = atoi(argv[1]), basic_size = atoi(argv[2]), iteration_num = argc > 3 ? atoi(argv[3]) : 6; printf("hash table size = %d, basic num of key = %d, iteration num = %d .\n", MAP_HASH_SIZE(bits), basic_size, iteration_num); map_t* hmap = map_init(bits); if (!hmap) { printf("hash map malloc error!\n"); exit(0); } FILE *fout_scal; if (!(fout_scal = fopen("dat/scalability.dat", "w"))) { printf("file open error!\n"); exit(0); } for (int iter = 1; iter <= iteration_num; iter++) { printf("iteration %d ...\n", iter); FILE *fout_hash, *fout_dist; char *f_hash = (char *)malloc(sizeof(char) * 32), *f_dist = (char *)malloc(sizeof(char) * 32); if (!f_hash || !f_dist) { printf("filename malloc error!\n"); exit(0); } int size = iter * basic_size; sprintf(f_hash, "dat/generate_%d.dat", size); sprintf(f_dist, "dat/distribution_%d.dat", size); if (!(fout_hash = fopen(f_hash, "w")) || !(fout_dist = fopen(f_dist, "w"))) { printf("file open error!\n"); exit(0); } for (int i = 0; i < size; i++) { fprintf(fout_hash, "%d\n", hash(i, bits)); map_add(hmap, i, NULL); } int ncnt = 0, ucnt = 0, ccnt = 0; for (int i = 0; i < MAP_HASH_SIZE(bits); i++) { int tmp = (hmap->ht)[i].cnt; ncnt += !tmp ? 1 : 0; // count empty : increment if this bucket is empty ucnt += tmp == 1 ? 1 : 0; // count unique : increment if this bucket contains exactly one key ccnt += tmp ? tmp - 1 : 0; // count collision : if this bucket contains more than one key, then add ("num of key in this bucket" minus 1) fprintf(fout_dist, "%d\n", tmp); } // count ratio float load_factor = (float)size / MAP_HASH_SIZE(bits); // n divide by b, n is the num of keys, b is the num of buckets float not_used_rate = (float)ncnt / MAP_HASH_SIZE(bits) * 100; float exactly_one_rate = (float)ucnt / MAP_HASH_SIZE(bits) * 100; float more_than_one_rate = (float)(MAP_HASH_SIZE(bits) - ncnt - ucnt) / MAP_HASH_SIZE(bits) * 100; float collision_rate = (float)ccnt / size * 100; fprintf(fout_scal, "%.2f %f %f %f %f\n", load_factor, not_used_rate, exactly_one_rate, more_than_one_rate, collision_rate); printf("============== result ==============\n"); printf("num of key = %d\n", size); printf("load factor = %f\n", load_factor); printf("not-used rate = %f%%\n", not_used_rate); printf("exactly-one rate = %f%%\n", exactly_one_rate); printf("more-than-one rate = %f%%\n", more_than_one_rate); printf("collision rate = %f%%\n", collision_rate); printf("====================================\n"); fclose(fout_hash); fclose(fout_dist); free(f_hash); free(f_dist); } fclose(fout_scal); return 0; } ``` ::: #### 第一種公式: \begin{equation} Golden Ratio = \frac{2 ^ {32}}{\phi} = \texttt{2654435769.5} \approx \texttt {0x9E3779BA} \end{equation} - bits = 10, for 0~99 ![](https://i.imgur.com/0l0CGkd.png) ![](https://i.imgur.com/hACnTLS.png) ``` load factor = 0.097656 not-used rate = 90.234375% exactly-one rate = 9.765625% more-than-one rate = 0.000000% collision rate = 0.000000% ``` - bits = 10, for 0~499 ![](https://i.imgur.com/Mzy7Pqy.png) ![](https://i.imgur.com/q7I0EDU.png) ``` load factor = 0.488281 not-used rate = 51.171875% exactly-one rate = 48.828125% more-than-one rate = 0.000000% collision rate = 0.000000% ``` - bits = 10, for 0~1023 ![](https://i.imgur.com/4qEZGuA.png) ![](https://i.imgur.com/y4uwC9X.png) ``` load factor = 1.000000 not-used rate = 12.402344% exactly-one rate = 75.195312% more-than-one rate = 12.402344% collision rate = 12.402344% ``` #### 第二種公式 (`hash.h` 的版本): \begin{equation} Golden Ratio = 2 ^ {32} \times (1 - \frac{1}{\phi}) = \texttt{1640531526.5} \approx \texttt {0x61C88647} \end{equation} - bits = 10, for 0~99 ![](https://i.imgur.com/i1FuGPS.png) ![](https://i.imgur.com/aACl63T.png) ``` load factor = 0.097656 not-used rate = 90.234375% exactly-one rate = 9.765625% more-than-one rate = 0.000000% collision rate = 0.000000% ``` - bits = 10, for 0~499 ![](https://i.imgur.com/XgpEiBQ.png) ![](https://i.imgur.com/mmC6nWu.png) ``` load factor = 0.488281 not-used rate = 51.171875% exactly-one rate = 48.828125% more-than-one rate = 0.000000% collision rate = 0.000000% ``` - bits = 10, for 0~1023 ![](https://i.imgur.com/oZyKj4H.png) ![](https://i.imgur.com/yF4MJ03.png) ``` load factor = 1.000000 not-used rate = 12.402344% exactly-one rate = 75.195312% more-than-one rate = 12.402344% collision rate = 12.402344% ``` #### 結果: 1. 同一個 key 對兩種版本的 hash 的值是不同的。 2. 兩種版本的 collision rate 及 hash table 中 key 在 bucket 中的分佈狀態皆得到完全一致的結果。 ### 實驗二 : 檢驗 Scability #### 動機及方法 由以上實驗已經證實兩個 Golden Ratio 對 hash table 的分佈是一樣的,進一步探討 hash function 的 scalability (以 `hash.h` 中的 Golden Ratio 當作範例) ,同時分別使用連續的 key 值以及隨機的 key 值去做實驗。 實驗程式碼同 [實驗一](#實驗一-:-32-bit-下不同-Golden-Ratio-的-collision-rate) ,此處用的 hash table 的 bucket 數皆控制在 1024 (bits = 10)。 #### 1. 使用連續的 key 值(ex: 0 ~ 2048)下去做 hash 以 [load factor](https://programming.guide/hash-table-load-factor-and-capacity.html) 當作 x 軸檢視 hash table 隨著 load factor 升高各個狀況 (not-used-bucket rate, exactly-one-bucket rate, more-than-one-bucket rate, collision rate),以百分比 (%) 的呈現: ![](https://i.imgur.com/uUzkZOQ.png) #### 結果: - **not-used-bucket rate** : 可以看到 load-factor 一旦到 1.5 時,幾乎沒有 bucket 是空的。 - **exactly-one-bucket rate** : 起初隨著 load-factor 上升而升高(因為越來越多 bucket 被 hash 中),最高為 load-factor = 1 時來到近八成,接著隨著 load-factor 升高而急速降低(因為越來越多的 bucket 被 hash 中超過一次),直到 load-factor 來到 2.5 左右時幾乎所有的 bucket 都被 hash 中不只一次。 - **more-than-one-bucket rate** : 隨著 load-factor 升高而快速升高,直到 load-factor 來到 2.5 左右時幾乎為 100% 。值得注意的是 load-factor 為 1( key 數量跟 hash table 的 bucket 數量一樣多時),已經有超過 10% 的 bucket 有超過一個 key 的現象。 - **collision rate** : 隨著 load-factor 升高而呈嚴格遞增曲線,增長速度漸緩。 #### 2. 使用隨機產生的 key 值下去做 hash 使用 c `stdlib.h` 的 `rand()` 搭配 `time.h` 的 `time(NULL)` 產生亂數種子,取其中一次結果 ![](https://i.imgur.com/R0YZTFw.png) #### 結果: 相對於連續的 key 值來說,可以看到同一個 load factor 下 exactly-one rate 是低了許多,當然主要是因為 random 不像連續的 key 容易產生較為分散的 hash 結果,而 non-used rate 直到 load factor 接近 5 時才趨近於 0 。有趣的現象是,其 collision rate 的曲線竟然跟使用連續的 key 相當接近。 :::info todo : - 閱讀 [Hash Collision Probabilities](https://preshing.com/20110504/hash-collision-probabilities/) 嘗試比較不同 hash 的理論 collision rate 公式 - 做實驗討論若是不用較高位元的 bit 會造成什麼結果 ::: ### 實驗三 : 32-bit 下不同 Golden Ratio 的 執行時間 #### 動機及方法 不確定 "very slightly easier to multiply" 是否是指運算成本較低一些些,因此做實驗來比較兩種版本的執行時間。實驗會將兩種不同的 Golden Ratio 做 hash 十億次分別計算執行時間,重複實驗 30 次 :::spoiler <span style="color:rgb(100, 155, 225)">實驗程式碼</span> ```c= #include <time.h> #include <stdio.h> #include <stdlib.h> #include "hash.h" #define NINO 1000000000 struct timespec time_diff(struct timespec start, struct timespec end); int main(int argc, char *argv[]) { if (argc < 3) { printf("should input bits and size.\n"); exit(0); } int bits = atoi(argv[1]), size = atoi(argv[2]); struct timespec start, end, diff; clock_gettime(CLOCK_MONOTONIC, &start); for (int i = 0; i < size; i++) int _ = hash(i, bits); clock_gettime(CLOCK_MONOTONIC, &end); diff = time_diff(start, end); printf("execution time = %lu.%lu sec\n", diff.tv_sec, diff.tv_nsec); return 0; } struct timespec time_diff(struct timespec start, struct timespec end) { struct timespec temp; if ((end.tv_nsec-start.tv_nsec) < 0) { temp.tv_sec = end.tv_sec - start.tv_sec - 1; temp.tv_nsec = NINO + end.tv_nsec-start.tv_nsec; } else { temp.tv_sec = end.tv_sec-start.tv_sec; temp.tv_nsec = end.tv_nsec-start.tv_nsec; } return temp; } ``` ::: ![](https://i.imgur.com/A1163wv.png) #### 結果: 可以看到兩種版本的執行時間皆落在 1.86 ~ 1.89 秒之間,看起來 `hash.h` 中 `0x61C88647` 的版本確實執行時間略少於 `0x9E3779BA` ,不過有鑑於實驗次數不多、環境也僅限於本身的桌機,因此也難以斷定是否真是如此。 :::warning 1. 建議直接對 30 筆數據取平均值與標準差, 可以更精確地呈現兩者的差距 2. 可參考 [fibdrv#排除干擾效能分析的因素](https://hackmd.io/@KYWeng/rkGdultSU#%E6%8E%92%E9%99%A4%E5%B9%B2%E6%93%BE%E6%95%88%E8%83%BD%E5%88%86%E6%9E%90%E7%9A%84%E5%9B%A0%E7%B4%A0) 來降低數據的飄動幅度. 雖然 1.89 ~ 1.86 僅有約1.6%的差異, 但當兩者速度的差異也在相同級距時, 會降低數據的參考性 3. 可以備註使用的 compiler flags, compiler version 等實驗環境設置, 以利其他讀者重現實驗或是比對數據 -KYG- ::: ### 實驗四 : 保留不同 bits 下的 collision rate 由[本作業]()提供的[程式碼]()中有一行註解提到: > High bits are more random, so use them. 為了檢驗這個說法,我嘗試保留不同 bit 位置的 hash 結果,從最高位元的 bit 到最低位元的 bit 做一個 collision rate 的比較,關鍵程式碼如下: ```c #define GOLDEN_RATIO_32 0x61C88647 static inline unsigned int hash(unsigned int val, unsigned int bits, int lowbit) { /* High bits are more random, so use them. */ unsigned int ret = (val * GOLDEN_RATIO_32); //step1 ret = ret << (32 - bits - lowbit); // step2 ret = ret >> (32 - bits); // step3 return ret; } ``` 將 `hash` 函數的 parameter 新增 `lowbit` 欄位,`lowbit` 為會被 right shift 掉的位元數,並保留第 `lowbit + 1` 到 `lowbit + bits` 位元、也就是提取中間 `bits` 個位元的值。 - 假設 `bits = 10` 、`lowbit = 3` step1 : 算出 `val * GOLDEN_RATIO_32` 的值存到 `ret` ```graphviz digraph hash_list { node [shape=record]; head [shape=plaintext label="32 - bits - lowbit"] mid [shape=plaintext fontcolor=red label="bits"] tail [shape=plaintext label="lowbit"] ret [label="<h>1000111011100101000|<m>1000101010|<t>001"] head->ret:h mid->ret:m[color=red] tail->ret:t } ``` step2 : 將 `ret` 左移 `32 - bits - lowbit` 個 bits (清除高位元不必要的值) ```graphviz digraph hash_list { node [shape=record]; head [shape=plaintext fontcolor=red label="bits"] mid [shape=plaintext label="lowbit"] ret [label="<h>1000101010|<m>001|<t>0000000000000000000"] head->ret:h mid->ret:m[color=red] } ``` step3 : 將 `ret` 右移 `32 - bits` 個 bits (讓要保留的 `bits` 個 bit 回到最低位元) ```graphviz digraph hash_list { node [shape=record]; mid [shape=plaintext fontcolor=red label="bits"] ret [label="<h>0000000000000000000000|<t>1000101010"] mid->ret:t[color=red] } ``` 最後的 `1000101010` (十進位 554) 為 hash 結果。 #### 動機及方法 控制 hash 出來的 key 介於 0 ~ 1023 (10 bits) 之間,用不同的 `lowbit` (0 ~ 22) 做出的 hash function 去對不同的 load factor 測試其 scalability ,並將結果作圖。此外,也嘗試兩種 key ,一種為連續的數字、一種為隨機的數字。 :::spoiler <span style="color:rgb(100, 155, 225)">實驗程式碼</span> - 對 generate.c 進行更改如下 ```c #include "hash.h" #include <stdio.h> #include <stdlib.h> #include <time.h> struct timespec time_diff(struct timespec start, struct timespec end); int main(int argc, char *argv[]) { // hash major code if (argc < 3) { printf("should input bits and basic_size.\n"); exit(0); } int bits = atoi(argv[1]), basic_size = atoi(argv[2]), iteration_num = argc > 3 ? atoi(argv[3]) : 1; printf("hash table size = %d, basic num of key = %d, iteration num = %d .\n", MAP_HASH_SIZE(bits), basic_size, iteration_num); FILE *fout_scal; if (!(fout_scal = fopen("dat/scalability_cmp.dat", "w"))) { printf("file open error!\n"); exit(0); } srand(time(NULL)); int shift_num = 12; int shift_arr[] = {22, 20, 18, 16, 14, 12, 10, 8, 6, 4, 2, 0}; // 12 float **shift_data = (float **)malloc(sizeof(float*) * (shift_num + 1)); for (int j = 0; j <= shift_num; j++) shift_data[j] = (float *)malloc(sizeof(float) * iteration_num); for (int shift_iter = 0; shift_iter < shift_num; shift_iter++) { printf("iteration %d ...\n", shift_iter); for (int iter = 1; iter <= iteration_num; iter++) { // printf("iteration %d ...\n", iter); map_t* hmap = map_init(bits); if (!hmap) { printf("hash map malloc error!\n"); exit(0); } int size = iter * basic_size; for (int i = 0; i < size; i++) { int v = rand(); // fprintf(fout_hash, "%d\n", hash(i, bits)); map_add(hmap, v, NULL, shift_arr[shift_iter]); } int ncnt = 0, ucnt = 0, ccnt = 0; for (int i = 0; i < MAP_HASH_SIZE(bits); i++) { int tmp = (hmap->ht)[i].cnt; ncnt += !tmp ? 1 : 0; // count empty : increment if this bucket is empty ucnt += tmp == 1 ? 1 : 0; // count unique : increment if this bucket contains exactly one key ccnt += tmp ? tmp - 1 : 0; // count collision : if this bucket contains more than one key, then add ("num of key in this bucket" minus 1) // fprintf(fout_dist, "%d\n", tmp); } // count ratio float load_factor = (float)size / MAP_HASH_SIZE(bits); // n divide by b, n is the num of keys, b is the num of buckets float not_used_rate = (float)ncnt / MAP_HASH_SIZE(bits) * 100; float exactly_one_rate = (float)ucnt / MAP_HASH_SIZE(bits) * 100; float more_than_one_rate = (float)(MAP_HASH_SIZE(bits) - ncnt - ucnt) / MAP_HASH_SIZE(bits) * 100; float collision_rate = (float)ccnt / size * 100; shift_data[0][iter - 1] = load_factor; shift_data[shift_iter][iter] = collision_rate; printf("%.2f %f %f %f %f\n", load_factor, not_used_rate, exactly_one_rate, more_than_one_rate, collision_rate); map_deinit(hmap); } } for (int i = 0; i < iteration_num; i++) { for (int j = 0; j <= shift_num; j++) { fprintf(fout_scal, "%.2f ", shift_data[j][i]); } fprintf(fout_scal, "\n"); } fclose(fout_scal); return 0; } ``` ::: 連續數字下保留不同位置的 `bit` 產生的結果 (shift 22 bits 就是保留最高位元的 10 個 bits) ![](https://i.imgur.com/pUbj55c.png) 用隨機數字保留不同位置的 `bit` 產生的結果 (shift 22 bits 就是保留最高位元的 10 個 bits) ![](https://i.imgur.com/H8sY0f5.png) #### 結果: 由第一張圖可以看出,只有在 load factor 較小時不同 hash function 有較不穩定的結果,而低 load factor 的情況下不一定 shift 的 bit 數越多(越高位元)就越穩定。 由第二張圖結果可以看出,在隨機情況下取不同的 bit 當作 hash 結果顯然對 collision rate 影響程度不大,且 load factor 在較大的情況的表現幾乎是一致的。 但儘管如此,hash function 考慮到 shift bit 是有一定的根據的。有一種 hash function 稱作 [Mid-Square hashing](https://www.geeksforgeeks.org/mid-square-hashing/),顧名思義就是將數字取平方後、提取中間幾個位數當作 hash 結果,例如對 4765 這個數字做 Mid-Square hashing,首先 4,765 x 4,765 = 22,705,225 ,這時假設只保留中間三個位數、捨棄最低的三位,得到 hash 結果為 705 。 至於為什麽要取中間而不取最高位或最低位主要有幾個原因: - 取高位元:萬一只有少數幾個資料的 key 可以到很高的位元、其他都是較小的數字,則可能 hash 出來的值大都為 0, 1, 2 等很小的數字,顯然這不符合負載均衡 (load balance) 或 碰撞機率低(low collision rate) 的精神。 - 取低位元:考慮到平方的特性 (2, 8 作為尾數取平方的最低位都是 4 ; 3, 7 作為尾數取平方的最低位都是 9 等) ,顯然容易抓到 hash 的規律、也不符合負載均衡的原則。而容易抓到 hash 的規律也造成在資安上使用這種容易猜測的 hash 函數被駭客破解。 因此取中間位數顯然是折衷又相對有效的方法,一般要取中間幾位也沒有制式化規定、要看 key 的範圍跟特性。而至於 Golden Ratio 相乘的情況不同於取平方、也不見得有容易猜出規律的問題,因此是否取高位元就是較 `random` 以實驗結果來看,是一個有還待商榷的說詞。 ## 改進實做 參考 [hash.h](https://github.com/torvalds/linux/blob/master/include/linux/hash.h), [hashtable.h](https://github.com/torvalds/linux/blob/master/include/linux/hashtable.h) 的實作方式,嘗試對程式碼做優化改進 ### Macro 從 Linux 核心對於 hashtable 的程式碼中可以有許多巨集 (Macro) 的定義,從中探討實做上的考量以及意義 #### ARRAY_SIZE/HASH_BITS 以下程式碼參考自 [hashtable.h](https://github.com/torvalds/linux/blob/master/include/linux/hashtable.h) ```c=27 #define HASH_SIZE(name) (ARRAY_SIZE(name)) #define HASH_BITS(name) ilog2(HASH_SIZE(name)) ``` 非常容易即可看出 `HASH_SIZE(name)` 的功能就是在計算 `name` 這個 array 的元素個數、並且 HASH_BITS 就是從 array 的元素個數回推其佔有的 bit 數,而簡單的程式碼暗藏玄機,注意到 `ARRAY_SIZE(name)` 這個 Macro 時去進行搜索,發現裡面暗藏許多相當厲害的技巧 參考自[從0開始](http://frankchang0125.blogspot.com/2012/10/linux-kernel-arraysize.html) ,從 Linux 程式碼中尋找 `ARRAY_SIZE` 時,可以發現它定義在 [linux/kernel.h](https://github.com/torvalds/linux/blob/master/include/linux/kernel.h) 中: ```c=38 /** * ARRAY_SIZE - get the number of elements in array @arr * @arr: array to be sized */ #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) + __must_be_array(arr)) ``` 前面並不難理解,計算 `(sizeof(arr) / sizeof((arr)[0])` 就是因為要根據不同的 type 去計算元素個數,因此若 arr 佔有的連續 byte 數為 `(arr)[0]` (即一個元素)佔有 byte 數的 k 倍顯然結果就是 k 。 不過 Macro 有一個很危險的特質是它只會將 define 後面的敘述做帶入的動作,若使用 Macro 做了相對複雜的操作,他是不會幫你檢查程式碼的正確性的,因此要是使用 `ARRAY_SIZE` 這個 Macro 的人不夠謹慎,把錯誤的參數傳入是容易出錯的,因此 Macro 中有時會加入一些檢查用的機制防止誤用。 顯然 `__must_be_array(arr)` 由名字來看就是表明 `arr` 本身必須是一個 array (連續的記憶體空間),但為什麼是用「加」的 ? 這就要參考 [linux/compiler.h](https://github.com/torvalds/linux/blob/master/include/linux/compiler.h) 的程式碼: ```c=239 /* &a[0] degrades to a pointer: a different type from an array */ #define __must_be_array(a) BUILD_BUG_ON_ZERO(__same_type((a), &(a)[0])) ``` 此時又發現了 `BUILD_BUG_ON_ZERO` 和 `__same_type` 兩個關鍵字,以下來分別探討。 1. 首先先看 `BUILD_BUG_ON_ZERO` ,參考自 [linux/build_bug.h](https://elixir.bootlin.com/linux/latest/source/include/linux/build_bug.h#L16) ```c=7 `#ifdef __CHECKER__ #define BUILD_BUG_ON_ZERO(e) (0) #else /* __CHECKER__ */ /* * Force a compilation error if condition is true, but also produce a * result (of value 0 and type int), so the expression can be used * e.g. in a structure initializer (or where-ever else comma expressions * aren't permitted). */ #define BUILD_BUG_ON_ZERO(e) ((int)(sizeof(struct { int:(-!!(e)); }))) #endif /* __CHECKER__ */` ``` 從註解可以理解到當 `!!(e)` 這個 condition 為 true 時會造成 compilation error ,先探討這個 condition 的意義: - `!(e)` 有兩種值,若 `e` 為 0 結果為 true(1) ; 反之 `e` 只要非 0 結果就是 0(false) 。 - `!!(e)` 就是再把 `!(e)` 做一次 Logical negation (NOT) - `-!!(e)` 的結果就是: `e` 為 0 則算出 0 ; `e` 為非 0 則算出 -1 而搭配 c 語言 [Bit-field](https://en.wikipedia.org/wiki/Bit_field) 的技巧搭配程式碼來看 `struct { int:(-!!(e)); }` ```c= // bit field box properties struct BoxProps { unsigned int opaque : 1; unsigned int fill_color : 3; unsigned int : 4; // fill to 8 bits unsigned int show_border : 1; unsigned int border_color : 3; unsigned int border_style : 2; unsigned char : 0; // fill to nearest byte (16 bits) unsigned char width : 4, // Split a byte into 2 fields of 4 bits height : 4; }; ``` 以上程式碼引用自維基百科,可以發現冒號後面 (:) 可以直接指定變數使用的 bit 數,如 `opaque` 只用 1 bit,而 `fill_color` 佔用 3 bits,而因為記憶體對齊 (alignment) 所以會有 `fill to 8 bits` 等操作(這邊不贅述)。 而顯然指定記憶體的 bit 數必須是一個非負整數,而再看回 `struct { int:(-!!(e)); }` 可以發現當 condition 為 true 時會指定一個 bit 數為 -1 的結構成員,這會引發 compilation error ; 而厲害的是可以指定 bit 數為 0 的成員,其主要的功能是強制將記憶體對齊至下一個 word 邊界 (Force alignment at the next word boundary),如上面程式碼的 `unsigned char : 0;`,而且不會佔任何的空間。到這邊似乎對前面程式碼越來越有頭緒了。 2. 接著來看 `__same_type((a), &(a)[0])` ,參考 [linux/compiler_type.h](https://elixir.bootlin.com/linux/latest/source/include/linux/compiler_types.h#L264) ```c=264 /* Are two types/vars the same type (ignoring qualifiers)? */ #define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b)) ``` 可以看到 `__same_type()` 呼叫了 GCC 的 built-in function:[`__builtin_types_compatible_p`](https://gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Other-Builtins.html#Other-Builtins) > You can use the built-in function __builtin_types_compatible_p to determine whether two types are the same. This built-in function returns 1 if the unqualified versions of the types type1 and type2 (which are types, not expressions) are compatible, 0 otherwise. The result of this built-in function can be used in integer constant expressions. 可以得知當 `type1` 和 `type2` 一致會回傳 1 否則為 0 ,而在範例中 `a` 如果是 array([]) 則 `__builtin_types_compatible_p(typeof(a), typeof(&(a)[0]))` 會回傳 0;`a` 如果是 pointer(*) 則回傳 1 。 總結:看回 `ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) + __must_be_array(arr))` ,顯然 arr 為 array([]) 則 `__must_be_array(arr)` 為 0 不影響結果、而 arr 為 pointer(*) 時會直接導致編譯不通過,而可能有人會問為什麼不用 `assert` 就好,主要是 `assert` 是在 runtime 才會提醒出錯、但發生 runtime error 是有機率性的,若不能儘早發現錯誤會加重 debug 的難度,考量到安全性、可維護性因此必須讓它在 compile time 就過不了。 雖然費了很大的心力只為了解釋兩行程式碼,但從過程中也看到了許多 linux 核心的風格,也接觸到了 c 語言的許多技巧,從中也知道自己的不足,可謂相當有收穫。 #### DEFINE_HASHTABLE, DEFINE_READ_MOSTLY_HASHTABLE, DECLARE_HASHTABLE - RCU - Safe ## Project : NLP vector generator ### 動機 [自然語言處理 (NLP)](https://zh.wikipedia.org/wiki/%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E5%A4%84%E7%90%86)屬於機器學習的一個分支,主要目的是希望電腦可以對自然語言(中文、英文)等經過訓練而產生認知語言的能力,主要方式是讓電腦把輸入的語言變成有意思的符號和關係,然後根據目的再處理。自然語言生成系統則是把計算機數據轉化為自然語言,常見的應用如我們熟知的 [Siri](https://zh.wikipedia.org/wiki/Siri) ,聊天機器人等。 有鑑於此題目和 NLP 都有「將輸入 (key 值) mapping 到某個值域(數字、特徵向量等)」的特性,而事實上 NLP 也有將語言經過 hash function 轉成特徵輸入的作法,而一般而言 hash 的過程大都使用現成的 library,且機器學習大多都以 python 等高階語言入門,能夠接觸到的部分幾乎都是調參數、對資料預處理 (preprocessing) 等很上層的領域。因此藉由本次實作機會想試試看結合 Linux 核心對 hash 的實作和字串特徵擷取,嘗試去實現以字串搭配 fabonacci hashing 的機制產出 NLP 的特徵向量產生器,以不同的面相切入當紅的機器學習領域。 ### 前置作業一 : 延伸實作出 string based 的 hash function #### 定義新型態、修改原本型態 `map_t` 新增 `type` 去區分 hashtable 做 hash 的型態新增 `strut hash_skey` ,跟 `struct hash_key` 的主要差別在於 key 變成了字串的形式 ```c= typedef struct { char type; // 'i' for integer hashing, 's' for string hashing int bits; struct hlist_head *ht; } map_t; struct hash_key { int key; void *data; struct hlist_node node; }; struct hash_skey { char * skey; void *data; struct hlist_node node; }; ``` (Todo) ```c= // check type #define TYPE_CONSISTENT(map, t) \ (map && map->type == t) ? 1 : 0 void map_adds(map_t *map, char *skey, void *data) { struct hash_skey *kn = find_skey(map, skey); if (kn) return; kn = (struct hash_skey*)malloc(sizeof(struct hash_skey)); if (!kn) // add return; size_t len = strlen(skey) + 1; kn->skey = (char *)malloc(sizeof(char) * len); if (!kn->skey){ free(kn); return; } strncpy(kn->skey, skey, len); kn->data = data; struct hlist_head *h = &map->ht[shash(skey, map->bits)]; struct hlist_node *n = &kn->node, *first = h->first; n->next = first; if (first) first->pprev = &n->next; h->first = n; n->pprev = &h->first; } ``` #### 字串版本的 hash function ```c= static inline unsigned int shash(const char *skey, unsigned int bits) { if (!skey) return 0; unsigned int ret = 1, len = strlen(skey); for (int i = 0; i < len; i++) ret = (ret * skey[i] * GOLDEN_RATIO_32) >> (32 - bits); return ret; } ``` (Todo) ### 前置作業二 : 檢驗 Scalability 檢驗看是否字串對於 Golden Ratio 為基礎的 hash 也有跟整數一樣的表現 (Todo) ```shell key : "&"8XL'*N3Y#", value : 40 key : "D$SOxkTq'R3Uc+'9", value : 505 key : "GR?w@{qFqr[CP2.:B(dl??", value : 378 key : "O0rL**cdsmTc"3dxgp", value : 195 key : "hbtmwMK6qv)1i*"K;sT&0o:", value : 689 key : "]yx5+!kkZc%25", value : 174 key : ";t`fprd$&hVa?lp,0Vg"QM", value : 347 ... ``` (Todo) ![](https://i.imgur.com/H4Egg1h.png) - number of key = 1024 ![](https://i.imgur.com/uMGWe92.png) - number of key = 2048 ![](https://i.imgur.com/lRoOxIK.png) - number of key = 4096 ![](https://i.imgur.com/t34FJpP.png) ## 參考資料 - 有效率的 hash table 實做 - https://medium.com/@fchern/%E8%A8%AD%E8%A8%88%E9%AB%98%E6%95%88%E8%83%BD%E7%9A%84hash-table-%E4%B8%80-303d9713abab - [Faster Hash Table Probing](https://craftinginterpreters.com/optimization.html#faster-hash-table-probing) - list, hlist, rcu_list - https://ithelp.ithome.com.tw/articles/10219190 - https://danielmaker.github.io/blog/linux/list_hlist_hashtable.html - https://www.programmersought.com/article/34182729928/ - golden ratio - https://stackoverflow.com/questions/38994306/what-is-the-meaning-of-0x61c88647-constant-in-threadlocal-java - https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/ - hashtable.h 相關討論 - http://frankchang0125.blogspot.com/2012/10/linux-kernel-buildbugonzero.html - RCU 機制 - [Linux 核心設計: RCU 同步機制](https://hackmd.io/@sysprog/linux-rcu) - [What is RCU, Fundamentally?](https://lwn.net/Articles/262464/)