首页 > 数据库 > 数据库开发技术 > ToplingDB CSPP Trie 设计解析
一提到 Trie,很多人可能会想到 Double Array Trie,但是,在这里,必须断了 Double Array Trie 这个念头。
这个 CSPP Trie 是要为 MemTable 服务的,所以性能需求十分苛刻,按照预期:
CSPP Trie 代码已开源: topling-zip/cspptrie.cpp,以极低的内存用量,在性能上远远超出了最开始的预期
| 并发级别 | 限制条件 |
|---|---|
| NoWriteReadOnly | 只读,不可修改 |
| SingleThreadStrict | 插入导致 Iterator 失效 |
| SingleThreadShared | 插入保持 Iterator 有效 |
| OneWriteMultiRead | 插入保持 Iterator 有效 |
| MultiWriteMultiRead | 插入保持 Iterator 有效
|
在自动机的语境中, 结点与 状态是等价的,文中有些地方会两者混用,比如: 孩子结点, 终止状态。
从内存池中分配出来的内存块用一个整数偏移来表示,偏移和尺寸都对齐到 4,这样做有两个好处:
为了最小化内存用量,光是把 64 位的指针换成 32 位的偏移是远远不够的:
Patricia Trie 结点最多有
256 个孩子指针ValueSize 大小的空间保存该节点对应的 value所以,结点的大小不能是固定的,而是要 按需分配!
写线程插入一条数据,会修改一些结点,在多线程场景下……
Copy On Write 不用解释。
Access Token 包含 Reader Token 和 Writer Token,它表示,每个 Token 可以通过某种方式持有对一些结点的引用,在该 Token Invalidate 这些引用之前,这些引用必须保持有效。
严格讲,只有 Access Token 是必须的(Copy On Write 非必须):如果结点大小是固定的,并且不是带有路径压缩的 Patricia Trie,那么:
从而,我们只需要确保,即便孩子指针已被修改为新值,旧的仍然被 Token 引用的孩子继续保持有效,那么,其它并发的操作就是有效的:要么访问新值,要么访问旧值,绝对不会出现未定义行为。在这里,只有 Access Token,没有 Copy On Write。
现在,结点的尺寸和布局不是固定的,修改无法通过原子操作完成,只能对整个读操作和整个写操作加锁,然而,即便使用读写锁,性能的损失也非常巨大。
所以,我们采用了 Copy On Write 机制:一个写操作(例如整个插入操作)所有的修改,都是在源结点的拷贝上进行修改,所有的新结点(和新节点上面的链接关系)完全构造好之后,再修改父结点上的指针。此时,需要结合 Copy On Write 与 Access Token。
2. Token 构造时拷贝 Trie 版本计数器的当前值
// 伪代码,省略加锁、延迟删除等具体细节
if (LazyFreeQueue.head.version < TokenList.head.version)
MemPool.Delete(LazyFreeQueue.pop().nodePtr);这个场景最简单:
单线程,不需要任何并发控制,也不需要 Token,修改会导致之前的 Iterator 失效。牺牲了这么多特性,获得的收益有:
单线程情况下,很多时候,我们需要保证修改操作不会破坏已有的 Iterator,此时:
一个写线程,多个读线程
3. 不能 realloc,从而内存池的容量必须一开始就固定下来
因为写操作不需要加锁,所以写性能可以达到极致,实测和 SingleThread 几乎相同。
最彻底的并发:
实测中,在 i7-6700K(4核8线程) 上,开 8 线程,写性能加速比达到 6.5 倍,所以,超线程还是非常有用的!
对 NVM(例如 Intel Optane DIMM)非常友好,每次插入最多修改三个 NVM 内存块!
我们达到的性能远超预期,特别是,动态随机插入的性能,比
strVec 还快! 这个性能是非常惊人的,Patricia 内存占用更小,并且是
有序的,
动态的,
并发的,性能还要比静态的
sort 更高!
strVec 是指以
StringVector 作为存储,进行排序、查找2.StringVector 进行了深度优化:StringVector 上逐个 Append String,然后排序OneWriteMultiRead3.NLT 的
插入,指 build 整个 NLT4.RbTree 和
Patricia 的
插入就是通常意义的插入2.NLT,
RbTree,
Patricia 的
LB 采用各自的优化实现· strVec 和
RbTree 的点查基于
LB 实现,所以比它们自己的
LB 要慢一点· Patricia 和
NLT 的点查相当于在 DFA 上执行匹配,原则上比它们自己的
LB 要快· Patricia 和
NLT 的
LB 通过一个重量级的
Iterator 对象来实现Patricia 的
LB 经过了极高度的优化,性能仍然比
点查慢NLT,不管是点查还是 LB,搜索过程中都需要重建压缩路径NLT 的点查,并没有比 LB 快多少(两者几乎一样)按照最开始的预期,不管是
Patricia 还是
NLT,他们的点查要比 LB 快得多,但实测结果是各自的点查和 LB 性能相差无几!使用 Profiling 工具,我们最终发现,性能瓶颈是
内存墙,在 Patricia 的点查里面,对 DFA 状态的访存(随机访问),占了 90% 的时间,所以,可以看到,顺序和随机搜索,性能差了那么多,其实就是随机访存的缘故!
NLT 的点查与 LB 性能比 Patricia 更接近,是因为在 NLT 中,内存的访问更加随机,内存墙造成的影响更大!
下面是性能对比图,测试用的数据是维基百科英文版的标题:

我们还加入了和静态 Patricia
NLT(Nest Louds Trie) 的对比
StringVector 创建 NLT图中 OPS 表示每秒 插入或 搜索多少个 Key(1 M op/sec 表示每秒 100万 次操作),把 OPS 折算成吞吐率,要乘以 Key 的平均长度,对这个维基百科数据,要乘以 20。
对于 Patricia Trie,在大部分情况下,随机插入比顺序插入要占用更多内存,极端情况下,内存占用会多出 20%。即便如此,内存占用仍比 RbTree 低很多:


这个性能意味着:把这个 Patricia Trie 用在 RocksDB 的 MemTable 上,性能会比 RocksDB 自身的
vector MemTable
更高!要知道,
vector MemTable
为了写性能而牺牲了读性能,读(搜索)是 O(n) 的,这相当于放弃了读(搜索)功能!
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70013015/viewspace-2908591/ ,如需转载,请注明出处,否则将追究法律责任。