Fast Succinct Tries

[TOC]

参考

读后感 SuRF: Practical Range Query Filtering with Fast Succinct Tries
SuRF: 基于Fast Succinct Tries的Range Query Filter

1. Succinct Data Structure

1.1. 简介

Succinct 数据结构的主要思想就是使用非常少量的空间(接近信息编码的下界)来
存储数据,是一种通过位图形式表达数据结构的技术。可以认为它就是使用了一种非常高效的压缩算法,但不同于压缩,它同时来提供非常高效的查询。开源实现可见sdsl-lite

1.2. LOUDS

对于 Succinct data structure 来说,我们会将数据按 0 和 1 来编码,所以可以用 bits,而不是 bytes。操作 succinct 数据,通常的就是以下几个操作函数:

// x 表示比特位的位置
rank1(x) - 返回在 range [0, x] 里面 1 的个数
select1(y) - 返回第 y 个 1 所在的位置
rank0(x) - 返回在 range [0, x] 里面 0 的个数
select0(y) - 返回第 y 个 0 所在的位置

在 SuRF 中,参考的基础编码方式是 Level order unary degree sequence(LOUDS),在 LOUDS 里面,我们会将一颗树,分层依次进行编码。而规则也是非常的简单,如果这个树的节点有 N 个子节点,那么就用 N 个 1 来编码,然后最后加上 0。如图1,把每个节点的编码顺序排列,就得到了 LOUDS 编码。

观察上图,有两个核心点需要理解:

  • 对于节点位置i,i之前比特值为0的个数表示前面存在的节点数量,如节点4之前,有4个0,表示节点4之前有4个节点,分别是0,1,2,3;
  • 对于节点位置i,i之前比特值为1的个数表示前面存在的节点数量加上这些节点对应的直接子节点个数,如节点4之前,有8个1,说明加上直接子节点,共有8个节点,分别是,0,1,2,3,5,6,7,8.
    注:前面 是 相对层次遍历而言
# i 表示节点编号(从0开始),p表示bit位编号(从0开始)
第i个节点的起始bit位置 = select0(i) + 1
已知bit位等于1且bit位置为p,该比特位对应节点的子节点编号i = rank1(p)
第i个节点的父节点起始bit位置 = select1(i)
起始bit位置为p的节点编号 = rank0(p)
第i个节点(其bit位置=p)的第k个子节点(k从0开始)的起始bit位置 = select0(rank1(p+k)) + 1
第i个节点的父节点的起始bit位置 = select1(rank0(p))

举例说,取节点3,那么他的子节点是5、6,父节点是0。

  • 节点3的起始bit位置 = select0(3) + 1 = 7
  • 节点3第一个子节点编号 = rank1(7 + 0) = 5
  • 节点3第二个子节点编号 = rank1(7 + 1) = 6
  • 节点3的父节点起始bit位置 = select1(3) = 2,父节点编号 = rank0(select1(3)) = rank0(2) = 0
  • 节点3的第1个子节点的起始bit位置 = select0(rank1(7 + 0)) + 1 = select0(5) + 1 = 11
  • 节点3的第2个子节点的起始bit位置 = select0(rank1(7 + 1)) + 1 = select0(6) + 1 = 12
  • 节点5的父节点起始bit位置 = select1(5) = 7,父节点编号 = rank0(select1(5)) = rank0(7) = 3
  • 节点6的父节点起始bit位置 = select1(6) = 8,父节点编号 = rank0(select1(6)) = rank0(8) = 3

2. SuRF: 一个优化的 Fast Succinct Tries

2.1. Fast Succinct Tries

SuRF 的核心数据结构就是 Fast Succinct Tries(FST),一种空间节省,支持 point 和 range query 的静态 trie。
基于LOUDS编码方式, FST对LOUDS进行了进一步压缩, 下图介绍了基本的压缩方法:



在很多时候,对于一棵树来说,上层的 trie 节点较少,但访问频繁,也就是我们俗称的 hot,而下层的节点则相对的 cold 一点。因此,SuRF 使用了两种数据结构来分别处理 hot 和 cold 节点。在 upper 层上面使用了 LOUDS-Dense,而在 lower 层上面使用 LOUDS-Sparse。

2.1.1. LOUDS-Dense

假设每个节点最多有256个子节点, 那么在LOUDS-Dense编码方式中, 每个节点使用3个256个bit的bitmap来保存信息. 这3个bitmap分别是:

  • D-Labels: 将子节点的label变化置位,表示这个 node 是否有 label i,如果有,那么第 i bit 位就是 1。譬如上面的例子,Dense 的 label 在 level 1 有 f,s 和 t,那么在第 102(f),115(s) 和 116 (t)bit 位就会设置为 1。大家其实可以看到,具体哪一个 bit 位,就是 ASCII 码的值。
  • D-HasChild: 标记对应的子节点是否是叶子节点还是中间节点,如果一个 node 下面还有子节点,那么就将该 label 对应的 bit 在 D-HasChild 里面设置为 1。继续上面的例子,f 和 t 都有子节点,而 s 没有,所以 102 和 116 bit 都会设置为 1。
  • D-IsPrefixKey: 标记当前前缀是否是有效的key 我们仍然可以使用select&rank操作来访问对应的tree节点。这个解释其实有点绕,主要是用来表示一个 prefix 是否也是一个合法的 key。还是上面的例子,我们可以看到,f 这个 node 是有子节点的,所以它是一个 prefix,但同时,f 也是一个 key。在上图中, SuRF 使用了 ‘$’ 这个符号(实际对应的值是 0xFF)来表示这样的情况。

最后一个字节序列就是 D-Values,存储的是固定大小的 value。Value 就是按照 每层 level 的顺序存放的。

如果要进行遍历 LOUDS-Dense,我们可以使用之前提到的 rank 和 select 操作。对于 bit 序列bs来说,我们用rank1/select1(bs, pos)来表示在bs上面posrankselect操作。譬如,假设posD-Labels上面的当前bit pos,如果D-HasChild[pos] = 1,那么第一个子节点的pos则是D-ChildNodePos(pos) = 256 x rank1(D-HasChild, pos),而父节点则是ParentNodePos(pos) = 256 x select1(D-HasChild, pos / 256)

2.1.2. LOUDS-Sparse

LOUDS-Sparse使用3个bit序列来对trie树进行编码, 在整个bit序列中, 每个节点的长度相同, 这三个bit序列分别是

  • S-Labels: 记录每个节点的label编号, key节点用0xFF标记, 按照树的层数按顺序记录(如果最多有256个子节点, 则每个节点占用4个byte)。譬如上图的 rst 这样的 bytes 顺序,Sparse 仍然使用了 0xFF(也就是上图的 $ 符号)来表示一个 prefix key。因为这样的 0xFF 只会出现在第一个子节点上面,所以是能跟实际的 0xFF label 进行区分的。
  • S-HasChild: 记录每个节点是否含有子节点, 有的话标记为1, 每个节点使用一个bit
  • S-LOUDS: 记录每个节点是否是第一个节点, 每个节点使用一个bit 仍然可以使用rank&select操作来访问整个trie树。如果一个 label 是第一个节点,那么对应的 S-LOUDS 就设置为 1,否则为 0。譬如上图第三层,r,p 和 i 都是第一个节点,那么对应的 S-LOUDS 就设置为 1 了。

2.1.3. 好处和空间开销

2.1.3.1. 好处

trie树经过LOUDS-DS编码之后, 可以高效支持下面3个操作:

  • ExtractKeySearch(key): 如果key存在, 返回value
  • LowerBound(key): 返回一个迭代器, 迭代器指向第一个大于等于key的位置
  • MoveToNext(iter): 移动迭代器指向下一个key-value

2.1.3.2. 空间复杂度分析

给定一个含有n个节点的trie树, S-labes需要使用8n个bits, S-HasChild和S-LOUDS一共使用2n个bits, 所以LOUDS-Sparse使用10n个bits. LOUDS-Dense的空间与Sparse和Dense的分界线有关, 通常情况下, Dense占用的空间要远远小于Sparse部分. 这样整个LOUDS-DS编码的Trie树接近10n个bits, 理论证明最少的编码数量大约是9.44n个bits, 接近理论的下限了.

2.2. 优化

对于 SuRF 来说,为了提高查询的速度,一个重要的优化手段就是提高 rank 和 select 执行的效率,在 SuRF 里面,引入了 LookUp Table(LUT)。

对于 rank 来说,会将 bit vector 切分成 B bits 大小的块,每块都使用 32 bits 的字段来预先保存了计算好的到这个 block 的 rank 值。譬如,在上面的例子,第三个就是 7,保存的就是前两个 block 总的 rank 数量。

而对于一个 pos 来说,它的 rank1(pos) = LUT[i / B] + popcount[i / B * B, i]popcount 是一个 CPU 指令,用来快速的计算某一段区间的 1 的个数。假设我们现在要得到 pos 12 的 rank 值,先通过 LUT[12 / 5] = LUT[2] = 7,然后得到 range [12 / 5 * 5, 12] = [10, 12],使用 popcount 得到 2,那么 12 的 rank 就是 9。
对于 select 来说,也是使用的 LUT 方法,预先记录算好的值。具体到上面,假设将采样的周期设置为 3,那么第三个 LUT 就保存的是 3 x 2,也就是第 6 的 1 的 pos 值,也就是 8。对于一个 pos 来说,select1(i) = LUT[i / S] + (selecting (i - i / S * S)th set bit starting from LUT[i / S] + 1) + 1。譬如,如果我们要得到 select1(8),首先得到 LUT[8 / 3] = LUT[2] = 8,然后需要从 position LUT[8 / 3] + 1 = 9 这个位置,得到第 (8 - 8 / 3 * 3) = 2 个位置的 bit,也就是 1,所以 select1(8) 就是 10。

当然,SuRF 还有其它很多优化手段,譬如使用 SIMD 来提速 label 的查找,使用 prefetch 技术等,这里就不说明了。

2.3. Succinct Range Filter

对于通常的 SuRF 来说,它因为对这个 trie 都进行了编码,所以它是完全精确的,虽然它是一种省空间的数据结构,但很多时候,我们仍然需要能保证在内存里面存储所有的 SuRF 数据,所以我们就需要对 SuRF 进行裁剪,不存储所有的信息,也就是说,我们需要在查询的 False Positive Rate(FPR)和空间上面做一个权衡。

在 SuRF 里面,有几种方式,Basic,Hash,Real 以及 Mixed。

  1. Basic SuRF

FST是一个完整的索引结构, 可以存储全部的索引数据, 这种情况下是100%精确的. Basic SuRF的思想就是只存储key的前缀, 实际上就是砍掉树的部分叶子节点. 我们使用FPR(false positive rate)来衡量效果, 具体的FPR与key的分布有关, 论文中给出了Basic SuRF的FPR的上限.

  1. SuRF with Hashed Key Suffixes

为了降低FPR, 在Basic SuRF的基础上, 对key进行hash计算之后, 将hash值的n个bits存储到value中, 查询的时候还原回来完整的key. 这种方法可以降低FPR, 论文中有计算公式, 但是这种方法对range query没什么帮助.

  1. SuRF with Real Key Suffixes

和SuRF with Hashed Key Suffixes不同, SuRF-Real存储n个bits的真实key, 这样point查询和range查询都可以获益, 但是在point查询下, FPR比SuRF-Hash要高.

  1. SuRF with Mixed Key Suffixes

为了享受Hash和Real两种方式的优点, Mix模式就是将两种方式混合使用, 混合的比例可以根据数据分布进行调节来获得最好的效果.

2.4. 应用场景

试想如果我们把rocksdb的所有key都复制一份存储在SuRF中的话(不存储value), 那么SuRF起的作用不就和bloomfilter一样了么, 同时还可以支持range query了。具体的性能测试可见(读后感 SuRF: Practical Range Query Filtering with Fast Succinct Tries) 为此论文将SuRF应用在了Rocsdb中, 替换了bloomfilter, 并且进行了对比测试(占用的空间和bloomfiler相同). 测试程序运行在普通的SSD上, 下图是性能对比数据:


从性能数据上看, 对于point query, SuRF的效果比bloomfilter相比还是差一些, 但是在range query下, 效果比bloomfilter要好很多了, IO减少的次数还是非常明显的.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容