Succinct Data Structure

最近看了一篇论文 SuRF: Practical Range Query Filtering with Fast Succinct Tries ,里面提到使用一种新的数据结构 Succinct Range Filter(SuRF) 替换掉了 RocksDB 默认的 Bloom filter, 在一些性能测试上面,尤其是 seek 上面,性能提升了不少,并且也降低了很多 I/O 开销,这一下子就引起了我的兴趣。

大家都知道,RocksDB 里面,为了加速 key 查询的速度,使用了 Bloom filter,但 Bloom filter 只适用于 point get,对于 seek 就无能为力了。虽然 RocksDB 后面引入了 prefix seek,但对于 key 的格式有要求,使用比较受限。为了提高 RocksDB range query 的速度,论文的作者引入了一种省空间的数据结构,也就是 SuRF。

在了解 SuRF 之前,首先要了解掌握的就是 Succinct data structure 相关的知识,这篇文章主要是讲 Succinct data structure 相关的东西,后面再讨论 SuRF 如何实现的。

Rank 和 Select

Succinct data structure 第一次提出,应该是 Guy Jacobson 的论文 "Succinct static data structures",但实话,我在网上找了半天,都没找到这篇 paper,只是找到了作者另一篇 Space-efficient static trees and graphs。它的主要思想就是使用非常少量的空间(接近信息编码的下界)来存储数据。你可以认为就是使用了一种非常高效的压缩算法,但不同于压缩,它同时来提供非常高效的查询。

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

  • rank1(x) - 返回在 range [0, x] 里面 1 的个数
  • select1(y) - 返回第 y 个 1 所在的位置

上面我们只是列举了 rank1 和 select1,对应的也有 rank0 和 select0,这里就不需要解释了。这么说有点过于抽象,这里举一个简单的例子。假设我们有一个 bits 序列 11000001,那么 rank1 和 select1 可以映射如下:

pos 0 1 2 3 4 5 6 7
bits 1 1 0 0 0 0 0 1
rank1(p) 1 2 2 2 2 2 2 3
select1(p) 0 1 7

另外,大家可以注意到,rank 和 select 其实是相反的,上面的例子,select1(3) = 7,然后我们也会发现, rank1(7) = 3

Level Order Unary Degree Sequence

上面简单介绍了下 Succinct data structure 的 rank 和 select。 在 SuRF 里面,它参考的基础编码方式,是 Level order unary degree sequence(LOUDS),在 LOUDS 里面,我们会将一颗树,分层依次进行编码。而规则也是非常的简单,如果这个树的节点有 N 个子节点,那么就用 N 个 1 来编码,然后最后加上 0。

假设我们有如下的 tree:

                    +---+
            +------ | 1 | ------+
            |       +---+       |
            |         |         |
            |         |         |
            v         v         v
          +---+     +---+     +---+
  +------ | 2 |     | 3 |     | 4 | ------+
  |       +---+     +---+     +---+       |
  |         |                   |         |
  |         |                   |         |
  v         v                   v         v
+---+     +---+               +---+     +---+
| 5 |     | 6 |               | 7 |     | 8 | ------+
+---+     +---+               +---+     +---+       |
                                                    |
                                                    |
                                                    v
                                                  +---+
                                                  | 9 |
                                                  +---+

为了计算简单,LOUDS 会加入一个 pseudo root 节点,这里我们变成如下的 tree

                    +---+
                    | 0 |
                    +---+
                      |
                      |
                      v
                    +---+
            +------ | 1 | ------+
            |       +---+       |
            |         |         |
            |         |         |
            v         v         v
          +---+     +---+     +---+
  +------ | 2 |     | 3 |     | 4 | ------+
  |       +---+     +---+     +---+       |
  |         |                   |         |
  |         |                   |         |
  v         v                   v         v
+---+     +---+               +---+     +---+
| 5 |     | 6 |               | 7 |     | 8 | ------+
+---+     +---+               +---+     +---+       |
                                                    |
                                                    |
                                                    v
                                                  +---+
                                                  | 9 |
                                                  +---+

然后我们对这个 tree 进行编码,得到

                          +--------+
                          |  0-10  |
                          +--------+
                            |
                            |
                            v
                          +--------+
              +---------- | 1-1110 | ------+
              |           +--------+       |
              |             |              |
              |             |              |
              v             v              v
            +-------+     +--------+     +-------+
  +-------- | 2-110 |     |  3-0   |     | 4-110 | ------+
  |         +-------+     +--------+     +-------+       |
  |           |                            |             |
  |           |                            |             |
  v           v                            v             v
+-----+     +-------+                    +-------+     +------+
| 5-0 |     |  6-0  |                    |  7-0  |     | 8-10 |
+-----+     +-------+                    +-------+     +------+
                                                         |
                                                         |
                                                         v
                                                       +------+
                                                       | 9-0  |
                                                       +------+

那么生成的 bits 序列为:

pos 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
bits 1 0 1 1 1 0 1 1 0 0 1 1 0 0 0 0 1 0 0
rank1 1 1 2 3 4 4 5 6 6 6 7 8 8 8 8 8 9 9 9
rank0 0 1 1 1 1 2 2 2 3 4 4 4 5 6 7 8 8 9 10
select1 0 2 3 4 6 7 10 11 16
select0 1 5 8 9 12 13 14 15 17 18

那么我们拿到了这一个序列到底有什么用呢?在 LOUDS 里面,我们可以非常方便的进行很多操作,假设我们的 node 就是按照上面的,0,1,2,这样的 number 来标记的,position 对应的就是 bits 里面的 position。我们通常会用两个计算公式来得到 node number 和 position 的对应关系:

  • node-num = rank1(i) :在 position i 得到对应的 node number,譬如 rank1(2) = 2
  • i = select1(node-num),根据 node number,知道对应的 position,譬如 select1(2) = 2

有了上面的公式,我们就能对这个 tree 进行操作了:

  • first_child(i) = select0(rank1(i)) + 1 - 得到第 i 个位置所在节点的第一个子节点所在的 position
  • last_child(i) = select0(rank1(i) + 1) - 1 - 得到第 i 个位置所在节点的最后一个子节点所在的 position
  • parent(i) = select1(rank0(i)) - 得到第 i 个位置所在节点的父节点所在的 position
  • children(i) = last_child(i) - first_child(i) + 1 - 得到第 i 个位置所在节点的子节点的个数
  • child(i, num) = first_child(i) + num 得到第 i 个位置所在节点的第 num 个子节点所在的 position,num >= 0

上面这些公式感觉好绕,那么我们来一个简单的例子,以节点 4 为例。从上面的 tree 可以知道,4 的 parent node 是 1,它的第一个子节点是 7,最后一个是 8,总共有两个子节点。

首先我们需要计算节点 4 的位置,根据上面的公式 select1(4) 我们得到 position 是 4。那么第一个子节点位置就是 first_child(4) = select0(rank1(4)) + 1 = select0(4) + 1 = 9 + 1 = 10,那么第一个子节点就是 rank1(10) = 7

我们再来计算最后一个子节点,根据公式,最后一个就是 last_child(4) = select0(rank1(4) + 1) - 1 = select0(4 + 1) - 1 = 12 - 1 = 11,那么最后一个子节点就是 rank1(11) = 8

再来看看父节点,就是 parent(4) = select1(rank0(4)) = select1(1) = 0,那么父节点就是 rank1(0) = 1

Epilogue

使用 LODUS,我们可以用 bits 方便的编码一棵树,然后用 rank 和 select 操作,就能方便的对 tree 进行遍历,业内已经有很多 paper,能将 rank 和 select 做到 O(1) 的开销,所以速度还是很快的。

但在实际中,如果光用 LODUS,性能还是没法保证的,所以这也是为啥会有 SuRF 的原因,关于 SuRF,后面会在说明。

在数据库领域,Succinct 是一个比较有趣的研究方向,也有很多数据库采用了 succinct 来保存数据,毕竟如果能用更少的空间存放数据,memory 能装的更多,cache 更友好,性能就更好。但现在 succinct 还没有大规模的落地,可以看看后续的发展。如果你对构建新的存储引擎有兴趣,欢迎联系我 tl@pingcap.com

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

推荐阅读更多精彩内容

  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,740评论 0 33
  • DPDK提供了三种classify算法:最长匹配LPM、精确匹配(Exact Match)和通配符匹配(ACL)。...
    cumirror阅读 26,385评论 13 21
  • 春姑娘踏着轻盈的脚步来了,每个角落里都蕴藏着春天的气息,到处是春暖花开,万物复苏的景象。今天在老师的带领下...
    茶语心林阅读 420评论 0 0
  • 这个周末,冬阳灿烂,缕缕如锦缎,暖暖的。我喜欢它万丈光芒,喜欢它温柔的亲吻万物,喜欢在它温暖的包围中或散步,或...
    荷塘恋雨阅读 209评论 0 0