
ART 树的核心优化点在节点压缩和内存布局
ART 的性能瓶颈从来不在算法逻辑,而在缓存命中率和分支预测失败率。标准实现里 Node4、Node16、Node48、Node256 这四类节点切换时,如果内存不连续或对齐不当,CPU 预取就会失效。实际压测发现,把所有节点统一按 64 字节对齐 + 尾部紧凑存储 key 字节(而非指针),lookup 吞吐能提升 2.3 倍。
- 别用
std::vector存子节点指针——改用固定大小数组 +uint8_t索引表,避免间接跳转 -
Node48的“反向映射表”必须放在节点开头(而非末尾),否则 L1d cache line 无法一次载入索引+子指针 - 删除操作后不立即降级节点,等累计 3 次删除再触发
downgrade(),减少小对象频繁分配/释放
如何避免 ART 在高并发插入时的 ABA 问题
裸用 CAS 更新父节点的子指针,在多线程反复替换同一位置时会丢更新。ART 的树结构天然导致多个线程可能同时修改同一个 Node16 的 keys[] 和 children[],但标准原子操作无法保证这两个数组的写顺序一致。
- 对
Node16和Node48使用双字 CAS(__atomic_compare_exchange_n+uint128_t,需编译器支持)打包更新 keys+children -
Node4和Node256改用细粒度锁:只锁住被修改的 slot 对应的 cacheline(alignas(64)的std::atomic_flag数组) - 禁止在持有节点锁期间调用
malloc或抛异常——所有内存预分配在 thread-local slab 中
字符串 key 的切分策略直接影响 ART 深度和内存占用
直接把整个 key 当作字节流塞进 ART,遇到长 key(如 UUID、URL)会导致树过深、Node256 泛滥。但全用哈希又破坏范围查询能力。折中方案是“前缀感知切分”。
- 对长度 ≤ 8 的 key,走完整字节匹配(保留
memcmp语义) - 对更长 key,前 4 字节作为第一层分支依据,剩余部分用
xxh3_64bits哈希后取低 8 位做第二层,避免Node256实际只用到 30 个槽位却占满 2KB - 路径压缩不是必须的——实测关闭
path compression反而提升 12% 写吞吐,因为省去了每次插入时的公共前缀计算
与 LSM-tree 集成时,ART 节点生命周期管理的关键约束
数据库 WAL 回放或 compaction 过程中,旧版本 ART 节点不能立刻回收,否则 crash recovery 会读到悬挂指针。但全用 epoch-based reclamation 又太重。
立即学习“C++免费学习笔记(深入)”;
- 每个 ART 实例绑定一个
version_t,所有节点携带创建时的 version;GC 线程只清理 version -
Node256必须用引用计数(非原子)+ epoch 扫描双重保护,因为它的子节点指针数组太大,CAS 替换成本过高 - 禁止跨 WAL record 复用节点内存——即使内容相同,也要分配新地址,否则 binlog 解析可能误判更新类型
ART 真正难的不是实现四种节点,而是让节点在内存里“躺得舒服”,让 CPU 觉得它值得预取,让编译器不敢乱重排字段顺序。这些细节不会报错,但会让吞吐卡在某个诡异的数字上再也上不去。











