CMU15445 2023 PROJECT #2 - EXTENDIBLE HASH INDEX

在解决了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

(1) 插入数据4、5、6、14

INSERT 4

在这里插入图片描述

INSERT 5

在这里插入图片描述

INSERT 6

在这里插入图片描述

INSERT 14

在这里插入图片描述

(2) 删除数据5、14、4、6

REMOVE 5

在这里插入图片描述

REMOVE 14

在这里插入图片描述

REMOVE 4

在这里插入图片描述

REMOVE 6

在这里插入图片描述

  • 28
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CMU 15445 课程的 Project 0 是一个调试练习,旨在帮助学生熟悉调试工具和技术。在这个项目中,你将开始使用 GDB 和 Valgrind 这两个常用的调试工具,以及一些其他辅助工具。以下是一些问题和步骤,帮助你完成这个练习: 1. 你需要查看项目中提供的代码,并了解它的结构和功能,这样你才能更好地理解程序的逻辑和可能出现的 bug。 2. 接下来,你需要编译项目,并确保没有编译错误。如果出现错误,你需要修复它们,这可能需要检查一些语法错误或缺失的库。 3. 一旦成功编译项目,你就可以使用 GDB 进行调试了。GDB 是一个强大的调试器,可以帮助你找出程序中的错误。你可以使用 GDB 来单步执行代码、设置断点、查看变量的值等等。通过使用 GDB,你可以逐步查看代码运行的路径,并找出程序崩溃或产生错误的原因。 4. 在使用 GDB 进行调试时,你可以通过设置断点来暂停程序的执行,并查看变量的值和程序的状态。你可以使用“break”命令在程序中设置断点,并通过“run”命令启动程序。当程序到达这个断点时,它会停止执行,你可以使用“print”命令查看变量的值,或者“step”命令逐步执行代码。 5. 另一个常用的调试工具是 Valgrind。Valgrind 可以帮助你检测内存泄漏和错误的访问方式。你可以使用“valgrind”命令来运行程序,并查看 Valgrind 的输出。它会告诉你有关程序中任何潜在问题的信息,例如未初始化的变量、访问越界等。 6. 最后,当你发现 bug 并修复它们后,可以运行各种测试用例来验证程序的正确性。测试用例可以帮助你确定程序是否按预期工作,并且在修改代码后,它们可以帮助你确保你的修复没有引入新的错误。 通过完成 CMU 15445 项目 0 的调试练习,你将掌握一些重要的调试技巧和工具,这对于进一步开发和调试软件应用程序将非常有用。希望上述步骤和建议对你有所帮助,祝你顺利完成这个项目!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值