简单复盘CMU15-445 fall2021 Lab Project2
概述
基于已经实现了的Buffer Pool实现一个Extendable HashTable,具体两个部分。
基本数据结构Bucket和Directory以及HashTable的主体。
Hash Table Bucket Page
基本说明
BucketPage的size和磁盘以及内存中page的大小相同,都是4KB,三个数据成员:readable_
,occupied_
以及array_
, 三者都是数组。
readable_
:char类型数数组,因此具有sizeof(readable_) * 8
个bit,第k个bit用来记录array[k]
是否有存储的可读数据。occupied_
:同上,但是每个bit用来记录array[i]是否被使用过,意义不大。不过由于我们使用array_
是顺序扫描,所以可在扫描array_
时提前结束,具体可看该官方在该page中实现的PrintBucket方法。array_
:存储的是支持泛型的std::pair
,而BucketPage限制了大小,因此array_
采用的是GNU C支持的零长数组。
其余的成员方法看起来难度都不大,但是想实现更高的性能,必须要使用很多巧妙的位运算,一开始我认为自己实现的方法都非常棒,但是实际测试时却有很多bug,不得已只能修改为比较平庸的算法。具体实现不放了。
注意事项
- 这个类的默认构造函数上被添加了
delete
关键字,关于这个关键字网上有很详细的说明,后续我也会学习一下贴一篇笔记,总之就是为了内存安全考虑。 - 注意一下三个成员变量数组长度的设置,尤其是前两个
occupied_
这个数组的每一个bit基本上只会被设置一次,即使对应元素被删除也不需要置0,因为我们判断位置上是否有存储的数据是根据readable_
数组的bit.
Hash Table Directory Page
基本说明
同上,DirectoryPage也是对应了硬件中的一个page:数据成员如下:
global_depth_
:当前HashTable的global depthlocal_depths_
:uint_32类型的数组,存储的是每个Bucket的local_depthbucket_page_ids_
:page_id_t类型的数组,存储的是当前每个Bucket指向的物理page idlsn_
:后面的lab中会用到的日志字段page_id_
:存储自己的物理page_idis_valid_
:应该是标识该directory是否可用,本次project中没有用到
这个类里的方法也都比较简单,但是也涉及到一些位运算,而且注意445这里是推荐使用末尾的n位作为bucket_idx的
注意事项
- 虽然本身有一个page_id的字段,但是该类并不是和Buffer Pool中管理的Page对象是同一级别的,而是与Bucket Page一样,和硬件的page大小相同。
- 虽然使用directory管理buckets,但是directory中不存储指向page的指针,只存储bucket对应的page_id,让上层可以i使用buffer pool访问到具体的bucket page。
- 并不是官方提供的所有方法都必须实现并且使用
Extendble Hash Table
基本说明
这个类持有一个buffer pool管理器和一个directory_page_id,也就是说上层引擎会持有一个此类的对象,然后调用这个对象的Insert
、GetValue
、Remove
方法,
讲一下逻辑,以上的三个方法,前提都是将key转换成Hash Result,然后用这个Hash Result配合当前directory的global_depth_
获取到key对应的bucket_idx
,有了bucket_idx
之后自然也就能获取到bucket_page_id
,从而也就能获取到实际的物理page。注意这个类只是持有directory_page_id_
。
至此,几个重要的工具方法:Hash
,KeyToDirectoryIndex
,KeyToPageId
,FetchDirectoryPage
,FetchBucketPage
的作用也就显而易见了。
接下来是几个核心方法:
GetValue
:传入的是key引用和result数组指针,注意这里是数组,因为本次实验中要求实现的是非唯一key Hash,也就是说允许同一个key与不同的value映射,传入一个key可能会查询到多个valueInsert
:传入的是key和value的引用,大题的逻辑是先获取到目标bucket,上文已经讲了那几个方法的作用,拿到bucket之后检查是不是满了,如果满了就需要进行SplitInsert
,也就是分裂bucket,分裂的逻辑有点复杂,中间包括directory扩容、rehash数据以及重定向bucket指针等等。后期会上参考资料。分裂完成之后再尝试重新插入目标的key和value。Remove
:传入的是key和value,逻辑和Insert
类似,但是反过来,如果删除成功,那么就要检查删除之后的bucket是不是为空,如果为空就要尝试着进行Merge
操作,也就是合并之前SplitInsert
分裂开的镜像bucket,中间掺杂了directory的收缩、重定向bucket指针,总体来说比SplitInsert
要简单一些。
注意事项:
- 官方给出的两个Fetch方法,返回类型分别是directory page和bucket page,但是在进行并发控制时,我们需要使用Page对象中的读写锁,因此,可以将FetchBucketPage的返回类型改为Page,然后再使用
reinterpret_cast
,将Page->data_转型成为Bucket Page对象,这样就能使用Page对象中的读写锁保护对应的Bucket Page - 类型的构造方法中需要对directory_page_id_初始化,并且为改directory page分配一个local depth为0的bucket
- 所有的NewPage和FetchPage操作,都会在Buffer Pool中Pin,因此当我们不再使用该page时一定要及时UnPin。读写锁也是同理。
- 关于并发控制,这是我第一次使用读写锁,因此没有太深的理解。最直观的想法就是,没有修改的可能,就是用读锁,只要有任何修改的可能,就要使用写锁。
- 另外该类中的读写锁命名为Lock,而Page中确实latch,可能值得好好思考一下。
总结
本篇文章仅作为作者自己的备忘,刚做完之后向随便记录一下加深理解,后续整个lab做完会写一份详细的解析或者是指导。当然并不会涉及具体代码,仅仅为像我一样没有什么基础的菜鸟提供一个入门的方向。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库