--- tags: linux2022 --- # 2022q1 Homework1 (quiz1) contributed by < `SmallHanley` > > [linux2022-quiz1](https://hackmd.io/@sysprog/linux2022-quiz1) ## 測驗 `1` :::success 解釋上述程式碼運作原理 ::: 首先是資料結構的部分: ```c struct hlist_node { struct hlist_node *next, **pprev; }; struct hlist_head { struct hlist_node *first; }; typedef struct { int bits; struct hlist_head *ht; } map_t; struct hash_key { int key; void *data; struct hlist_node node; }; ``` `struct hlist_node` 這個結構體包含指標 `next` ,用來指向下一個節點,以及指標的指標 `prev`,指向前一個節點的 `next` 指標,可以在接到 `hlist_head` 時不用額外判斷,避免分支。 :::warning 這裡不懂開發者選擇使用雙向鏈結串列的考量,查看 history 最早出現 `hash_node` 是在 [v2.5.64](https://elixir.bootlin.com/linux/v2.5.64/A/ident/hlist_node),github commit history 沒有這麼舊的資料,[ChangeLog-2.5.64](https://cdn.kernel.org/pub/linux/kernel/v2.5/ChangeLog-2.5.64) 也沒有查到有用的資訊。 ::: `struct hlist_head` 結構體則是 hash table 各個 bucket 的 head,其中 `first` 指標指向第一個 `struct hlist_node` 節點。 而 `map_t` 是 hash table,`bits` 代表 bucket 的多寡 (以多少位元表示),因此 bucket 的數量為 2 的冪。`ht` 則是一個陣列,用來存放每個 bucket 的 head。 `struct hash_key` 結構體是用來包裝每個 key-value pair,key 會根據 hash function 找到對應的 bucket,該 bucket 的 `struct hlist_head` 會串接所有映射到這個 bucket 的 `struct hlist_node`,示意圖如下: ```graphviz digraph G { rankdir=LR; node[shape=record]; map [label="hlist_head.first |<ht0> |<ht1> |<ht2> |<ht3> |<ht4> |<ht5> |<ht7> |<ht8> "]; node[shape=none] null1 [label=NULL] null2 [label=NULL] subgraph cluster_1 { style=filled; color=lightgrey; node [shape=record]; hn1 [label="hlist_node | {<prev>pprev | <next>next}"]; label="hash_key 1" } subgraph cluster_2 { style=filled; color=lightgrey; node [shape=record]; hn2 [label="hlist_node | {<prev>pprev | <next>next}"]; label="hash_key 2" } subgraph cluster_3 { style=filled; color=lightgrey; node [shape=record]; hn3 [label="hlist_node | {<prev>pprev | <next>next}"]; label="hash_key 3" } map:ht1 -> hn1 hn1:next -> hn2 hn2:next -> null1 hn2:prev:s -> hn1:next:s map:ht5 -> hn3 hn3:next -> null2 hn1:prev:s -> map:ht1 hn3:prev:s -> map:ht5 } ``` * **map_init()** 根據傳進來的 `bits` 來決定 hash table 的大小,`bits` 代表 bucket 的多寡 (以多少位元表示),因此 hash table 的 size 為 2 的冪。配置 `map_t` 以及 `map->ht` 的空間,並將所有 entry 的 `first` 指標指向 `NULL`。 * **hash()** 即 hash function 的所在,這裡使用 magic number `0x61C88647`,看後續分析 linux kernel 相關實作會不會探討,這裡先跳過。 * **find_key()** 首先根據 key 的值,透過 hash function 映射到 hash table 的其中一個 bucket,並用 for 迴圈走訪每一個 `struct hlist_node` 結構體,如果該結構體的 `key` 是我們要找的,則回傳該結構體,若找不到則回傳 `NULL`。 * **map_get()** 先呼叫 `find_key()` 取得目標結構體,並回傳其 `data`。 * **map_add()** 插入新的 key-data pair 到 hash table,如果該 key 已經存在,則不做任何事情。 * **map_deinit()** 釋放 hash table 相關的資源。 :::success 研讀 Linux 核心原始程式碼 [include/linux/hashtable.h](https://github.com/torvalds/linux/blob/master/include/linux/hashtable.h) 及對應的文件 [How does the kernel implements Hashtables?](https://kernelnewbies.org/FAQ/Hashtables),解釋 hash table 的設計和實作手法,並留意到 [tools/include/linux/hash.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/hash.h) 的 `GOLDEN_RATIO_PRIME`,探討其實作考量 ::: 光是 hash table 的 define 就有分三種: ```c #define DEFINE_HASHTABLE(name, bits) \ struct hlist_head name[1 << (bits)] = \ { [0 ... ((1 << (bits)) - 1)] = HLIST_HEAD_INIT } #define DEFINE_READ_MOSTLY_HASHTABLE(name, bits) \ struct hlist_head name[1 << (bits)] __read_mostly = \ { [0 ... ((1 << (bits)) - 1)] = HLIST_HEAD_INIT } #define DECLARE_HASHTABLE(name, bits) \ struct hlist_head name[1 << (bits)] ``` 中間多了 `__read_mostly`,追蹤相關程式碼,發現 `__read_mostly` 的定義是被分散在不同處理器架構的 cache.h (arch/xxx/include/asm/cache.h),其中,有部分架構定義如下,包含 `arm`、`arm64`、`ia64`、`mips`、`parisc`、`powerpc`、`s390`、`sh`、`sparc`、`x86`: ```c #define __read_mostly __section(".data..read_mostly") ``` 其中 `__section` 又被定義在 [include/linux/compiler_attributes.h](https://elixir.bootlin.com/linux/latest/source/include/linux/compiler_attributes.h#L276) 中,在 [GNU C and C++](https://gcc.gnu.org/) 中,可以用來描述函式的特定性質 (參見 [Declaring Attributes of Functions](https://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html))。而這裡使用到的是 `section` 這個 attribute (參見 [Function-Attributes-section](https://gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Function-Attributes.html)),概念上是將一段程式放置在指定的 special section,推測跟 cache 的相關優化有關。 ### hashtable.h Linux hashtable.h 有 include `linux/rculist.h`,可以參考 linux Documentation [rcu.rst](https://github.com/torvalds/linux/blob/master/Documentation/RCU/rcu.rst)、[listRCU.rst](https://github.com/torvalds/linux/blob/master/Documentation/RCU/listRCU.rst)。 **重點整理:** * RCU (read-copy update) 的概念是將 destructive operation 分成兩個部分,第一是將要 destroy 的資料隱藏不讓其他 reader 看到,第二是實際執行 destruction 的動作。而兩個部分中間有一段時間稱作 "grace period",grace period 需要足夠長使得所有 reader drop the reference。 * RCU 在鏈結串列的操作之一,流程上是將節點從鏈結串列中移除,等待 grace period,然後釋放對應空間。 * 使用情境:==待補充== * Read-mostly list: Deferred Destruction * Read-Side Action Taken Outside of Lock: No In-Place Updates * Handling In-Place Updates * Eliminating Stale Data * Skipping Stale Objects #### hash_init() * 將 hash table 初始化。 * 從上面示意圖可以知道 hash table 是一個 `struct hlist_head` 組成的陣列,每個 `struct hlist_head` 是映射到同一個 bucket 的鏈結串列的 head。將每個結構體的成員 `first` 初始化成 `NULL`。 ```c // linux/list.h #define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL) // linux/hashtable.h static inline void __hash_init(struct hlist_head *ht, unsigned int sz) { unsigned int i; for (i = 0; i < sz; i++) INIT_HLIST_HEAD(&ht[i]); } #define hash_init(hashtable) __hash_init(hashtable, HASH_SIZE(hashtable)) ``` #### hash_add() ```c // linux/hashtable.h #define hash_add(hashtable, node, key) \ hlist_add_head(node, &hashtable[hash_min(key, HASH_BITS(hashtable))]) // linux/list.h static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) { struct hlist_node *first = h->first; WRITE_ONCE(n->next, first); if (first) WRITE_ONCE(first->pprev, &n->next); WRITE_ONCE(h->first, n); WRITE_ONCE(n->pprev, &h->first); } ``` [commit c54a274](https://github.com/torvalds/linux/commit/c54a2744497db4b6887b9c905ef7aa0b3620c956) > Also add various WRITE_ONCE() in __hlist_del(), hlist_add_head() and hlist_add_before()/hlist_add_behind() to pair with the READ_ONCE(). ## hash.h 在 [tools/include/linux/hash.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/hash.h) 可以發現一個巨集 `GOLDEN_RATIO_PRIME`, ```c /* * Knuth recommends primes in approximately golden ratio to the maximum * integer representable by a machine word for multiplicative hashing. * Chuck Lever verified the effectiveness of this technique: * http://www.citi.umich.edu/techreports/reports/citi-tr-00-1.pdf * * These primes are chosen to be bit-sparse, that is operations on * them can use shifts and additions instead of multiplications for * machines where multiplications are slow. */ #if BITS_PER_LONG == 32 /* 2^31 + 2^29 - 2^25 + 2^22 - 2^19 - 2^16 + 1 */ #define GOLDEN_RATIO_PRIME 0x9e370001UL #elif BITS_PER_LONG == 64 /* 2^63 + 2^61 - 2^57 + 2^54 - 2^51 - 2^18 + 1 */ #define GOLDEN_RATIO_PRIME 0x9e37fffffffc0001UL #else #error Define GOLDEN_RATIO_PRIME for your wordsize. #endif ``` 在 [commit 689de1d](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/include/linux/hash.h?id=689de1d6ca95b3b5bd8ee446863bf81a4883ea25) 後改成以下實作版本: ```c /* * This hash multiplies the input by a large odd number and takes the * high bits. Since multiplication propagates changes to the most * significant end only, it is essential that the high bits of the * product be used for the hash value. * * Chuck Lever verified the effectiveness of this technique: * http://www.citi.umich.edu/techreports/reports/citi-tr-00-1.pdf * * Although a random odd number will do, it turns out that the golden * ratio phi = (sqrt(5)-1)/2, or its negative, has particularly nice * properties. (See Knuth vol 3, section 6.4, exercise 9.) * * 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. */ #define GOLDEN_RATIO_32 0x61C88647 #define GOLDEN_RATIO_64 0x61C8864680B583EBull ``` 其中有討論到有關 `GOLDEN_RATIO_PRIME` 的實作考量,出自於 The Art of Computer Programming (Knuth vol 3, section 6.4, exercise 9) 這本書,以下取自這本書的片段。 **Theorem S.** Let $\theta$ be any irrational number. When the points {$\theta$}, {$2\theta$}, ..., {$n\theta$} are placed in the line segment [0 ... 1], then $n + 1$ line segments formed have at most three different lengths. Moreover, the next point {$(n + 1)\theta$} will fall in one of the largest existing segments. (用 Three-gap theorem 推導。) :::info {$n \theta$} 代表 $n \theta$ 的小數部分。 ::: 之所以要取小數部分,原理是這個分割問題可以 model 成 [Three-gap theorem](https://en.wikipedia.org/wiki/Three-gap_theorem)。line segment [0 … 1] 對應到 Three-gap theorem 中周長為 1 的圓,$n \theta$ 的整數部分相當於繞了圓多少圈 (即在 line segment 循環多少次),而小數部分相當於新的點坐落於圓上的哪一個位置 (即 line segment 中新分割點的位置)。 有關 **Theorem S** 的證明在 exercise 8 ![](https://i.imgur.com/s2klaYD.jpg) ![](https://i.imgur.com/uwOsYPZ.jpg) 任意數乘上這兩個數再取其高位元,可呈現好的數值分布。實際的 hash function (舉 32-bit 為例) 是定義在 [tools/include/linux/hash.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/hash.h#L57-L69): ```c= #ifndef HAVE_ARCH__HASH_32 #define __hash_32 __hash_32_generic #endif static inline u32 __hash_32_generic(u32 val) { return val * GOLDEN_RATIO_32; } static inline u32 hash_32(u32 val, unsigned int bits) { /* High bits are more random, so use them. */ return __hash_32(val) >> (32 - bits); } ``` 上述的實作是將 0 ~ 1 的浮點數以 32-bit 無號整數表示。第 6 行將欲進行 hash 的數值 `val` 乘上 `GOLDEN_RATIO_32` ,第 12 行是取最高的 `bits` 個位元。對應上述 Fibonacci Hashing 理論,`val` 對應到 $n$,`GOLDEN_RATIO_32` 為 $\phi^{-1}$ 以 32-bit 無號整數表示,兩者相乘,因為會產生溢位 (一次溢位相當於 Three-gap theorem 中繞了圓一圈),相乘的結果為 {$n \phi^{-1}$} 以 32-bit 無號整數表示,取其高位元,則相當於取小數前幾位數。 ## 實驗 實際對比新、舊版本的 `GOLDEN_RATIO_PRIME` 實作,新版的實作可以讓 hash function 結果的分佈更為均勻。以下分別測試 0 到 1000、10000 映射到 hash table 不同 locations 的次數,hash table 的大小為 1024 (10 bits): ![](https://i.imgur.com/rKQvfVb.png) ![](https://i.imgur.com/VqL1geo.png) ## Reference [並行程式設計: Atomics 操作](https://hackmd.io/@sysprog/concurrency-atomics) [6.46 When is a Volatile Object Accessed?](https://gcc.gnu.org/onlinedocs/gcc/Volatiles.html) [Documentation/memory-barriers](https://www.kernel.org/doc/Documentation/memory-barriers.txt) [Golden_ratio](https://en.wikipedia.org/wiki/Golden_ratio) [Fibonacci_number#Relation_to_the_golden_ratio](https://en.wikipedia.org/wiki/Fibonacci_number#Relation_to_the_golden_ratio)