纯 C 中有哪些支持 RapidHash 和动态扩容的高性能哈希表?
可以先比较 khash 和 uthash:两者都适用于 C,支持扩容,也能接入自定义哈希函数。建议先用 khash 建立性能基线;如果需要把现有结构体直接加入哈希表,并保持结构体地址稳定,可以重点考虑 uthash。
现有信息不足以判断哪一个“最快”。键长度、数据规模、查找命中率、增删比例和内存分配方式都会影响结果,需要在实际负载下比较。
两个候选实现有什么区别?
| 实现 | 自定义哈希方式 | 扩容与存储特点 | 适合优先评估的需求 |
|---|---|---|---|
| khash | 通过 KHASH_INIT 指定哈希函数和相等判断函数 | 开放寻址,插入时按容量条件扩容,也提供 kh_resize | 希望使用数组存储键和值,评估吞吐与内存占用 |
| uthash | 定义 HASH_FUNCTION | 通过桶链组织元素,按需自动增加桶数 | 希望索引现有结构体,并保持结构体地址稳定 |
这些功能可以在 khash 源码和 uthash 使用指南中确认。功能符合要求,并不能证明哪个实现更快。
如何给 khash 接入 RapidHash?
以非空、以 \0 结尾的字符串键和 int 值为例,准备好 khash.h 和 rapidhash.h,再使用自定义函数实例化哈希表。
示例使用 rapidhash(const void *key, size_t len) 接口,返回值为 64 位哈希。接入其他版本前,应核对实际头文件中的声明:RapidHash 接口源码。
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "rapidhash.h"
#include "khash.h"
static khint_t hash_string(const char *key)
{
uint64_t h = rapidhash(key, strlen(key));
return (khint_t)(h ^ (h >> 32));
}
static int equal_string(const char *a, const char *b)
{
return strcmp(a, b) == 0;
}
KHASH_INIT(rapid_map, const char *, int, 1,
hash_string, equal_string)
int main(void)
{
khash_t(rapid_map) *map = kh_init(rapid_map);
if (map == NULL)
return EXIT_FAILURE;
int status;
khiter_t it = kh_put(rapid_map, map, "alpha", &status);
if (status < 0) {
kh_destroy(rapid_map, map);
return EXIT_FAILURE;
}
kh_value(map, it) = 42;
it = kh_get(rapid_map, map, "alpha");
if (it != kh_end(map))
printf("%d\n", kh_value(map, it));
kh_destroy(rapid_map, map);
return EXIT_SUCCESS;
}
示例显式将 64 位结果折叠到 khash 使用的 khint_t,让位宽转换清楚可见,但不能保证这种处理方式的性能最优。哈希值相同仍需比较键,因此不能省略 equal_string。
接入时需要注意什么?
- 示例保存字符串指针,不复制字符串。字符串字面量在这里有效;换成动态字符串后,需要自行管理内存,键留在表中期间也不能修改其内容。
- 判定为相等的键必须产生相同哈希。若使用带 seed 的接口,同一张表在存续期间应保持 seed 一致。
strlen不适用于含有内嵌\0的二进制数据。此时应把指针和长度一起作为键,并按长度和字节内容比较。- khash 扩容可能移动内部数组;修改表后,不应继续使用此前取得的槽位索引或内部元素指针。上面的示例在插入后重新查找。
若改用 uthash,应在包含 uthash.h 前定义 HASH_FUNCTION,并在所有操作同一张表的编译单元中使用一致配置。它的桶扩容不会搬动应用提供的结构体,但结构体及键的内存仍由应用负责管理。uthash 使用指南
如何确定实际负载下哪个更快?
先让两个实现使用相同的键集合、RapidHash 配置和编译优化选项,再分别测量:
- 构建:从空表插入全部数据,计入内存分配和扩容成本。
- 查找:分开测量命中与未命中,并使用实际键长度分布。
- 混合操作:按业务中的比例执行插入、删除和查找。
- 资源开销:记录内存占用;如果关心延迟,再记录扩容时的耗时尖峰。
已知最终规模时,可以额外比较预分配容量后的表现,但要把它与从空表增长的结果分开记录。测试中还要实际使用查找结果,避免编译器消除无效操作。
RapidHash 的单独计算速度不能代表整张哈希表的速度。缺少上述测量结果时,这两个实现都值得试用,但尚不能认定其中任何一个最快。对 GLib 安全性的疑虑也需要具体说明是并发访问、内存管理还是其他问题,不能据此直接断言其实现不安全。
备注:内容仅供参考。
