UThash 的数据结构
简介:
由于项目的需要,需要在一个嵌入式平台(用C语言)上用到hash map这个数据结构。于是搜索到开源的Uthash。Uthash 是一个C语言开发的hash map工具。其特点是用宏定义了所需要的对map的基本操作,如 插入、删除、查找和遍历。对应地,在uthash中采用 HASH_ADD、HASH-DELETE、HASH_FIND和HASH_ITER宏来操作,非常方便。 Uthash的高级功能也很好用,比如实现一个数据结构对应两个hash map等等,对于int型和string类型也有更简化的操作。对于Uthash的其他特性和使用方法在此不再加以赘述,请参考Uthash的指导文档,写得非常详细。下面仅仅介绍一下Uthash的数据结构。
关于uthash的数据结构,我起初是参考:
http://blog.youkuaiyun.com/devilcash/article/details/7230733
但是在仔细看了源代码之后发现这个博客的解释并不完全正确(可能是对应的版本不一样)。下面是我自己的解释(我采用的版本是1.9.8)。参考图见本文的最后部分。建议先看代码再看下图,更能加深理解。
数据结构:
//Uthash的三个数据结构:
//1. UT_hash_bucket作用提供根据hash进行索引。
typedef struct UT_hash_bucket {
struct UT_hash_handle *hh_head;
unsigned count;
unsigned expand_mult;
} UT_hash_bucket;
//2. UT_hash_table可以看做hash表的表头。
typedef struct UT_hash_table {
UT_hash_bucket *buckets;
unsigned num_buckets, log2_num_buckets;
unsigned num_items;
struct UT_hash_handle *tail; /* tail hh in app order, for fast append */
ptrdiff_t hho; /* hash handle offset (byte pos of hash handle in element */
unsigned ideal_chain_maxlen;
unsigned nonideal_items;
unsigned ineff_expands, noexpand;
uint32_t signature; /* used only to find hash tables in external analysis */
#ifdef HASH_BLOOM
uint32_t bloom_sig; /* used only to test bloom exists in external analysis */
uint8_t *bloom_bv;
char bloom_nbits;
#endif
} UT_hash_table;
//3. UT_hash_handle,用户自定义数据必须包含的结构。
typedef struct UT_hash_handle {
struct UT_hash_table *tbl;
void *prev; /* prev element in app order */
void *next; /* next element in app order */
struct UT_hash_handle *hh_prev; /* previous hh in bucket order */
struct UT_hash_handle *hh_next; /* next hh in bucket order */
void *key; /* ptr to enclosing struct's key */
unsigned keylen; /* enclosing struct's key len */
unsigned hashv; /* result of hash-fcn(key) */
} UT_hash_handle;
分析:
void *prev; /* prev element in app order */
void *next; /* next element in app order */
struct UT_hash_handle *hh_prev; /* previous hh in bucket order */
struct UT_hash_handle *hh_next; /* next hh in bucket order */
注意其注释。那么可以看到实际上在uthash中维护了两个链表,一个是app的链表,可以推测应该是按照插入的顺序连起来的。另外一个是和bucket相关的,同样可以推测该bucket应该就是UT_hash_bucket的bucket。
可以根据 HASH_ADD()操作来分析这个数据结构的用法。上面参考文章的图除了在上述四个指针处出错外,其他地方是正确的。
#define HASH_ADD(hh,head,fieldname,keylen_in,add) \
HASH_ADD_KEYPTR(hh,head,&((add)->fieldname),keylen_in,add)
#define HASH_ADD_KEYPTR(hh,head,keyptr,keylen_in,add) \
do { \
unsigned _ha_bkt; \
(add)->hh.next = NULL; \
(add)->hh.key = (char*)keyptr; \
(add)->hh.keylen = (unsigned)keylen_in; \
if (!(head)) { \
head = (add); \
(head)->hh.prev = NULL; \
HASH_MAKE_TABLE(hh,head); \
} else { \
(head)->hh.tbl->tail->next = (add); \
(add)->hh.prev = ELMT_FROM_HH((head)->hh.tbl, (head)->hh.tbl->tail); \
(head)->hh.tbl->tail = &((add)->hh); \
} \
(head)->hh.tbl->num_items++; \
(add)->hh.tbl = (head)->hh.tbl; \
HASH_FCN(keyptr,keylen_in, (head)->hh.tbl->num_buckets, \
(add)->hh.hashv, _ha_bkt); \
HASH_ADD_TO_BKT((head)->hh.tbl->buckets[_ha_bkt],&(add)->hh); \
HASH_BLOOM_ADD((head)->hh.tbl,(add)->hh.hashv); \
HASH_EMIT_KEY(hh,head,keyptr,keylen_in); \
HASH_FSCK(hh,head); \
} while(0)
对于每个用户定义的hash_map来说,包含N个用户建立的表项。在第一次HASH_ADD的时候, 内部进行了初始化(内部调用了HASH_MAKE__TABLE()操作),通过调用uthash_malloc函数, 该操作分配了1个 UT_hash_table结构体的空间,然后分配了 32 个UT_hash_bucket的空间(起始默认是32个bucket,可以通过修改宏
HASH_INITIAL_NUM_BUCKETS
来修改)。
代码是:
#define HASH_MAKE_TABLE(hh,head) \
do { \
(head)->hh.tbl = (UT_hash_table*)uthash_malloc( \
sizeof(UT_hash_table)); \
if (!((head)->hh.tbl)) { uthash_fatal( "out of memory"); } \
memset((head)->hh.tbl, 0, sizeof(UT_hash_table)); \
(head)->hh.tbl->tail = &((head)->hh); \
(head)->hh.tbl->num_buckets = HASH_INITIAL_NUM_BUCKETS; \
(head)->hh.tbl->log2_num_buckets = HASH_INITIAL_NUM_BUCKETS_LOG2; \
(head)->hh.tbl->hho = (char*)(&(head)->hh) - (char*)(head); \
(head)->hh.tbl->buckets = (UT_hash_bucket*)uthash_malloc( \
HASH_INITIAL_NUM_BUCKETS*sizeof(struct UT_hash_bucket)); \
if (! (head)->hh.tbl->buckets) { uthash_fatal( "out of memory"); } \
memset((head)->hh.tbl->buckets, 0, \
HASH_INITIAL_NUM_BUCKETS*sizeof(struct UT_hash_bucket)); \
HASH_BLOOM_MAKE((head)->hh.tbl); \
(head)->hh.tbl->signature = HASH_SIGNATURE; \
} while(0)
回到 HASH_ADD_KEYPTR中去,
分析前面部分,可以看出新建的项add 是通过 prev 和next连接进app链表关系的,也不难看出是插入到链表的后面的。对于每个新建项来说,其
(add)->hh.tbl = (head)->hh.tbl;
所以每个新建项的 tbl字段都指向 HASH_MAKE_TABLE中创建的UT_hash_table结构体。
注意UT_hash_table结构体有个字段是tail, 从HASH_ADD_KEYPTR中可以看出,这个tail总是指向app链表的尾部节点。
有tail的好处不言而喻就是为了往链表的尾部插入新节点更方便。
下面分析HASH_ADD_KEYPTR内部的HASH_ADD_TO_BKT():
/* add an item to a bucket */
#define HASH_ADD_TO_BKT(head,addhh) \
do { \
head.count++; \
(addhh)->hh_next = head.hh_head; \
(addhh)->hh_prev = NULL; \
if (head.hh_head) { (head).hh_head->hh_prev = (addhh); } \
(head).hh_head=addhh; \
if (head.count >= ((head.expand_mult+1) * HASH_BKT_CAPACITY_THRESH) \
&& (addhh)->tbl->noexpand != 1) { \
HASH_EXPAND_BUCKETS((addhh)->tbl); \
} \
} while(0)
后半部分是bucket扩展的部分,类似于很多容器的扩容操作,对此不做详细分析。仅分析前半部分。
由
(addhh)->hh_next = head.hh_head;
可以看出是插入到bucket链表的头部的,这与插入到app链表的尾部不同。然后修改头部的指针指向新的节点。
图示:
根据上面的理解不难画出下图。 为了方便看出 app链表和bucket链表,分成了两个部分。实际上是合在一块的。
这个图说明了3种结构体的关系,以及在app链表关系。新的节点插入在app链表的尾部。
这个图显示了bucket和它们的关系。假设左边的两个节点求得的hash值是一致的,那么会被分配到同样的bucket里面去。左上的节点先加入,左下的节点后加入。
可以看出节点是加入到bucket链表的头部的。
其他:
在根据key和key_len计算出hash值后,存在hashv处。通过
#define HASH_TO_BKT( hashv, num_bkts, bkt ) \
do { \
bkt = ((hashv) & ((num_bkts) - 1)); \
} while(0)
来将其分配到bucket中去的。可以看到每个用户定义的项对应于1个bucket,含有相同hashv的项对应同一个bucket,一个bucket链表里面的各个节点其hashv有可能相同也有可能不同。
#define DECLTYPE_ASSIGN(dst,src) \
do { \
(dst) = DECLTYPE(dst)(src); \
} while(0)
将src强制转化成dst的类型,再把值赋给dst;
/* calculate the element whose hash handle address is hhe */
#define ELMT_FROM_HH(tbl,hhp) ((void*)(((char*)(hhp)) - ((tbl)->hho)))
经常通过这个求出用户自定义结构体的首地址。因为要满足对用户自定义的类型的兼容性,所以采用了void * 类型。
UT_hash_handle结构体中的prev和next都是void * 类型的,也是这个原因。
根据上面的情况,不难猜出:
HASH_ITER 是通过遍历app链表实现的
HASH_FIND 是通过遍历 bucket链表实现的
链接:
uthash的User Guide:
http://troydhanson.github.io/uthash/userguide.html
uthash的下载:
https://github.com/troydhanson/uthash/blob/master/src/uthash.h