在解决了P2的bpm_test(可见前一篇文章)之后,我又花费了一些时间解决了一下P2其他test的bug,终于完成了P2。在做P2的过程中我遇到了很多问题,有着一些自己的思考,在此进行粗浅的记录,希望能够帮助到需要的人。由于本人还是学生,一些问题的理解难免有偏差与错误,欢迎大家批评指正。
课程要求不得在网上传播自己的源代码,以下内容均不包含我的代码。
一、Grandscope测试截图
二、遇到的问题
1. ExtendibleHTableTest.RemoveTest2
(1)问题描述
VerifyIntegrity()出现错误,其在Bustub的源代码如下
template <typename K, typename V, typename KC>
void DiskExtendibleHashTable<K, V, KC>::VerifyIntegrity() const {
BUSTUB_ASSERT(header_page_id_ != INVALID_PAGE_ID, "header page id is invalid");
BasicPageGuard header_guard = bpm_->FetchPageBasic(header_page_id_);
auto *header = header_guard.As<ExtendibleHTableHeaderPage>();
// for each of the directory pages, check their integrity using directory page VerifyIntegrity
for (uint32_t idx = 0; idx < header->MaxSize(); idx++) {
auto directory_page_id = header->GetDirectoryPageId(idx);
if (static_cast<int>(directory_page_id) != INVALID_PAGE_ID) {
BasicPageGuard directory_guard = bpm_->FetchPageBasic(directory_page_id);
auto *directory = directory_guard.As<ExtendibleHTableDirectoryPage>();
directory->VerifyIntegrity();
}
}
}
(2)问题分析
在仔细查看VerifyIntegrity()函数的源代码之后,我发现它对于header中所有的有效页面都要VerifyIntegrity(),是否有效判断的标准为页面ID是否等于INVALID_PAGE_ID(-1)。在仔细查看我的代码后发现,对于header_page而言,只有当我需要创建directorypage的时候才会调用header_page->SetDirectoryPageId()。由于header_page的映射数组的定义是:
page_id_t directory_page_ids_[HTABLE_HEADER_ARRAY_SIZE];
由于C/C++数组的默认值为0,不是-1(INVALID_PAGE_ID),这也就意味着虽然我没有创建过header_page对应index的directory_page,它的值仍然存在且为0。在header_page进行VerifyIntegrity()的时候会认为这个index对应存在directory_page,但实际上并没有创建。对没有创建的directory_page进行验证当然会报错。
此外,需要注意的是,数组默认的值为0,这也就意味着每个没有创建的directory_page的page_id都为0,而0恰恰是header_page的ID,所以以下操作相当于取出了header_page,将其转成directory_page,再对其进行验证,简直难蚌
BasicPageGuard directory_guard = bpm_->FetchPageBasic(directory_page_id);
auto *directory = directory_guard.As<ExtendibleHTableDirectoryPage>();
directory->VerifyIntegrity();
(3)解决方案
在初始化header_page的时候对其所有页面进行ID的初始化(即设置ID为-1)
//设置directory的页面为INVALID_PAGE_ID防止被检查integrity
for (uint32_t i = 0; i < header_page->MaxSize(); i++) {
header_page->SetDirectoryPageId(i, INVALID_PAGE_ID);
}
2. ExtendibleHTableTest.RemoveTest2
(1)问题描述
在设置了header_page的directory_page_ids_之后仍然VerifyIntegrity()错误
(2)问题分析
这个问题我真的要吐槽一下Bustub了,源码错了也不改,别人都在github上提出来了还是不改
我已经按照之前提的方式修改了数组的初始值,但是然并卵,于是我又分析起了源代码,结果发现了Bustub源代码的问题。为了证明Bustub真的是这样的,我又登陆了一下Bustub的页面,截了一下图,如下所示
截图只是为了证明在此时此刻它真的是这样的,出现错误的代码如下
auto ExtendibleHTableHeaderPage::GetDirectoryPageId(uint32_t directory_idx) const -> uint32_t { return 0; }
这个函数显然该返回page_id_t(int32_t),但是函数签名写的却是uint32_t,这也就意味着我所设置的-1会被强制转换为uint32_t导致数据的溢出,uint32_t当然也就不存在等于-1的概念,也就意味着无效的directory_page仍然会进行VerifyIntegrity(),当然会导致报错。
在我发现这个问题的时候已经有人在github提出来了,但是这个文件的上次修改还是2个月以前
(3)解决方案
显然将函数签名修改一下即可解决问题
3. ExtendibleHTableTest.GrowShrinkTest
(1)问题描述
插入数据不成功,并显示空指针访问
(2)问题分析
通过std::cout打印log得知,这个test的参数如下:
40: header_max_depth_: 9
40: directory_max_depth_: 9
40: bucket_max_size_: 511
40: bpm size: 3
在插入511对应的键值对时出现错误
仔细查看代码以及对照此测试数据可知,bpm的size只有3,也就是说,如果在一次插入中不释放页面的话,header_page+directory_page+bucket_page总共就占据了3页,在插入511时刚好旧的bucket_page大小不够,需要获得新的bucket_page,而bpm的大小只是3,以下语句会失败
BasicPageGuard bucket_guard = bpm_->NewPageGuarded(&new_bucket_id);
此bucket_guard对应的page为nullptr,在我操作此空指针时当然会报错,也不会插入成功
(3)解决方案
在不需要header_page时及时释放,腾出bpm的空间,供新的bucket_page使用
4. ExtendibleHTableTest.RecursiveMergeTest以及其他涉及Merge的test
在学习ExtendibleHashTable的过程中我发现网上很多教程都在讲Split bucket,很少有讲Merge bucket的,以致很多Merge的过程都需要我自己花很长的时间去探索。此外,Split bucket和Merge bucket都是一个recursive的过程,这也给编程带来了一定的难度,所以我自己在第三部分粗略的总结了一下Split/Merge bucket的方法,并尝试深入的探究recursive的过程。
(1)问题描述
36: /autograder/source/bustub/test/container/disk/hash/grading_extendible_htable_test.cpp:291: Failure
36: Expected equality of these values:
36: directory_page->GetGlobalDepth()
36: Which is: 1
36: 0
(2)问题分析
根据报错显示directory_page的tGlobalDepth()不对,我的程序运行的结果为1,但是实际上的结果应该是0。可以推测出在最后一次merge的时候没能成功的进行recrusive_merge,只是merge了一次导致错误。
利用std::cout打印log可知本次测试的参数以及流程为:
36: header_max_depth_: 1
36: directory_max_depth_: 2
36: bucket_max_size_: 2
36: insert 4
36: insert 5
36: insert 6
36: insert 14
36: remove 5
36: remove 14
36: remove 4
可见在remove4的时候merge出现了问题,我将RecursiveMergeTest的测试内容作为一个新的test加入本地测试,单步调试发现自己merge的过程有bug,并对其进行了修改,成功解决了RecursiveMergeTest中的问题,具体的insert、remove过程分析放在了第三部分(三、2)
(3)解决方案
修改merge过程代码,实现recursive_merge
三、Split/Merge Buckets 分析
Recrusive Split
在插入数据时,如果一个bucket的size已经达到了max_size,则需要申请新的bucket,并对原bucket中的内容以及新插入的内容进行重新分布。这样就产生去了一种可能:尽管创建了新的bucket,但是原bucket中的内容在重新分布之后仍然均处于旧的bucket中,而且新插入的数据仍然插入旧的bucket,则需要继续进行位数的扩张,直到原bucket中的内容与新的数据的内容加起来至少有一个数据插入了新产生的bucket,才算插入成功。如果达到了directory_page的最大depth仍然插入失败,则返回false。
Recrusive Merge
Merge的条件:桶为空且Split_Image的local_depth与自身的local_depth一致
在一个桶变空后,首先Drop并在bpm中删除此桶,接着在directory_page中将与本桶相关联的index的page_id均设置为INVALID_PAGE_ID。接着判断Split_Image的local_depth与自身的local_depth是否一致,若不一致则停止merge,若一致则设置被删除bucket_page的page_id为Split_Image的bucket_page_id,并减小原bucket_page对应的所有index与Split_Image对应bucket_page的所有index的local_depth。接着判断是否能够减小drectory_page的global_depth,直到不能减小。接着判断条件:合并之后的桶是否为空且新的Split_Image的桶是否为空,若不符合条件,则继续merge,如果符合条件,则退出。
算法如下(我自己总结的,可能有问题,欢迎批评指正):
设与bucket_index指向相同页面的所有index的集合为A(bucket_index)
若删除后bucket_index对应的bucket_page为空
设置A(bucket_index)的page_id为INVALID_PAGE_ID
while(bucket_index的local_depth==bucket_index_split的local_depth)
{
A(bucket_index)的local_depth-1
A(bucket_index_split)的local_depth-1
设置A(bucket_index)的page_id为=bucket_index_split的bucket_page_id
while(directory_page->canShrink())
{
directory_page->Shrink();
}
更新集合A:A(bucket_index)=A(bucket_index)+A(bucket_index_split)
更新bucket_index_split(因为local_depth减小了,bucket_index对应的bucket_index_split也会变化)为new_bucket_index_split
if(旧的bucket_index_split对应的bucket_page非空且new_bucket_index_split对应的bucket_page非空)
{
break;
}
}
1. 经典插入、删除情形分析
DiskExtendibleHashTable参数:
header_max_depth_: 0
directory_max_depth_: 3
bucket_max_size_: 2
bpm size: 50
操作数据:
0、1、2、3、4、5、6、7
(1) 插入数据0、1、2、3、4、5、6、7
INSERT 0
INSERT 1
INSERT 2
INSERT 3
INSERT 4
INSERT 5
INSERT 6
INSERT 7
(2) 删除数据0、1、2、3、4、5、6、7
REMOVE 0
REMOVE 1
REMOVE 2
REMOVE 3
REMOVE 4
REMOVE 5
REMOVE 6
REMOVE 7
在此directory_page不存在数据之后,将此directory_page的page_id设置为INVALID_PAGE_ID(-1)
2. 特殊插入、删除情形分析(ExtendibleHTableTest.RecursiveMergeTest测试案例)
DiskExtendibleHashTable参数:
header_max_depth_: 1
directory_max_depth_: 2
bucket_max_size_: 2
bpm size: 50
操作数据:
4、5、6、14