源码阅读 libevent - 数据结构:哈希表
libevent
源码中除了 queue.h
文件中定义了 5 种数据结构,在 ht-internal.h
文件中定义了另一个重要的数据结构:哈希表。
本系列大部分文章介绍
linux
系统下libevent
的源码,但libevent
在linux
环境下并没有用到哈希表结构,但学习libevent
中哈希表的实现非常有助于对哈希表结构的理解。
哈希表定义
哈希表定义宏
#define HT_HEAD(name, type) \ struct name { \ struct type **hth_table; /* The hash table itself. */ \ unsigned hth_table_length; /* How long is the hash table? */ \ unsigned hth_n_entries; /* How many elements does the table contain? */ \ unsigned hth_load_limit; /* How many elements will we allow in the table before resizing it? */ \ int hth_prime_idx; /* Position of hth_table_length in the primes table. */ \ } #define HT_ENTRY(type) \ struct { \ struct type *hte_next; \ unsigned hte_hash; /* Hash cache */ \ }
哈希表结构
与双向链表一样,我们通过测试程序来看哈希表在内存中的结构:
#include <stdio.h> #include <stdlib.h> #include <time.h> #include "ht-internal.h" typedef struct Node { int key; int value; HT_ENTRY(Node) map_node; } Node; HT_HEAD(INT_MAP, Node); static inline unsigned hashfn(struct Node *e) { unsigned h = (unsigned) e->key; h += (h>> 2) | (h << 30); return h; } static inline int eqfn(struct Node *e1, struct Node *e2) { return e1->key == e2->key; } HT_PROTOTYPE(INT_MAP, Node, map_node, hashfn, eqfn) HT_GENERATE(INT_MAP, Node, map_node, hashfn, eqfn, 0.5, malloc, realloc, free) int main(void) { struct INT_MAP table; HT_INIT(INT_MAP, &table); int a = 0; for (int i = 0; i < 20; i++) { srand(time(NULL) + a % 1000003); a = rand() % 1000003; Node* new_item = malloc(sizeof(Node)); new_item->key = a; new_item->value = i; HT_INSERT(INT_MAP, &table, new_item); } printf("\n[HEAD]: **hth_table = %p\n" "hth_table_length = %d\n" "hth_n_entries = %d\n" "hth_load_limit = %d\n" "hth_prime_idx = %d\n\n", table.hth_table, table.hth_table_length, table.hth_n_entries, table.hth_load_limit, table.hth_prime_idx); for (int i = 0; i < table.hth_table_length; i++) { if (table.hth_table[i]) { printf("[TABLE]: [%03d] - [%p]", i, table.hth_table[i]); Node* item = table.hth_table[i]->map_node.hte_next; while (item) { printf("--> [%p]", item); item = item->map_node.hte_next; } printf("\n"); } } Node** item; HT_FOREACH(item, INT_MAP, &table) { printf("[NODE]: Key = %-8d Value = %-5d Next = %-10p Addr = %-10p\n", (*item)->key, (*item)->value, (*item)->map_node.hte_next, *item); } return 0; }
程序执行结果如下:

哈希表头
可以看出,libevent
中定义的哈希表头结构体意义如下:
hth_table
:哈希表的存储位置hth_table_length
:哈希表的长度hth_n_entries
:哈希表的元素个数hth_load_limit
:哈希表元素限制,超过限制后需要进行哈希表扩容hth_prime_idx
:哈希表容量等级,libevent 的哈希表存在 26 个容量等级
哈希冲突的解决
libevent
的哈希表是使用链地址法解决冲突问题的,这一点可以从 hth_talbe
成员变量看到。它是一个二级指针,其指向了哈希表的元素所在的地址。
哈希函数
libevent
中的哈希函数由使用者定义,上述测试代码中使用了和 libevent
中一样的哈希函数:模 (%)。
libevent
中哈希表的代码存在大量的宏定义,可读性低,本文通过gcc
的-E
选项结合AStyle
将上文中的测试程序转化为了可读性较好的代码,以下分析均采用转化后的代码片段。
哈希表的访问
查找
static inline struct Node ** INT_MAP_HT_FIND_P_(struct INT_MAP *head, struct Node *elm) { struct Node **p; if (!head->hth_table) return NULL; p = &((head)->hth_table[((elm)->map_node.hte_hash) % head->hth_table_length]); while (*p) { if (eqfn(*p, elm)) return p; p = &(*p)->map_node.hte_next; } return p; } static inline struct Node * INT_MAP_HT_FIND(const struct INT_MAP *head, struct Node *elm) { struct Node **p; struct INT_MAP *h = (struct INT_MAP *) head; do { (elm)->map_node.hte_hash = hashfn(elm); } while (0); p = INT_MAP_HT_FIND_P_(h, elm); return p ? *p : NULL; }
INT_MAP_HT_FIND_P_
和 INT_MAP_HT_FIND
均为哈希表的查找函数,两者的区别是:
- 返回数据类型不同:
INT_MAP_HT_FIND_P_
返回值是 Node**,是一个二级指针,指向的是保存被查找哈希元素的地址的指针变量 > 比如:hth_table[1]
中保存着带查找元素的地址,该函数则返回的是hth_table[1]
的地址信息。INT_MAP_HT_FIND
返回值是 Node*,指向查找到的哈希元素
- 查找步骤差别:
INT_MAP_HT_FIND_P_
直接使用计算过的哈希值进行查找INT_MAP_HT_FIND
重新使用哈希函数计算哈希值后进行查找
- 判断查找成功与否方式不同:
INT_MAP_HT_FIND_P_
返回值始终非空,需要判断其返回值指向的数据是否为NULL
来确认查找结果INT_MAP_HT_FIND_P_
查找失败时,返回的地址可以作为该元素插入的地址进行插入操作INT_MAP_HT_FIND
未查找到对应元素时返回值为NULL
遍历
static inline struct Node ** INT_MAP_HT_START(struct INT_MAP *head) { unsigned b = 0; while (b < head->hth_table_length) { if (head->hth_table[b]) return &head->hth_table[b]; ++b; } return NULL; } static inline struct Node ** INT_MAP_HT_NEXT(struct INT_MAP *head, struct Node **elm) { if ((*elm)->map_node.hte_next) return &(*elm)->map_node.hte_next; else { unsigned b = (((*elm)->map_node.hte_hash) % head->hth_table_length)+1; while (b < head->hth_table_length) { if (head->hth_table[b]) return &head->hth_table[b]; ++b; } return NULL; } }
遍历的三要素:
- 遍历的起始节点
- 依次查找哈希表数组中的每一个元素,首个非空的元素即为起始节点
- 下一个节点的获取
- 如果当前节点的
next
指针非空,则next
指针指向的位置为下一个节点 - 如果当前节点的
next
指针为空,则继续遍历哈希表数组中的元素
- 如果当前节点的
- 遍历的结束条件
- 如果遍历完哈希表数组遍历完成(根据哈希表长度值
hth_table_length
判断),则遍历结束
- 如果遍历完哈希表数组遍历完成(根据哈希表长度值
哈希表的操作
哈希表的基础操作
/* 哈希表的判空 */ #define HT_EMPTY(head) ((head)->hth_n_entries == 0) /* 哈希表的表数组大小 */ #define HT_SIZE(head) ((head)->hth_n_entries) /* 哈希表的表占用的内存大小 */ #define HT_MEM_USAGE(head) (sizeof(*head) + (head)->hth_table_length * sizeof(void*))
以上接口较为简单,不进行过多说明。
初始化
static inline void INT_MAP_HT_INIT(struct INT_MAP *head) { head->hth_table_length = 0; head->hth_table = NULL; head->hth_n_entries = 0; head->hth_load_limit = 0; head->hth_prime_idx = -1; }
哈希表的初始化其实就是对表头数据进行初始化。
插入
static inline void INT_MAP_HT_INSERT(struct INT_MAP *head, struct Node *elm) { struct Node **p; if (!head->hth_table || head->hth_n_entries >= head->hth_load_limit) INT_MAP_HT_GROW(head, head->hth_n_entries + 1); ++head->hth_n_entries; do { (elm)->map_node.hte_hash = hashfn(elm); } while (0); p = &((head)->hth_table[((elm)->map_node.hte_hash) % head->hth_table_length]); elm->map_node.hte_next = *p; *p = elm; }
插入操作可分为 3
~ 4
个步骤:
- 判断当前哈希表是否需要扩容
- 调用哈希函数计算新插入数据的哈希值
- *(需要时) 解决哈希冲突
- 头插法插入新数据(后插入的数据在链表头)
需要注意的是:
libevent
的插入函数INT_MAP_HT_INSERT
并没有对重复的哈希key
进行判断,所以根据不同的应用场景,可能会有不同的应用方式。
修改 & 插入
static inline struct Node * INT_MAP_HT_REPLACE(struct INT_MAP *head, struct Node *elm) { struct Node **p, *r; if (!head->hth_table || head->hth_n_entries >= head->hth_load_limit) INT_MAP_HT_GROW(head, head->hth_n_entries + 1); do { (elm)->map_node.hte_hash = hashfn(elm); } while (0); p = INT_MAP_HT_FIND_P_(head, elm); r = *p; *p = elm; if (r && (r!=elm)) { elm->map_node.hte_next = r->map_node.hte_next; r->map_node.hte_next = NULL; return r; } else { ++head->hth_n_entries; return NULL; } }
INT_MAP_HT_REPLACE
函数相比 INT_MAP_HT_INSERT
,多了对于重复数据的查找和替换。
需要注意的是:
INT_MAP_HT_REPLACE
在插入时返回值为NULL
,而在替换时返回值为被替换元素的地址,如果被替换元素空间是通过malloc
等内存申请函数申请得到的,需要调用free
等函数对空间进行释放。
删除
static inline struct Node * INT_MAP_HT_REMOVE(struct INT_MAP *head, struct Node *elm) { struct Node **p, *r; do { (elm)->map_node.hte_hash = hashfn(elm); } while (0); p = INT_MAP_HT_FIND_P_(head,elm); if (!p || !*p) return NULL; r = *p; *p = r->map_node.hte_next; r->map_node.hte_next = NULL; --head->hth_n_entries; return r; } static inline struct Node ** INT_MAP_HT_NEXT_RMV(struct INT_MAP *head, struct Node **elm) { unsigned h = ((*elm)->map_node.hte_hash); *elm = (*elm)->map_node.hte_next; --head->hth_n_entries; if (*elm) return elm; else { unsigned b = (h % head->hth_table_length)+1; while (b < head->hth_table_length) { if (head->hth_table[b]) return &head->hth_table[b]; ++b; } return NULL; } }
删除操作本质就是一次查找与一次数组或链表操作,两个删除函数作用类似,但返回值存在区别:
INT_MAP_HT_REMOVE
和INT_MAP_HT_REPLACE
类似,返回值是被删除元素的地址。INT_MAP_HT_NEXT_RMV
和INT_MAP_HT_FIND_P_
类似,返回的是一个二级指针,指向的是保存被删除哈希元素的下一个元素地址的指针变量。 > 比如:hth_table[0]
中保存着被删除元素的地址,hth_table[3]
中保存着被删除元素的下一个元素地址,则该函数会删除hth_table[0]
所指向元素的地址,并返回的hth_table[3]
的地址信息。
两个删除操作均不会进行被删除元素的内存释放操作:
INT_MAP_HT_REMOVE
返回被删除元素的地址,可通过返回值释放内存。INT_MAP_HT_NEXT_RMV
需要在删除前保存被删除元素的地址信息,然后在删除操作之后释放内存。
参数检查
static inline void INT_MAP_HT_FOREACH_FN(struct INT_MAP *head, int (*fn)(struct Node *, void *), void *data) { unsigned idx; struct Node **p, **nextp, *next; if (!head->hth_table) return; for (idx=0; idx < head->hth_table_length; ++idx) { p = &head->hth_table[idx]; while (*p) { nextp = &(*p)->map_node.hte_next; next = *nextp; if (fn(*p, data)) { --head->hth_n_entries; *p = next; } else p = nextp; } } }
调用 fn
函数检查每个哈希表中的元素,如果 fn
函数返回值非 0
,则删除对应的元素。
注:如有需要,
fn
函数中应对被删除元素进行内存释放
销毁
void INT_MAP_HT_CLEAR(struct INT_MAP *head) { if (head->hth_table) free(head->hth_table); INT_MAP_HT_INIT(head); }
该函数需要在哈希表清空后使用,否则会丢失哈希表中元素信息,导致一些 malloc
的空间无法释放,例如:
/* 自定义函数,非 libenent 宏定义生成函数 */ void INT_MAP_HT_CLEAR_ALL(struct INT_MAP * head) { struct Node **ent, **next, *this; for (ent = HT_START(INT_MAP, head); ent; ent = next) { this = *ent; next = HT_NEXT_RMV(INT_MAP, head, ent); mm_free(this); } HT_CLEAR(INT_MAP, head); }
扩容
static unsigned INT_MAP_PRIMES[] = { 53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593, 49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 6291469, 12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741 }; static unsigned INT_MAP_N_PRIMES = (unsigned)(sizeof(INT_MAP_PRIMES)/sizeof(INT_MAP_PRIMES[0])); int INT_MAP_HT_GROW(struct INT_MAP *head, unsigned size) { unsigned new_len, new_load_limit; int prime_idx; struct Node **new_table; if (head->hth_prime_idx == (int)INT_MAP_N_PRIMES - 1) return 0; if (head->hth_load_limit > size) return 0; prime_idx = head->hth_prime_idx; do { new_len = INT_MAP_PRIMES[++prime_idx]; new_load_limit = (unsigned)(0.5*new_len); /* 0.5 为装载因子 */ } while (new_load_limit <= size && prime_idx < (int)INT_MAP_N_PRIMES); if ((new_table = malloc(new_len*sizeof(struct Node*)))) { unsigned b; memset(new_table, 0, new_len*sizeof(struct Node*)); for (b = 0; b < head->hth_table_length; ++b) { struct Node *elm, *next; unsigned b2; elm = head->hth_table[b]; while (elm) { next = elm->map_node.hte_next; b2 = ((elm)->map_node.hte_hash) % new_len; elm->map_node.hte_next = new_table[b2]; new_table[b2] = elm; elm = next; } } if (head->hth_table) free(head->hth_table); head->hth_table = new_table; } else { unsigned b, b2; new_table = realloc(head->hth_table, new_len*sizeof(struct Node*)); if (!new_table) return -1; memset(new_table + head->hth_table_length, 0, (new_len - head->hth_table_length)*sizeof(struct Node*)); for (b = 0; b < head->hth_table_length; ++b) { struct Node *e, **pE; for (pE = &new_table[b], e = *pE; e != NULL; e = *pE) { b2 = ((e)->map_node.hte_hash) % new_len; if (b2 == b) pE = &e->map_node.hte_next; else { *pE = e->map_node.hte_next; e->map_node.hte_next = new_table[b2]; new_table[b2] = e; } } } head->hth_table = new_table; } head->hth_table_length = new_len; head->hth_prime_idx = prime_idx; head->hth_load_limit = new_load_limit; return 0; }
哈希表的扩容分为以下几个步骤:
- 根据装载因子、当前容量、当前数据量判断是否需要扩容
- 获取当前容量等级并计算需要扩容的等级
- 申请新的哈希表空间
- 重建哈希表,并释放旧空间
- 更新哈希表头信息
libevent
采用的扩容方法方法的缺点是,容量扩张是一次完成的,期间要花很长时间一次转移hash
表中的所有元素。这样在 hash 表每次扩容时,往里边插入一个元素将会等候很长的时间。