上一个Project中,我们实现了Bustub中的Buffer Pool模块,实现了包括LRU置换算法,Buffer Manager和并行Buffer等内容,而这次的Project2需要我们自己实现Bustub中的Index模块
START
Bustub中的索引采用的是Hash Index,并且今年的Project中采用了可扩展的Extendable Hash作为索引的核心组成部分,并要求实现并发的控制等操作。
整个实验分成了三个部分,分别是Page相关的数据结构的实现,Hash表本身的实现和并发控制的实现,下面进入正题。
PAGE LAYOUTS
第一部分我们需要实现索引页的基本数据结构,前面我们已经提到过,Buffer Manager的功能是对页进行管理,里面集成了Disk Manager,LRU等多个组件,就是为了对各种各样的页的读写和置换进行有效的管理,而现在就轮到了我们真的实现其中的一些页的时候。可扩展Hash中,页分成了两种,一种是目录页(Directory page),另一种是桶页面(Bucket Page),目录页面主要保存的是Hash的结果和桶的对应关系(后面会详细说可扩展Hash在这方面的设计),而桶主要用来保存key-value的数据对。
同时我们不能使用页以外的东西存储Hash表的信息,也就是说需要做到自包含,即可以只通过读入Hash表来复原整个可扩展Hash索引。
Directory Page
首先是第一部分目录页的设计,目录页用来保存hash表中的元数据,并且被分成了以下几个部分:
变量名 | 大小 | 描述 |
---|---|---|
page_id_ |
4 bytes | 自身的页编号 |
lsn_ |
4 bytes | 日志序列号(这个Project中暂时不用) |
global_depth_ |
4 bytes | 可扩展Hash的全局深度 |
local_depths_ |
512 bytes | 可扩展Hash的局部深度,是一个数组,每个桶对应一个值 |
bucket_page_ids_ |
2048 bytes | 桶和Page_id 的对应关系 |
我们不难发现其实这个设计非常合理,而我们需要实现的函数主要都是一些对这个基本数据结构的查询和修改,包括:
1 | uint32_t HashTableDirectoryPage::GetGlobalDepth() { |
这里的绝大部分内容都没什么难度,比较有点意思的只有GetGlobalDepthMask
,这个方法可以获得一个全局深度的mask用来处理key经过hash函数后得到的值。这是因为可扩展哈希中,我们需要将一个key映射到目录的索引中,这个过程使用的规则就是: 0x00000007
Bucket Page
Bucket Page是用来存放具体key-value对的页,它的核心数据结构是三个数组:
occupied_
: 数组中的第i个bit代表array中的第i个位置是否已经被占用readable_
: 数组中的第i个bit代表array中的第i个元素是否可读array_
: 用来存储具体的key-value对
因此初始代码中这样定义了三个数组:
1 | template <typename KeyType, typename ValueType, typename KeyComparator> |
值得注意的是这里的occupied, readable
被定义成了char数组,而1个char的大小是1byte,也就是8bit,我们之前说两个数组用第i位表示key-value数组的第i个位置是否被占用/可读,而这几个标记数组中每8个位置就凑成一个char,所以我们在查询某个位置是否被占用/可读的时候,要先将其映射到对应的char中,然后通过位运算对8个bit中指定的bit进行操作。
事实上写代码的时候我真的用到了位运算的操作,包括and, or和shift等等,作业的要求是我们要实现这些内容:
Insert
插入新的Key-value对Remove
移除旧的Key-value对IsOccupied
判断某个位置是否被占用IsReadable
判断某个位置是否可读KeyAt
找到某个位置的KeyValueAt
找到某个位置的Value
两个判断函数的实现方式如出一辙,就是将输入的index转化到对应的char和对应的位置上,然后通过位运算取出对应位置的数据进行操作,比如判断被占用,只要把那个位置上的bit拿出来,和1求一个and就可以判断那个位置是不是1,值得注意的是,虽然按道理说bit应该是从左往右数的,但是因为二进制表示是从右往左排列的,为了操作方便,我们在每个char里,bit和索引的对应关系就按照从右往左的方式排序了,即从右往左,8个bit依次是当前char的0-7位,输入的索引号先对应到某个具体的char上(index / 8
),然后用余数代表在一个char中的偏移量,也就是排位。不过好消息是这么做区别不大,甚至更方便了。
1 | template <typename KeyType, typename ValueType, typename KeyComparator> |
而KeyAt
和ValueAt
两个函数也是如出一辙,实现比较简单,就是找到对应位置上的pair然后返回key或者value即可:
1 | template <typename KeyType, typename ValueType, typename KeyComparator> |
这里要注意array是一个弹性数组,这是一个我没听说过的C++特性,但总之就是定义了
array_[0]
之后就可以把array当成数组来使用
比较有挑战性的是Insert和Remove两个函数,倒不是因为他们的操作很复杂,而是因为作业给我们设定好的函数签名比较诡异,比如Remove,作业给定的函数签名中并没有对应的index,而是给了key,value和一个comparator比较器,需要我们自己遍历整个bucket来找到要删除的位置,Insert也是类似,没有指定要我们插入到哪个位置,而是给了key和value让我们自己决定,这样一来也需要我们遍历整个bucket然后找到一个合适的位置来插入新的key-value对。
- 在Insert中,我们实际上就是要先找到一个没有被占用的空位
- 在Remove中,我们实际上要先找到待删除的Key和Value对应的位置,然后把readble和occupied两个数组对应位置设置成0
所以这两个函数最后实现的代码如下:
1 | template <typename KeyType, typename ValueType, typename KeyComparator> |