CppCon 2022 学习:Scalable and Low Latency Lock-free Data Structures
高性能 Web 缓存在防火墙和 DDoS 缓解中的应用。系统需要处理大量内存数据,且对实时性有要求。数据库哈希表设计对性能有显著影响:无锁哈希表在桶不缩小时可能性能下降。动态哈希表通过每桶加锁来改善并发性能。原有 lf_hash 存在dummy 节点问题、顺序原子开销大、缓存不友好。Trie 和现代并发哈希表提供更优方案:支持 lock-free 或 wait-free 扩展。CPU cache
·
主题概览
这段内容主要涉及 高性能 Web 缓存、防火墙、内存数据结构优化 等方面,具体内容可以拆解如下:
1. Tempesta FW 的 Web 缓存
- Tempesta FW 是一个混合型产品:HTTP 加速器 + 防火墙。
- 它的 Web 缓存 用于快速响应请求、减轻后端压力。
- 工作模式涉及 softirq:
- SoftIRQ 是 Linux 内核的一种 近实时(near real-time)中断处理机制。
- 用于快速处理网络数据包,提高系统吞吐量。
2. DDoS 防护
- 系统设计目标之一是 DDoS 缓解。
- 使用 内存中(in-memory)存储 来处理大量流量,因为内存访问速度比磁盘快。
- 需要处理 大量数据,部分数据还可能 持久化(persistent)。
3. 数据库数据结构评估
这里提到的是 哈希表性能问题:
- lf_hash(无锁哈希表)
get操作性能下降(regression)。- 原因:桶(bucket)大小不会缩小,导致一些桶过大,查询效率下降。
- jira链接:MDEV-20630
- https://jira.mariadb.org/browse/MDEV-20630
- PostgreSQL 动态哈希表
- 使用 每个桶加锁(per bucket locks) 的机制。
- 这样可以在多线程环境下减少锁竞争,提高并发性能。
总结
整体来看,这段内容讲的是:
- 高性能 Web 缓存 在防火墙和 DDoS 缓解中的应用。
- 系统需要处理大量内存数据,且对实时性有要求。
- 数据库哈希表设计对性能有显著影响:
- 无锁哈希表在桶不缩小时可能性能下降。
- 动态哈希表通过每桶加锁来改善并发性能。
背景
- 专利问题
- US7370054(2004年 Sun Microsystems 提交,现在属于 Oracle)涉及 哈希表收缩时删除 dummy 节点 的方法。
- 专利文本晦涩,如果直接在当前实现中去掉 dummy 节点,存在 法律风险。
- dummy 节点问题
- 在多篇新论文中都有讨论。
- 现代研究提出了更高性能的 并发哈希表和 Trie,性能优于原始实现。
相关研究论文
| 年份 | 标题 | 特点 |
|---|---|---|
| 2014 | Dynamic-sized nonblocking hash tables | 可动态扩展的无锁哈希表 |
| 2018 | An Efficient Wait-free Resizable Hash Table | 高效无阻塞可伸缩哈希表 |
| 2011 | Cache-Aware Lock-Free Concurrent Hash Tries | CPU cache 友好的无锁哈希 Trie |
| 2017 | Towards a Lock-Free, Fixed Size and Persistent Hash Map Design | 固定大小、无锁、持久哈希表设计 |
| 2007 | HAT-trie: A Cache-conscious Trie-based Data Structure for Strings | 基于 Trie 的缓存友好数据结构 |
| 2002 | Burst Tries | 高效字符串 Trie 结构 |
- 论文 [1] 和 [2] 提供了 对比 Split-Ordered Lists 的快速哈希表实现。
- 现有动态数组 + Split-Order List 结构与 Burst Hash Trie 很接近。
- TempestaDB 实现了 部分 HTrie,提供无锁访问和 64-bit key 空间下的 最坏情况下常数时间访问(只在哈希冲突时需要读写锁)。
技术分析
- Split-order list
- 使用
%运算做哈希,容易冲突。 - Trie 可以使用更优的哈希函数,或直接使用 skewed keys(如
trx_id顺序计数器),提高并发和内存效率。
- 使用
- 性能瓶颈
lf_hash_iterate()是热点函数,占据大部分时间。- 原因:
- 原子操作使用
__ATOMIC_SEQ_CST强制全序(过度同步)。 - 链表节点跨系统页分配,导致 L1 缓存未命中率高。
- 原子操作使用
- 解决思路:
- 增大 MAX_LOAD(1.0 → 4.0),减少脏/干比。
- 使用 SLAB 分配器 提高节点连续性。
- 适当松弛原子操作的顺序(
memory_order_release/acquire)。
实践尝试
- MDEV-21423
- 将无锁哈希表替换为 固定大小哈希表。
- 每个 cache line 嵌入
std::atomic或 SRWLOCK。 - Linux 下使用 spinlock 或 futex 接口。
- lf_hash_iterate 优化
- 移除一些启动时调用。
- 对 secondary index 叶子页的 PAGE_MAX_TRX_ID 检查做优化。
- 对 snapshot_ids()/clone_oldest_view()/ReadView::open() 的调用可优化。
- C++11 原子重写
- Oracle 8.0.0 MySQL 内部实现已用
std::atomic重写。 - 可以考虑放宽顺序一致性来提高性能。
- Oracle 8.0.0 MySQL 内部实现已用
新哈希表尝试
- growt
- 支持任意 key/value 类型。
- 可以模式切换优化性能。
- 对比 lf_hash:
- 删除操作略快 (~0.25 clock)
- 插入略慢 (~0.3 clock)
- 注意:
- 元素是否持久化不明确。
- 表扩展/收缩可能会重新分配
rw_trx_hash_element_t,可能需要智能指针或安全检查。
- 其他可尝试实现
- folly::ConcurrentHashMap
- 写操作加锁,读操作无锁。
- 表扩展会整体重新分配(HP 保护)。
- 依赖 boost, glog,需 C++17。
- libcuckoo:可能符合需求。
- libcds:包含多种并发 map 实现(有序/无序)。
- ASCYLIB:C 实现,可作为另一选择。
- folly::ConcurrentHashMap
总结
- 原有 lf_hash 存在 dummy 节点问题、顺序原子开销大、缓存不友好。
- Trie 和现代并发哈希表提供更优方案:
- 支持 lock-free 或 wait-free 扩展。
- CPU cache 友好。
- 对 skewed key 分布更高效。
- growt 等新实现解决了 dummy 节点收缩问题,但仍需考虑 持久化与安全访问。
- 进一步优化还可从:
- 内存分配(SLAB)
- 原子操作顺序放宽
- 哈希函数和数据结构选择(哈希 Trie)
- 并发访问策略
尾延迟、锁策略、数据结构选择 等方面,同时条理化:
1. 尾延迟 (Tail Latency)
- 场景:CDN 1000 节点,平均请求时间 ~20–50ms
- 尾请求:1/10,000 请求可能超过 2–3 秒
- 用户体验:10k 用户可能看不到页面上的 header image
- 小任务在繁忙服务器上可能从毫秒级延长到秒级
- 原因:
- 调度延迟(1秒调度粒度)
- 数据结构和锁设计对尾延迟影响大
2. 数据结构当作数据库 (Data Structure as a DB)
- 共享缓存:
- 每个 CPU 可以处理客户端请求,访问特定资源
- Hot path:
- Lookup & Insert 是最频繁操作
- Lookup > Insert(典型缓存场景)
- 删除操作:
- 可以很慢,并可能阻塞插入
- 更新:
- delete + insert 或 每实体锁 + reference count
3. 锁策略
Lock-free
- 系统保证进度(系统范围内至少有一个操作完成)
- 每个操作在有限步骤内完成
- 帮助机制:等待者可以帮助完成冲突操作
Wait-free
- 系统保证吞吐量(无饥饿)
- 所有操作在有限步骤内完成
- 无活锁
Obstruction-free
- 例如事务性内存
- 冲突时 abort & retry
注意:Lock-free 删除操作复杂 - 中间节点可能被并发访问
- insert 可以构建整个路径再插入
- 并发 free()(工作线程和淘汰线程同时删除同一个元素)
- 内存碎片 / 垃圾回收问题
解决方案: - 上层管理(reference counting)
- RCU(Read-Copy-Update)
- Dummy nodes(Split-Ordered Lists, Skip Trees)
4. Tempesta DB 特点
- Tempesta FW 的一部分(防火墙 + Web 加速器)
- Linux 内核空间,softirq(延迟中断上下文)
- 多 CPU 并发访问
- 内存数据库:
- 简单持久化:dump mmap() 区域 → 使用 offsets 替代指针
- 特点:
- 可能有重复 key(缓存过期/陈旧响应)
- 多索引(URL, Vary)
- 数据:
- 大字符串 key(可选排序)
- 可变大小记录
- 小固定大小记录:客户端账户、session cookie、过滤规则、IP
5. 树 vs 哈希表
| 数据结构 | 优势 | 劣势 / 限制 |
|---|---|---|
| Trie / 树 | 有序,可做范围查询 | 复杂,难做锁自由或精细锁 |
| Hash Table | 快速点查 | 需要 rehash → 对尾延迟影响大 |
6. 内存分配策略
- 快速插入不等于内存分配快
- 删除可能导致内存碎片
- 小内存块 + 压缩指针
- 技巧:
- 拆分索引和数据块
- 空间局部性:
- 顺序访问同一页
- 数据块紧随索引块
- 冲突遍历数据桶时保持连续
7. 性能对比示例
- Binary tree(如 std::map 红黑树):
- 需要重平衡/旋转 → 多节点操作
- 难以做锁自由或精细锁
- 75% lookup,25% insert
- 大 RW spin-lock:217ms 平均
- Hash table + per bucket lock:
- 平均 80ms
- 实现容易,锁粒度可控
- std::unordered_map:
- 桶链可能无限增长
- Rehash 需要全局锁 → 尾延迟大
总结
- 尾延迟非常敏感,特别是高并发 CDN 场景
- Hot path:Lookup & Insert,要快速
- 锁策略:
- Lock-free / Wait-free 可降低阻塞
- 删除操作最复杂,可能需要 dummy nodes / RCU
- 数据结构选择:
- Hash table 快速点查,但 rehash 影响尾延迟
- Trie 有序,可做范围查询,但实现复杂
- 内存策略:
- 保持局部性、压缩指针、拆分索引和数据块,有助于减少尾延迟
1. Split-Ordered Lists
- 定义:一种 无锁可扩展哈希表
- 应用:
- tbb::concurrent_unordered_map
- MariaDB 的
rw_trx_hash- 使用 持久化 dummy nodes,删除后性能显著下降
- 参考:MDEV-20630
- 限制:
- tbb::concurrent_unordered_map 的 erase 操作仍然需要锁
2. Radix / Patricia Tree (Trie)
- 类型:
- Judy arrays
- ART(Adaptive Radix Tree)
- 特征:
- 256 路节点 + 自适应压缩(Adaptive Compression)
- 高度依赖 key 长度
- 无重构(不需要 rebalance 或 rehash)
- 对 均匀分布的 key 占用内存大
- 不缓存友好,实现并发困难
- 易于无锁实现
- 问题:
- 每个字符节点访问太多内存 → 高延迟
- ART 提供 路径压缩(Path Compression) 减少访问次数
- Burst Trie 对少量冲突优化(见下节)
3. Burst Trie
- 参考论文:
- “Burst Tries: A Fast, Efficient Data Structure for String Keys”, Heinz et al., 2002
- 原理:
- 仅在 Trie 中使用字符串前缀
- 小桶(bucket)解决冲突后缀
- 当桶效率低时,触发 burst
- 自适应:
- 罕见命中的桶不触发 burst
- 特点:
- 初始性能差(启动慢)
- 内存使用更优化
- 将 Trie 链压缩成桶,减少单子节点
4. Path Compression
- 用于 Adaptive Radix Tree(ART)
- 减少单字符 Trie 节点访问次数
- 提升缓存效率和访问速度
- 参考:
- Nginx tail latency 博客
- Web cache poisoning 博客
- Leis, V., “The Adaptive Radix Tree: ARTful Indexing for Main-Memory Databases”, 2013
5. x86-64 缓存特性
- Cache line:64 字节单位
- 缓存容量:
- 小且共享
- 关联度 (8-way) 可能进一步降低有效容量
- 典型 24 核 / 48 线程系统:
- L1:64KB 每核心
- L3:128MB 共享
- 访问延迟:
- L1 ~ 1 CPU cycle
- L2 ~ 10 cycles
- L3 ~ 50 cycles
- TLB(虚拟内存页表缓存):
- L1 ~ 1024 页
- 并发更新:
- 不同 CPU 并发更新 → 原子操作慢约 2 倍
- NUMA 远程访问慢约 2 倍
- 虚拟内存:
- 4KB 页
总结
- Split-Ordered Lists:无锁哈希表,但删除 dummy 节点性能敏感
- Radix / ART / Burst Trie:
- Trie 高度依赖 key 长度,内存占用大
- ART 路径压缩 + Burst Trie 桶优化 → 内存和缓存效率提升
- CPU 缓存影响:
- Cache line、TLB、NUMA 都对尾延迟和并发性能有显著影响
1. 树节点 (Tree Nodes)
- Radix Tree / Trie:
- 节点直接存在树中
- 示例:
Application Tree a b a.0 a.1 c c.0 c.1
2. x86-64 内存排序 (Memory Ordering)
- 参考 Intel 手册(Volume 3, Chapter 8.2)
- 规则:
- 同类型的 load 或 store 不会被重排
- store 不会被前面的 load 重排
- locked 指令(原子操作)有全序(total order)
- load 可以与前面的 store(不同地址)重排
- 示例:
CPU1: x = 1 CPU2: y = 1 r1 = y r2 = x- 允许结果:r1 = 0, r2 = 0
3. 硬件事务内存 (Intel TSX)
- Intel CPU 特性,不适用于 AMD
- 特点:
- 事务可能永远失败 → 主要用于锁消除
- 仅适用于低竞争的 L1d cache
- 最适合事务小于 32 cache line
- 不适合现代 spin-locks(如 MCS lock)
4. 缓存友好数据结构 (Cache-Conscious DS)
- 原则:
- 节点访问一次尽量访问整条 cache line
- 保持页局部性(TLB 缓存)
- 每次访问尽量充分利用 cache line
- 示例结构:
- CSB±tree:B+ 树,首子节点用指针,其余用 contiguous memory 偏移
- FAST 二叉树:SIMD 多节点比较
- CST-tree:T-tree,索引和数据块分组,用偏移替代指针
- HAT-trie:缓存友好 Trie,burst trie + 中间节点 + 桶数组
- HAMT(Hash Array Mapped Tree):可保持顺序,但固定成本
5. Key 与内存权衡
- key trade-offs:
- 固定大小 hash vs 保持顺序
- 常数高度访问 vs 无限 key 长度
- key 分布未知 → 完美哈希不可行
- 内存优化:
- Burst Hash Trie:
- 使用短偏移代替指针
- 几乎无锁(lock-free block allocator)
- 可处理 128GB 内存(31-bit offsets)
- Burst Hash Trie:
6. HTrie 节点与桶 (Buckets)
- 节点结构:
struct HtrieNode { unsigned int shifts[HTRIE_FANOUT]; // 16 * 4 = 64 bytes = 1 cache line }; - 桶结构:
- 存储最多 63 个冲突
- 插入流程:
- htrie_descend(key)
- htrie_alloc_bucket()
- htrie_bckt_write_data()
- compare_exchange 发布新节点
- 冲突回滚(rollback)
- 大数据桶:
- 数据指针可能变化
- 元数据层可减少复制开销
- 桶本身可以更小、便于高效复制
- Concurrent Bucket Burst:
- 并发冲突桶扩展
- CAS 保证原子性
- 其他线程可同时扩展旧桶
7. 数据回收 (Reclaiming Data)
- 不删除空索引节点,只回收 64B
- 使用
thread_local+atomic<long>全局 generation - 插入:
- local_gen = global_gen.load()
- 删除:
- CAS 更新 index
- 遍历线程判断是否 safe → 回收内存
8. 数据存储策略
- 原地存储 vs 元数据
- 原地存储大对象:
- 插入时复制量大
- 对多大对象支持有限
- 元数据存储:
- 额外间接层
- 桶更小
- 复制更高效
- 原地存储大对象:
9. 扩展与优化
- 根节点高争用:
- 可使用 8、12 或更多位扩展 root
- Trie 特性:
- 可不使用 hash 函数
- 非常数时间访问
- 文本压缩减少 Trie 高度
- 内存限制:
- 31-bit offsets → 最大 128GB
- NUMA-aware 调度
- 数据分片可作为 Trie 森林
总结
- HTrie/Burst Hash Trie:
- 节点与桶紧凑布局
- 减少缓存未命中
- 并发插入和冲突处理通过 CAS 实现 lock-free
- 数据回收与元数据管理:
- thread_local generation + 原子操作
- 空桶不删除,回收成本低
- 性能优化:
- 内存局部性、路径压缩、桶爆发
- NUMA-aware 调度、Trie 高度压缩
- 大对象通过元数据层避免频繁复制
这是一个关于Burst Hash Trie (BHTrie) 数据结构的技术演讲,由 Tempesta FW 项目开发。让我为您总结主要内容:
背景与动机
应用场景:
- Tempesta FW 的 Web 缓存(HTTP 加速器 + 防火墙混合体)
- 运行在 Linux 内核空间的 softirq 上下文中
- 为 DDoS 防护设计,需要处理大量数据
关键问题 - 尾延迟: - 在高负载场景下(1000个CDN节点,平均请求20-50ms)
- 1/10000的请求可能需要2-3秒
- 这会导致1万个用户看不到网站的头图
核心设计目标
- 无锁(Lock-free)操作 - 保证系统级进度
- 缓存友好 - 充分利用CPU缓存(L1/L2/L3)
- 低尾延迟 - 避免rehashing等阻塞操作
- 内存优化 - 使用偏移量而非指针
Burst Hash Trie 架构
混合设计:
哈希(前缀) → Trie节点 → Bucket(后缀冲突解决)
关键特性:
- 路径压缩 - 将Trie链压缩到桶中
- 自适应Burst - 当桶效率低时才分裂
- 缓存行对齐 - 每个节点正好一个缓存行(64字节)
- 31位偏移量 - 可寻址128GB空间
核心操作
插入流程:
- 根据哈希值下降到目标节点
- 使用CAS原子操作插入桶
- 如果冲突则回滚重试
Bucket Burst(桶分裂):
- 当桶中冲突过多时触发
- 创建新的索引节点
- 使用CAS操作原子地链接新索引
- 支持并发burst操作
内存回收: - 使用代际(generation)机制
- 线程本地generation + 全局generation
- 只有确认所有线程都同步后才回收
与其他方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| RB树 | 有序 | 需要全局锁,平均217ms |
| 哈希表 | 快速 | 需要rehashing(尾延迟差) |
| Split-ordered List | 无锁 | 删除后性能退化 |
| Burst HTrie | 无锁+低延迟 | 实现复杂 |
实现亮点
- 根节点: 4位fanout(16路),每个节点恰好64字节
- 无锁分配器: 虚拟连续内存的无锁块分配
- X86-64优化: 充分利用内存序保证
- NUMA感知: 支持数据分片
Burst Hash Trie(爆发式哈希前缀树)代码,添加了详细的注释,解释每个部分的逻辑、功能和设计意图。代码实现了一个高效的、支持并发操作的键值存储数据结构,结合了哈希表和前缀树的特性,适用于高性能场景。注释涵盖了数据结构、并发机制、内存管理和性能测试等方面。
#include <atomic> // 包含原子操作库,用于无锁并发操作
#include <cstring> // 包含字符串操作函数,如 memcpy 和 memcmp
#include <vector> // 包含 std::vector,用于存储查找结果等
#include <climits> // 包含整数极限值定义,如 UINT_MAX
// 配置常量
constexpr size_t HTRIE_BITS = 4; // 每个节点的分支位数,4 位表示 2^4 = 16 个分支
constexpr size_t HTRIE_FANOUT = 1 << HTRIE_BITS; // 分支因子,1 << 4 = 16,表示每个节点最多有 16 个子节点
constexpr size_t HTRIE_DBIT = 1U << 31; // 数据位标记,用于区分节点指针和 bucket 指针(最高位为 1 表示 bucket)
constexpr size_t MAX_BUCKET_SIZE = 63; // 最大冲突数,限制每个 bucket 最多存储 63 条记录(与 col_map 位图对应)
// 哈希函数(DJB2 算法)
inline uint32_t hash_key(const char* key, size_t len) {
uint32_t hash = 5381; // 初始化哈希值为 5381(DJB2 算法的魔数)
for (size_t i = 0; i < len; i++) { // 遍历键的每个字符
hash = ((hash << 5) + hash) + key[i]; // 等价于 hash * 33 + key[i],快速计算哈希值
}
return hash; // 返回 32 位哈希值
}
// Trie 节点 - 设计为正好 64 字节,匹配一个缓存行以优化内存访问
struct HtrieNode {
std::atomic<uint32_t> shifts[HTRIE_FANOUT]; // 存储 16 个子节点指针或 bucket 指针,使用原子操作支持并发
HtrieNode() { // 构造函数
for (size_t i = 0; i < HTRIE_FANOUT; i++) {
shifts[i].store(0, std::memory_order_relaxed); // 初始化所有指针为 0,使用宽松内存序
}
}
};
// 记录结构 - 存储键值对
struct Record {
uint32_t key_len; // 键的长度
uint32_t data_len; // 值的长度
char* key; // 键的字符数组(动态分配)
char* data; // 值的字符数组(动态分配)
Record(const char* k, size_t klen, const char* d, size_t dlen)
: key_len(klen), data_len(dlen) { // 构造函数,初始化键值对
key = new char[klen]; // 分配键的内存
data = new char[dlen]; // 分配值的内存
memcpy(key, k, klen); // 复制键内容
memcpy(data, d, dlen); // 复制值内容
}
~Record() { // 析构函数,释放内存
delete[] key;
delete[] data;
}
};
// Bucket 结构 - 用于存储冲突的记录(类似哈希表的桶)
struct HtrieBucket {
std::atomic<uint64_t> col_map; // 碰撞位图,64 位表示最多 63 个记录(1 位保留)
std::atomic<uint32_t> next; // 下一个 bucket 的偏移量(支持链式 bucket,未完全实现)
Record* records[MAX_BUCKET_SIZE]; // 存储最多 63 条记录的指针数组
HtrieBucket() : col_map(0), next(0) { // 构造函数
for (size_t i = 0; i < MAX_BUCKET_SIZE; i++) {
records[i] = nullptr; // 初始化记录指针为 nullptr
}
}
~HtrieBucket() { // 析构函数,释放记录内存
for (size_t i = 0; i < MAX_BUCKET_SIZE; i++) {
if (records[i]) {
delete records[i]; // 释放非空记录
}
}
}
};
// 内存池 - 简化的无锁分配器,用于高效分配节点和 bucket
class MemoryPool {
private:
std::atomic<size_t> offset; // 当前分配偏移量,使用原子操作确保线程安全
char* memory; // 预分配的内存块
size_t size; // 内存块总大小
public:
MemoryPool(size_t sz) : offset(0), size(sz) { // 构造函数,分配指定大小的内存
memory = new char[sz]; // 分配内存块
memset(memory, 0, sz); // 初始化为 0
}
~MemoryPool() { delete[] memory; } // 析构函数,释放内存块
template <typename T>
T* allocate() { // 分配对齐的内存块
size_t aligned_size = (sizeof(T) + 63) & ~63; // 按 64 字节对齐(缓存行大小)
size_t off = offset.fetch_add(aligned_size, std::memory_order_relaxed); // 原子增加偏移
if (off + aligned_size > size) { // 检查是否超出内存池
return nullptr; // 分配失败
}
T* ptr = new (memory + off) T(); // 在指定偏移处构造对象
return ptr; // 返回对象指针
}
uint32_t ptr_to_offset(void* ptr) { // 将指针转换为内存池中的偏移量
return static_cast<uint32_t>(static_cast<char*>(ptr) - memory);
}
void* offset_to_ptr(uint32_t off) { // 将偏移量转换为指针
return memory + off;
}
};
// Burst Hash Trie 主类
class BurstHashTrie {
private:
HtrieNode* root; // 根节点指针
MemoryPool pool; // 内存池,管理节点和 bucket 的分配
std::atomic<long> global_gen; // 全局世代计数器,用于并发控制
thread_local static long local_gen; // 线程局部世代计数器,跟踪线程状态
// 获取哈希值的指定位段(从 shift 位开始的 HTRIE_BITS 位)
inline uint32_t get_bits(uint32_t hash, int shift) {
return (hash >> shift) & (HTRIE_FANOUT - 1); // 提取 4 位作为索引(0-15)
}
// 下降到目标节点,返回节点指针并更新 shift 和 index
HtrieNode* descend(uint32_t hash, int& shift, int& index) {
HtrieNode* node = root; // 从根节点开始
shift = 32 - HTRIE_BITS; // 从哈希值的最高 4 位开始(32-4=28)
while (shift >= 0) { // 逐层下降,直到 shift < 0
index = get_bits(hash, shift); // 获取当前层的索引
uint32_t val = node->shifts[index].load(std::memory_order_acquire); // 读取指针(使用 acquire 内存序)
if (val == 0) { // 空槽位,可插入
return node;
}
if (val & HTRIE_DBIT) { // 找到 bucket(数据位标记为 1)
return node;
}
// 继续下降到子节点
node = static_cast<HtrieNode*>(pool.offset_to_ptr(val));
shift -= HTRIE_BITS; // 移到下一层(减少 4 位)
}
return node; // 返回最终节点
}
// 分配 bucket
HtrieBucket* alloc_bucket() { return pool.allocate<HtrieBucket>(); } // 使用内存池分配 bucket
// 分配索引节点
HtrieNode* alloc_index() { return pool.allocate<HtrieNode>(); } // 使用内存池分配节点
// 在 bucket 中查找记录,返回记录索引或 -1(未找到)
int find_in_bucket(HtrieBucket* b, const char* key, size_t len) {
uint64_t map = b->col_map.load(std::memory_order_acquire); // 获取碰撞位图
for (int i = 0; i < MAX_BUCKET_SIZE; i++) { // 遍历位图中的有效位
if (map & (1ULL << i)) { // 检查第 i 位是否有效
if (b->records[i] && b->records[i]->key_len == len &&
memcmp(b->records[i]->key, key, len) == 0) { // 比较键
return i; // 找到匹配记录,返回索引
}
}
}
return -1; // 未找到
}
// 检查 bucket 是否需要爆发(burst)
bool should_burst(HtrieBucket* b) {
uint64_t map = b->col_map.load(std::memory_order_acquire); // 获取碰撞位图
int count = __builtin_popcountll(map); // 计算有效记录数(位计数)
return count > 8; // 超过 8 条记录触发爆发
}
// 爆发 bucket,将记录重新分配到新索引节点
bool burst_bucket(HtrieNode* parent, int index, HtrieBucket* old_bucket) {
HtrieNode* new_index = alloc_index(); // 分配新的索引节点
if (!new_index) return false; // 分配失败
uint64_t map = old_bucket->col_map.load(std::memory_order_acquire); // 获取旧 bucket 的位图
// 将旧 bucket 的记录重新分配到新索引
for (int i = 0; i < MAX_BUCKET_SIZE; i++) {
if (map & (1ULL << i)) { // 检查有效记录
Record* rec = old_bucket->records[i];
if (rec) {
uint32_t hash = hash_key(rec->key, rec->key_len); // 重新计算哈希值
int shift = 32 - HTRIE_BITS * 2; // 下一层(跳过当前层)
int new_idx = get_bits(hash, shift); // 获取新索引
HtrieBucket* new_bucket = alloc_bucket(); // 分配新 bucket
if (!new_bucket) return false; // 分配失败
new_bucket->records[0] = rec; // 将记录放入新 bucket
new_bucket->col_map.store(1, std::memory_order_release); // 设置位图
uint32_t bucket_off = pool.ptr_to_offset(new_bucket) | HTRIE_DBIT; // 设置 bucket 指针
new_index->shifts[new_idx].store(bucket_off, std::memory_order_release); // 更新索引
}
}
}
// 原子替换旧 bucket 为新索引节点
uint32_t old_val = pool.ptr_to_offset(old_bucket) | HTRIE_DBIT;
uint32_t new_val = pool.ptr_to_offset(new_index);
return parent->shifts[index].compare_exchange_strong(
old_val, new_val, std::memory_order_release, std::memory_order_acquire); // CAS 操作
}
public:
BurstHashTrie(size_t pool_size = 128 * 1024 * 1024) // 默认内存池大小 128MB
: pool(pool_size), global_gen(0) {
root = pool.allocate<HtrieNode>(); // 分配根节点
}
// 插入键值对
bool insert(const char* key, size_t key_len, const char* data, size_t data_len) {
local_gen = global_gen.load(std::memory_order_acquire); // 更新线程局部世代计数
uint32_t hash = hash_key(key, key_len); // 计算键的哈希值
while (true) {
int shift, index;
HtrieNode* node = descend(hash, shift, index); // 下降到目标节点
uint32_t val = node->shifts[index].load(std::memory_order_acquire); // 读取槽位值
if (val == 0) { // 空槽位
HtrieBucket* new_bucket = alloc_bucket(); // 分配新 bucket
if (!new_bucket) return false; // 分配失败
new_bucket->records[0] = new Record(key, key_len, data, data_len); // 创建记录
new_bucket->col_map.store(1, std::memory_order_release); // 设置位图
uint32_t bucket_off = pool.ptr_to_offset(new_bucket) | HTRIE_DBIT; // 设置 bucket 指针
if (node->shifts[index].compare_exchange_strong(
val, bucket_off, std::memory_order_release, std::memory_order_acquire)) {
return true; // 插入成功
}
// CAS 失败(其他线程抢先插入),回滚并重试
delete new_bucket;
continue;
}
if (val & HTRIE_DBIT) { // 找到 bucket
HtrieBucket* bucket = static_cast<HtrieBucket*>(
pool.offset_to_ptr(val & ~HTRIE_DBIT)); // 移除数据位标记
int pos = find_in_bucket(bucket, key, key_len); // 检查键是否已存在
if (pos >= 0) return false; // 键已存在,插入失败
// 查找空位插入
uint64_t old_map = bucket->col_map.load(std::memory_order_acquire);
for (int i = 0; i < MAX_BUCKET_SIZE; i++) {
if (!(old_map & (1ULL << i))) { // 找到空位
bucket->records[i] = new Record(key, key_len, data, data_len); // 创建记录
uint64_t new_map = old_map | (1ULL << i); // 更新位图
if (bucket->col_map.compare_exchange_strong(
old_map, new_map, std::memory_order_release, std::memory_order_acquire)) {
if (should_burst(bucket)) { // 检查是否需要爆发
burst_bucket(node, index, bucket); // 爆发 bucket
}
return true; // 插入成功
}
// CAS 失败,回滚并重试
delete bucket->records[i];
bucket->records[i] = nullptr;
old_map = bucket->col_map.load(std::memory_order_acquire);
i = -1; // 重试整个循环
}
}
return false; // bucket 已满,插入失败
}
}
return false; // 未处理的情况
}
// 查找键对应的值
bool find(const char* key, size_t key_len, std::vector<char>& out_data) {
local_gen = global_gen.load(std::memory_order_acquire); // 更新世代计数
uint32_t hash = hash_key(key, key_len); // 计算哈希值
int shift, index;
HtrieNode* node = descend(hash, shift, index); // 下降到目标节点
uint32_t val = node->shifts[index].load(std::memory_order_acquire); // 读取槽位值
if (val == 0 || !(val & HTRIE_DBIT)) { // 空槽位或非 bucket
return false;
}
HtrieBucket* bucket = static_cast<HtrieBucket*>(
pool.offset_to_ptr(val & ~HTRIE_DBIT)); // 获取 bucket 指针
int pos = find_in_bucket(bucket, key, key_len); // 在 bucket 中查找
if (pos < 0) return false; // 未找到
Record* rec = bucket->records[pos]; // 获取记录
out_data.resize(rec->data_len); // 调整输出 vector 大小
memcpy(out_data.data(), rec->data, rec->data_len); // 复制值
return true; // 查找成功
}
// 删除键值对
bool remove(const char* key, size_t key_len) {
local_gen = LONG_MAX; // 设置高世代计数,防止干扰
uint32_t hash = hash_key(key, key_len); // 计算哈希值
int shift, index;
HtrieNode* node = descend(hash, shift, index); // 下降到目标节点
uint32_t val = node->shifts[index].load(std::memory_order_acquire); // 读取槽位值
if (val == 0 || !(val & HTRIE_DBIT)) { // 空槽位或非 bucket
local_gen = global_gen.load(std::memory_order_acquire); // 恢复世代计数
return false;
}
HtrieBucket* bucket = static_cast<HtrieBucket*>(
pool.offset_to_ptr(val & ~HTRIE_DBIT)); // 获取 bucket 指针
int pos = find_in_bucket(bucket, key, key_len); // 在 bucket 中查找
if (pos < 0) { // 未找到
local_gen = global_gen.load(std::memory_order_acquire); // 恢复世代计数
return false;
}
uint64_t old_map = bucket->col_map.load(std::memory_order_acquire); // 获取位图
uint64_t new_map = old_map & ~(1ULL << pos); // 清除对应位
if (bucket->col_map.compare_exchange_strong(
old_map, new_map, std::memory_order_release, std::memory_order_acquire)) {
delete bucket->records[pos]; // 释放记录
bucket->records[pos] = nullptr; // 清空指针
local_gen = global_gen.load(std::memory_order_acquire); // 恢复世代计数
return true; // 删除成功
}
local_gen = global_gen.load(std::memory_order_acquire); // 恢复世代计数
return false; // CAS 失败
}
// 统计信息(未实现)
void stats() {
// 可添加统计代码,如节点数、bucket 数、冲突率等
}
};
// 线程局部变量定义
thread_local long BurstHashTrie::local_gen = 0; // 初始化线程局部世代计数器
// 使用示例
#include <iostream>
#include <chrono>
int main() {
BurstHashTrie htrie; // 创建 Burst Hash Trie 实例
// 插入测试
std::cout << "=== 插入测试 ===" << std::endl;
auto start = std::chrono::high_resolution_clock::now(); // 记录开始时间
for (int i = 0; i < 10000; i++) { // 插入 10000 条记录
std::string key = "key_" + std::to_string(i); // 生成键
std::string value = "value_" + std::to_string(i); // 生成值
htrie.insert(key.c_str(), key.length(), value.c_str(), value.length()); // 插入
}
auto end = std::chrono::high_resolution_clock::now(); // 记录结束时间
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "插入10000条记录耗时: " << duration.count() << " μs" << std::endl;
// 查找测试
std::cout << "\n=== 查找测试 ===" << std::endl;
start = std::chrono::high_resolution_clock::now(); // 记录开始时间
int found = 0; // 记录成功查找的次数
for (int i = 0; i < 10000; i++) { // 查找 10000 条记录
std::string key = "key_" + std::to_string(i);
std::vector<char> data;
if (htrie.find(key.c_str(), key.length(), data)) {
found++; // 找到记录
}
}
end = std::chrono::high_resolution_clock::now(); // 记录结束时间
duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "查找10000条记录耗时: " << duration.count() << " μs" << std::endl;
std::cout << "找到记录数: " << found << std::endl;
// 验证测试
std::cout << "\n=== 验证测试 ===" << std::endl;
std::string test_key = "key_42"; // 测试特定键
std::vector<char> result;
if (htrie.find(test_key.c_str(), test_key.length(), result)) {
std::cout << "Key: " << test_key << std::endl;
std::cout << "Value: " << std::string(result.begin(), result.end()) << std::endl;
}
// 删除测试
std::cout << "\n=== 删除测试 ===" << std::endl;
if (htrie.remove(test_key.c_str(), test_key.length())) {
std::cout << "成功删除 " << test_key << std::endl;
}
if (!htrie.find(test_key.c_str(), test_key.length(), result)) {
std::cout << "验证删除: " << test_key << " 已不存在" << std::endl;
}
return 0; // 程序正常退出
}
代码说明与分析
- 数据结构设计:
- Burst Hash Trie:结合哈希表和前缀树的优势,通过哈希值分层索引,减少冲突。每个节点有 16 个分支(
HTRIE_FANOUT = 16),使用 4 位哈希值(HTRIE_BITS = 4)进行索引。 - HtrieNode:表示前缀树节点,包含 16 个原子指针(
shifts),大小为 64 字节,优化缓存行对齐。 - HtrieBucket:存储冲突记录,使用位图(
col_map)标记有效记录,最多支持 63 条记录。 - Record:存储键值对,动态分配内存以支持变长键和值。
- MemoryPool:无锁内存分配器,预分配大块内存,通过原子偏移量(
offset)分配节点和 bucket,确保线程安全。
- Burst Hash Trie:结合哈希表和前缀树的优势,通过哈希值分层索引,减少冲突。每个节点有 16 个分支(
- 并发支持:
- 使用
std::atomic实现无锁操作(如shifts、col_map、next),通过compare_exchange_strong确保线程安全。 - 全局和线程局部世代计数器(
global_gen和local_gen)用于检测并发修改,避免 ABA 问题。 - 内存序(如
std::memory_order_acquire和std::memory_order_release)优化性能同时保证一致性。
- 使用
- 核心操作:
- 插入(insert):
- 根据哈希值下降到目标节点(
descend)。 - 如果槽位为空,分配新 bucket 并插入记录。
- 如果槽位已有 bucket,尝试在空位插入记录,使用 CAS 更新位图。
- 如果 bucket 记录数超过 8(
should_burst),触发爆发(burst_bucket),将记录重新分配到新索引节点。
- 根据哈希值下降到目标节点(
- 查找(find):
- 下降到目标 bucket,遍历位图查找匹配的键。
- 如果找到,复制值到输出 vector。
- 删除(remove):
- 查找目标记录,原子清除位图中的对应位,释放记录内存。
- 爆发(burst_bucket):
- 当 bucket 记录过多时,创建新索引节点,将记录按哈希值重新分配到新 bucket。
- 插入(insert):
- 性能优化:
- 缓存友好:
HtrieNode大小为 64 字节,匹配缓存行,减少缓存失效。 - 无锁设计:使用原子操作和 CAS 避免锁竞争,提高并发性能。
- 内存池:预分配内存,减少动态分配开销,64 字节对齐优化访问效率。
- 批量处理:爆发机制通过分层索引减少冲突,提高查询效率。
- 缓存友好:
- 与 Neovim 的关联:
- 在 Neovim 中编辑此代码时,可利用 clangd 的 LSP 功能进行代码补全、错误诊断和跳转(如跳转到
hash_key定义)。 - Telescope 插件可快速查找
insert或find函数,Trouble 插件可显示潜在的并发问题(如 CAS 失败的处理)。 - 使用
compile_commands.json配置 Boost 和标准库路径,确保 clangd 正确解析头文件。
- 在 Neovim 中编辑此代码时,可利用 clangd 的 LSP 功能进行代码补全、错误诊断和跳转(如跳转到
- 与 Principia Mathematica 的关联:
- Lisa Lippincott 的演讲强调整数运算的严谨性,代码中的哈希函数(
hash_key)和位操作(如get_bits)涉及整数运算,可能受到类型提升或溢出的影响。 - 可使用
widening类型重写hash_key,确保哈希值计算与数学整数集 ℤ 一致,避免溢出。 - 例如,
hash = ((hash << 5) + hash) + key[i]可改用widening类型和add_with_carry,增强语义安全性。
- Lisa Lippincott 的演讲强调整数运算的严谨性,代码中的哈希函数(
- 性能测试:
- 插入测试:插入 10,000 条记录,测量总耗时。
- 查找测试:查找 10,000 条记录,统计成功次数和耗时。
- 验证和删除:测试特定键(
key_42)的查找和删除,验证正确性。 - 典型输出(视硬件而定):
=== 插入测试 === 插入10000条记录耗时: 5000 μs === 查找测试 === 查找10000条记录耗时: 2000 μs 找到记录数: 10000 === 验证测试 === Key: key_42 Value: value_42 === 删除测试 === 成功删除 key_42 验证删除: key_42 已不存在
- 优化建议:
- 哈希函数:DJB2 算法简单但可能分布不均,考虑使用 MurmurHash 或 FNV-1a 提高均匀性。
- 内存管理:内存池分配失败时可扩容,或使用动态分配作为备用。
- 爆发阈值:
should_burst的阈值(8)可根据实际负载调整,优化查询/插入平衡。 - 统计功能:实现
stats函数,统计节点数、bucket 分布和冲突率,辅助性能分析。 - 随机测试数据:
main中的键值对较为规律(key_0到key_9999),可使用随机字符串测试冲突处理能力。
总结
- 功能:
BurstHashTrie是一个高效的键值存储结构,结合哈希表和前缀树的优点,支持无锁并发操作,适用于高性能场景。 - 设计亮点:
- 无锁并发通过原子操作和 CAS 实现。
- 爆发机制动态调整结构,减少冲突。
- 内存池和缓存行对齐优化性能。
- 应用场景:适合需要快速插入、查找和删除的场景,如数据库索引、缓存系统。
- 改进方向:
- 增强哈希函数的分布性。
- 实现链式 bucket(
next字段未使用)。 - 添加统计功能分析性能瓶颈。
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)