[已满分在线评测] cmu15445 2022 PROJECT #1 - BUFFER POOL
CMU15-445 PROJECT #1 - BUFFER POOL
前前言
本地测试通过是真的比较简单,因为有数据可以单步debug,很快就能定位错误。但是要通过在线评测还是比较痛苦的,没有数据,没办法单步调试,痛苦面具。
由于本次实验比较新,很少有现成的博客参考思路以及踩坑(有些坑真的很蠢,但是要花费很多很多很多时间,甚至2/3的时间都花在一个非常蠢的坑上面),因此我建了一个群,大家一起来讨论交流,共同学习,共同进步吧。
687463684
前言
终于来到了Project #1 了,这次是要实现三个组件,分别是 Extendible Hash Table
, LRU-K Replacement Policy
, Buffer Pool Manager Instance
。个人感觉,LRU-K Replacement Policy
的实现比较自由,需要自己编写一些额外的内容。另外两个组件的大致框架以及内置的一些变量都已经写好了,只需要实现对应的函数即可。三个组件中,分别都有一个比较细节或者不那么trival的地方(至少我感觉是这样的)。第一个哈希表比较卡壳的地方就是Insert,第二个替换策略比较细节或者说自由的地方就是如何记录时间戳。第三个缓冲池管理组件中,就是按照函数的brief
中的内容按部就班写了。由于再写这篇博客的时候gradescope
中non cmu课程的对应lab还没开放测评,因此我这里只过了本地评测,可能并不是完全正确,特别是在并发这方面。对于并发,我直接一把大锁保平安了,遇事不决,一把大锁。至于如果之后在线评测的时候TLE了的话,那就只能拆分锁了。
工业革命
这次debug用上了船新的lldb+vscode,体验是真的不错,比自己手动debug效率提高了好多好多。具体看博文:https://www.cnblogs.com/alyjay/p/16709127.html
Extendible Hash Table
实现细节以及一些思考
-
对于key是从高位取比特还是从地位开始作为dir的index
在已经实现的IndexOf函数中,取的是地位比特作为index。为什么取低位而不是高位呢?比如有一批key,范围从0到1000,那么其高位全为0,低位不相同,所以这里取低位比特更容易将所有key分散到不同的bucket中,而不是挤在一个bucket中从而导致全局深度以指数级速度爆涨。但是key如果不是整数,而是某种特殊的编码,比如身份证号,高位表示省份等信息,低位表示个人信息,在这种情况下说不清是取高位还是低位,各有优劣吧。奇思妙想一下,是否有可能将高位和低位组合起来。
-
insert,bucket分裂以及global深度增加细节(已经废弃,请看文章末尾)
- 对于bucket分裂,我们迫切需要的就是找到其兄弟索引。在这里,兄弟索引指的是在全局深度的限制下,仅最高位bit与自身不同的index即为兄弟。比如索引
001011101
的兄弟就是索引101011101
。也就是0的兄弟是1,1的兄弟是0。这就呼之欲出了,直接上异或即可,mask的最高位为1,其余位为0。公式如下。分裂之后将新的索引项指向新创建出来的bucket即可。idx ^ (1 << (global_depth_ - 1))
- 对于全局深度的增加,也就是索引项增加了一倍。假设增加后的索引数量为2n,那么我们只需要将n到2n-1对应的索引指向其兄弟索引指向的bucket即可。
- 对于插入,使用while循环不断尝试插入,直到插入成功为止。由于重新hash也要使用插入函数,因此这里存在一个递归的操作,这比较影响并发。我在这里分别实现了一个带lacth和不带latch的Insert函数,带latch的调用不带latch的实现递归。
- 对于bucket分裂,我们迫切需要的就是找到其兄弟索引。在这里,兄弟索引指的是在全局深度的限制下,仅最高位bit与自身不同的index即为兄弟。比如索引
测评结果
LRU-K Replacement Policy
实现细节以及一些思考
-
如何记录Frame的metadata、
在LRUKReplacer类中创建了一个辅助类FrameMeta。
-
用什么来保存key到value的映射信息
其实是可以用咱们上面自己实现的hash表的,但是因为上面的哈希表没有实现收缩功能,因此对内存压力比较大。因此我用的unordered_map。在这里也有trade off。我觉得access的次数坑定是要远远大于换页的操作的。原本我想采用优先队列实现,因为优先队列在换页的情况下O(1)的时间复杂度就能找到需要换出的页,但是要更新页时间戳的话需要O(N)去遍历查询。由于查询更新操作数量必定远远大于换页操作,因此在这种wordload下,采用unordered_map我觉得是非常好的。
-
用什么来存储时间戳信息
我使用了一个循环数组(或者说循环vecotr)来存储时间戳。数组大小为K。如果数组元素小于k,那么表示这个还没达到k次access,直接返回
timestamps[0]
即第一次访问的时间戳即可。如果大小超过或者与k相同,那么返回timestamps[(cur+1)%k]
即可。其中cur为上一次访问的时间戳索引。(cur+1)%k
必定为前K次的访问时间戳的索引。
评测结果
Buffer Pool Manager Instance
实现细节以及一些思考
- 没啥细节,细节就是建议画个流程图。步骤有点复杂,不画流程图可能会漏情况。(我就因为没画,debug了1小时)。
评测结果
在线评测满分截图
。。。最近真的是没啥学习的动力,导致直接摆了一整个月都没动。本来的代码是通过本地评测的,然后上了在线评测就直接寄了,又因为一个月没碰这份代码了,很多思路都消散了,改了两天才满分,真的心累。改的过程中也发现了一些本来就错误的地方。
-
在extendible hash table中
之前的那个思路是错误的,当时没有细想local depth的作用,直到这次仔细思考了一下local depth的作用才知晓了。主要作用是表明bucket内的有效识别位数,次要作用是为了记录有效位数以便到时候增长global depth。每次满了之后需要提高local depth是为了让bucket内的元素进行分类,每次都需要将所有指向该bucket的索引重新分配指向old bucket还是new bucket。 -
在lru-k中
踩的坑主要就是当页面已经被驱逐,这时候如果对已经驱逐的页面设置evitable属性,这时的操作应该是直接设置,不进行curr_size_的增加与减少。一开始我的设计是会将其放入buffer中,导致有一个点卡了整整一下午,直到转变了思想才过。呜呜呜呜呜呜呜 -
在buffer pool中
做了太多的骚操作,在每个相关函数里面都RecordAccess了一下,其实只需要在NewPage以及FetchPage中record一下就行了,这玩意又卡了我一下午,呜呜呜呜呜 -
满分截图
-
下面来看看苦逼的提交记录吧
-
这次更新了用时排行榜
一把大锁保平安,甚至是普通的互斥锁保平安,读写锁也没用,所以时间慢是肯定的。有时间改成读写锁吧。然后如果时间再多一点就拆分锁吧(大工程,感觉又要和一大堆bug斗争了,本身就没系统学习过并行编程,哎,还是要多学学)。 -
b+树
p2的b+树的本地测试也已经通过了,接下来就是和在线测试斗智斗勇了。
总结
这次lab内容确实是比lab0要多了一些,但是难度可能还稍微有点下降了(2022.10.29 是我大意了)。毕竟lab0还有一些c++11标准的相关特性以及标准。不过也有可能是因为在线测评没开放,我只过了local test,可能还有没踩到的坑。后续在线测试更新了再来跟新这篇博客吧。(感觉并发要杀我,我全用的大锁保平安大法(2022.10.29 没想到竟然没超时,没超时我就没有拆分锁的动力了啊。。。啊啊啊啊啊))