我将使用五个并行哈希表,对应于人们可能搜索的五个可能的前缀。每个哈希表槽将包含零个或多个引用,每个引用包含该特定键值对的前缀长度、该键前缀的哈希以及指向实际键和数据结构的指针。
对于删除,实际的键和数据结构将包含所有五个前缀长度和相应的哈希,加上键和值的字符数据。
例如:
#define PARTS 5
struct hashitem {
size_t hash[PARTS];
size_t hlen[PARTS];
char *data;
char key[];
};
struct hashref {
size_t hash;
size_t hlen;
struct hashitem *item;
};
struct hashrefs {
size_t size;
size_t used;
struct hashref ref[];
};
struct hashtable {
size_t size[PARTS];
struct hashrefs **slot[PARTS];
};
在struct hashitem 中,如果key 是k1k2k3k4k5,那么hlen[0]=2、hash[0]=hash("k1")、hlen[1]=4、hash[1]=hash("k1k2") 等等,直到hlen[4]=10、hash[4]=hash("k1k2k3k4k5")。
当插入一个新的键值对时,首先会找出前缀长度(hlen[])和它们对应的哈希值(hash[]),然后调用一个辅助函数
static int insert_pair(struct hashtable *ht,
const char *key,
const size_t hash[PARTS],
const size_t hlen[PARTS],
const char *data,
const size_t datalen)
{
struct hashitem *item;
size_t p, i;
/* Verify the key is not already in the
hash table. */
/* Allocate 'item', and copy 'key', 'data',
'hash', and 'hlen' to it. */
for (p = 0; p < PARTS; p++) {
i = hash[p] % ht->size[p];
if (!ht->entry[i]) {
/* Allocate a new hashrefs array,
with size=1 or greater, initialize
used=0 */
} else
if (ht->entry[i].used >= ht->entry[i].size) {
/* Reallocate ht->entry[i] with
size=used+1 or greater */
}
ht->entry[i].ref[ht->entry[i].used].hash = hash[p];
ht->entry[i].ref[ht->entry[i].used].hlen = plen[p];
ht->entry[i].ref[ht->entry[i].used].item = item;
ht->entry[i].used++;
}
return 0; /* Success, no errors */
}
前缀查找与使用全键查找哈希表相同:
int lookup_filter(struct hashtable *ht,
const size_t hash,
const size_t hashlen,
const size_t parts, /* 0 to PARTS-1 */
const char *key,
int (*func)(struct hashitem *, void *),
void *custom)
{
const struct hashrefs *refs = ht->entry[parts][hash % ht->size[parts]];
int retval = -1; /* None found */
size_t i;
if (!refs)
return retval;
for (i = 0; i < refs->used; i++)
if (refs->ref[i].hash == hash &&
refs->ref[i].hlen == hashlen &&
!strncmp(refs->ref[i].item->key, key, hashlen)) {
if (func) {
retval = func(refs->ref[i].item, custom);
if (retval)
return retval;
} else
retval = 0;
}
return retval;
}
注意使用的回调样式,以允许单个查找匹配所有前缀。假设唯一键的完整键匹配会稍微简单一些:
struct hashitem *lookup(struct hashtable *ht,
const size_t hash,
const size_t hashlen,
const char *key)
{
const struct hashrefs *refs = ht->entry[PARTS-1][hash % ht->size[PARTS-1]];
size_t i;
if (!refs)
return NULL;
for (i = 0; i < refs->used; i++)
if (refs->ref[i].hash == hash &&
refs->ref[i].hlen == hashlen &&
!strncmp(refs->ref[i].item->key, key, hashlen))
return refs->ref[i].item;
return NULL;
}
删除将使用查找,除了可以通过将匹配条目替换为同一引用数组中的最终项目来删除匹配项;或者如果该项是引用数组中唯一的一项,则完全释放整个数组。
之所以使用引用数组(每个哈希表条目有多个数据项)是可以接受的,是因为当前处理器以块的形式缓存数据(cacheline 是缓存的最小块)。因为每个哈希表槽包含一个或多个匹配,以及代码的完整哈希和哈希长度,所以需要进行逐字节比较以确定实际匹配的实际冲突非常罕见,即使是快速和 -简单的哈希函数。 (我希望每个匹配条目进行 1.05 到 1.10 次字符串比较,即使是像 DJB2 哈希这样简单的东西。)
换句话说,这种方法试图最小化为找到所需对而访问的缓存线的数量。
由于初始部分会有很多重复的哈希(相对较少的唯一前缀哈希)和哈希长度,因此将其哈希表缩小可能更有效。 (参考数组会更大。)因为哈希和哈希长度不会改变,所以可以随时调整任何哈希表的大小,而无需重新计算任何哈希。
请注意,因为除了 PARTS-1 之外的所有哈希表都用于扫描 sets 项,因此它们的引用数组可能会变得很长并不是一件坏事:这些数组将几乎完全包含一个正在查找的项目! (换句话说,如果一个引用数组长到 10,000 个条目,如果它用于查找所需的 9,750 个左右的条目,这不是问题。)
我个人确实也考虑过某种表格,例如每个关键部分都是表格中的一个附加级别。但是,查找具有给定前缀的条目集涉及表遍历和相当分散的内存访问。我相信,但尚未验证(通过比较两种方法的微基准测试),每个插槽具有潜在大参考数组的哈希表在运行时更有效。