主题概览

这段内容主要涉及 高性能 Web 缓存、防火墙、内存数据结构优化 等方面,具体内容可以拆解如下:

1. Tempesta FW 的 Web 缓存

  • Tempesta FW 是一个混合型产品:HTTP 加速器 + 防火墙
  • 它的 Web 缓存 用于快速响应请求、减轻后端压力。
  • 工作模式涉及 softirq
    • SoftIRQ 是 Linux 内核的一种 近实时(near real-time)中断处理机制
    • 用于快速处理网络数据包,提高系统吞吐量。

2. DDoS 防护

  • 系统设计目标之一是 DDoS 缓解
  • 使用 内存中(in-memory)存储 来处理大量流量,因为内存访问速度比磁盘快。
  • 需要处理 大量数据,部分数据还可能 持久化(persistent)

3. 数据库数据结构评估

这里提到的是 哈希表性能问题

  1. lf_hash(无锁哈希表)
  2. PostgreSQL 动态哈希表
    • 使用 每个桶加锁(per bucket locks) 的机制。
    • 这样可以在多线程环境下减少锁竞争,提高并发性能。

总结

整体来看,这段内容讲的是:

  • 高性能 Web 缓存 在防火墙和 DDoS 缓解中的应用。
  • 系统需要处理大量内存数据,且对实时性有要求。
  • 数据库哈希表设计对性能有显著影响:
    • 无锁哈希表在桶不缩小时可能性能下降。
    • 动态哈希表通过每桶加锁来改善并发性能。

背景

  1. 专利问题
    • US7370054(2004年 Sun Microsystems 提交,现在属于 Oracle)涉及 哈希表收缩时删除 dummy 节点 的方法。
    • 专利文本晦涩,如果直接在当前实现中去掉 dummy 节点,存在 法律风险
  2. 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 空间下的 最坏情况下常数时间访问(只在哈希冲突时需要读写锁)。

技术分析

  1. Split-order list
    • 使用 % 运算做哈希,容易冲突。
    • Trie 可以使用更优的哈希函数,或直接使用 skewed keys(如 trx_id 顺序计数器),提高并发和内存效率。
  2. 性能瓶颈
    • lf_hash_iterate() 是热点函数,占据大部分时间。
    • 原因:
      • 原子操作使用 __ATOMIC_SEQ_CST 强制全序(过度同步)。
      • 链表节点跨系统页分配,导致 L1 缓存未命中率高。
    • 解决思路:
      • 增大 MAX_LOAD(1.0 → 4.0),减少脏/干比。
      • 使用 SLAB 分配器 提高节点连续性。
      • 适当松弛原子操作的顺序(memory_order_release/acquire)。

实践尝试

  1. MDEV-21423
    • 将无锁哈希表替换为 固定大小哈希表
    • 每个 cache line 嵌入 std::atomic 或 SRWLOCK。
    • Linux 下使用 spinlock 或 futex 接口。
  2. lf_hash_iterate 优化
    • 移除一些启动时调用。
    • 对 secondary index 叶子页的 PAGE_MAX_TRX_ID 检查做优化。
    • 对 snapshot_ids()/clone_oldest_view()/ReadView::open() 的调用可优化。
  3. C++11 原子重写
    • Oracle 8.0.0 MySQL 内部实现已用 std::atomic 重写。
    • 可以考虑放宽顺序一致性来提高性能。

新哈希表尝试

  1. growt
    • 支持任意 key/value 类型。
    • 可以模式切换优化性能。
    • 对比 lf_hash:
      • 删除操作略快 (~0.25 clock)
      • 插入略慢 (~0.3 clock)
    • 注意:
      • 元素是否持久化不明确。
      • 表扩展/收缩可能会重新分配 rw_trx_hash_element_t,可能需要智能指针或安全检查。
  2. 其他可尝试实现
    • folly::ConcurrentHashMap
      • 写操作加锁,读操作无锁。
      • 表扩展会整体重新分配(HP 保护)。
      • 依赖 boost, glog,需 C++17。
    • libcuckoo:可能符合需求。
    • libcds:包含多种并发 map 实现(有序/无序)。
    • ASCYLIB:C 实现,可作为另一选择。

总结

  • 原有 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
  • 原理
    1. 仅在 Trie 中使用字符串前缀
    2. 小桶(bucket)解决冲突后缀
    3. 当桶效率低时,触发 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 页

总结

  1. Split-Ordered Lists:无锁哈希表,但删除 dummy 节点性能敏感
  2. Radix / ART / Burst Trie
    • Trie 高度依赖 key 长度,内存占用大
    • ART 路径压缩 + Burst Trie 桶优化 → 内存和缓存效率提升
  3. 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)

6. HTrie 节点与桶 (Buckets)

  • 节点结构
    struct HtrieNode {
        unsigned int shifts[HTRIE_FANOUT]; // 16 * 4 = 64 bytes = 1 cache line
    };
    
  • 桶结构
    • 存储最多 63 个冲突
    • 插入流程:
      1. htrie_descend(key)
      2. htrie_alloc_bucket()
      3. htrie_bckt_write_data()
      4. compare_exchange 发布新节点
      5. 冲突回滚(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 森林

总结

  1. HTrie/Burst Hash Trie
    • 节点与桶紧凑布局
    • 减少缓存未命中
    • 并发插入和冲突处理通过 CAS 实现 lock-free
  2. 数据回收与元数据管理
    • thread_local generation + 原子操作
    • 空桶不删除,回收成本低
  3. 性能优化
    • 内存局部性、路径压缩、桶爆发
    • 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万个用户看不到网站的头图

核心设计目标

  1. 无锁(Lock-free)操作 - 保证系统级进度
  2. 缓存友好 - 充分利用CPU缓存(L1/L2/L3)
  3. 低尾延迟 - 避免rehashing等阻塞操作
  4. 内存优化 - 使用偏移量而非指针

Burst Hash Trie 架构

混合设计:

哈希(前缀) → Trie节点 → Bucket(后缀冲突解决)

关键特性:

  1. 路径压缩 - 将Trie链压缩到桶中
  2. 自适应Burst - 当桶效率低时才分裂
  3. 缓存行对齐 - 每个节点正好一个缓存行(64字节)
  4. 31位偏移量 - 可寻址128GB空间

核心操作

插入流程:

  1. 根据哈希值下降到目标节点
  2. 使用CAS原子操作插入桶
  3. 如果冲突则回滚重试
    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;                                     // 程序正常退出
}

代码说明与分析

  1. 数据结构设计
    • Burst Hash Trie:结合哈希表和前缀树的优势,通过哈希值分层索引,减少冲突。每个节点有 16 个分支(HTRIE_FANOUT = 16),使用 4 位哈希值(HTRIE_BITS = 4)进行索引。
    • HtrieNode:表示前缀树节点,包含 16 个原子指针(shifts),大小为 64 字节,优化缓存行对齐。
    • HtrieBucket:存储冲突记录,使用位图(col_map)标记有效记录,最多支持 63 条记录。
    • Record:存储键值对,动态分配内存以支持变长键和值。
    • MemoryPool:无锁内存分配器,预分配大块内存,通过原子偏移量(offset)分配节点和 bucket,确保线程安全。
  2. 并发支持
    • 使用 std::atomic 实现无锁操作(如 shiftscol_mapnext),通过 compare_exchange_strong 确保线程安全。
    • 全局和线程局部世代计数器(global_genlocal_gen)用于检测并发修改,避免 ABA 问题。
    • 内存序(如 std::memory_order_acquirestd::memory_order_release)优化性能同时保证一致性。
  3. 核心操作
    • 插入(insert)
      • 根据哈希值下降到目标节点(descend)。
      • 如果槽位为空,分配新 bucket 并插入记录。
      • 如果槽位已有 bucket,尝试在空位插入记录,使用 CAS 更新位图。
      • 如果 bucket 记录数超过 8(should_burst),触发爆发(burst_bucket),将记录重新分配到新索引节点。
    • 查找(find)
      • 下降到目标 bucket,遍历位图查找匹配的键。
      • 如果找到,复制值到输出 vector。
    • 删除(remove)
      • 查找目标记录,原子清除位图中的对应位,释放记录内存。
    • 爆发(burst_bucket)
      • 当 bucket 记录过多时,创建新索引节点,将记录按哈希值重新分配到新 bucket。
  4. 性能优化
    • 缓存友好HtrieNode 大小为 64 字节,匹配缓存行,减少缓存失效。
    • 无锁设计:使用原子操作和 CAS 避免锁竞争,提高并发性能。
    • 内存池:预分配内存,减少动态分配开销,64 字节对齐优化访问效率。
    • 批量处理:爆发机制通过分层索引减少冲突,提高查询效率。
  5. 与 Neovim 的关联
    • 在 Neovim 中编辑此代码时,可利用 clangd 的 LSP 功能进行代码补全、错误诊断和跳转(如跳转到 hash_key 定义)。
    • Telescope 插件可快速查找 insertfind 函数,Trouble 插件可显示潜在的并发问题(如 CAS 失败的处理)。
    • 使用 compile_commands.json 配置 Boost 和标准库路径,确保 clangd 正确解析头文件。
  6. 与 Principia Mathematica 的关联
    • Lisa Lippincott 的演讲强调整数运算的严谨性,代码中的哈希函数(hash_key)和位操作(如 get_bits)涉及整数运算,可能受到类型提升或溢出的影响。
    • 可使用 widening 类型重写 hash_key,确保哈希值计算与数学整数集 ℤ 一致,避免溢出。
    • 例如,hash = ((hash << 5) + hash) + key[i] 可改用 widening 类型和 add_with_carry,增强语义安全性。
  7. 性能测试
    • 插入测试:插入 10,000 条记录,测量总耗时。
    • 查找测试:查找 10,000 条记录,统计成功次数和耗时。
    • 验证和删除:测试特定键(key_42)的查找和删除,验证正确性。
    • 典型输出(视硬件而定):
      === 插入测试 ===
      插入10000条记录耗时: 5000 μs
      === 查找测试 ===
      查找10000条记录耗时: 2000 μs
      找到记录数: 10000
      === 验证测试 ===
      Key: key_42
      Value: value_42
      === 删除测试 ===
      成功删除 key_42
      验证删除: key_42 已不存在
      
  8. 优化建议
    • 哈希函数:DJB2 算法简单但可能分布不均,考虑使用 MurmurHash 或 FNV-1a 提高均匀性。
    • 内存管理:内存池分配失败时可扩容,或使用动态分配作为备用。
    • 爆发阈值should_burst 的阈值(8)可根据实际负载调整,优化查询/插入平衡。
    • 统计功能:实现 stats 函数,统计节点数、bucket 分布和冲突率,辅助性能分析。
    • 随机测试数据main 中的键值对较为规律(key_0key_9999),可使用随机字符串测试冲突处理能力。

总结

  • 功能BurstHashTrie 是一个高效的键值存储结构,结合哈希表和前缀树的优点,支持无锁并发操作,适用于高性能场景。
  • 设计亮点
    • 无锁并发通过原子操作和 CAS 实现。
    • 爆发机制动态调整结构,减少冲突。
    • 内存池和缓存行对齐优化性能。
  • 应用场景:适合需要快速插入、查找和删除的场景,如数据库索引、缓存系统。
  • 改进方向
    • 增强哈希函数的分布性。
    • 实现链式 bucket(next 字段未使用)。
    • 添加统计功能分析性能瓶颈。
Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐