CMU15445(2023fall) Project #2 - Extendible Hash Index 匠心分析

news2025/5/16 12:49:12

胡未灭,鬓已秋,泪空流

此生谁料

心在天山

身老沧州

——诉衷情

完整代码见:

SnowLegend-star/CMU15445-2023fall: Having Conquered the Loftiest Peak, We Stand But a Step Away from Victory in This Stage. With unwavering determination, we press onward, for destiny favors those brave enough to forge ahead, cutting through the thorns and overcoming every obstacle that dares to stand in their way.

Task #1 - Read/Write Page Guards

实验说明分析:

        在缓冲池管理器中,FetchPage NewPage 函数返回已经锁定的页面指针。这个锁定机制确保页面在没有任何读取和写入时不会被驱逐。为了表示页面在内存中不再需要,程序员必须手动调用 UnpinPage

        然而,如果程序员忘记调用 UnpinPage,页面将永远不会被驱逐出缓冲池。由于缓冲池实际上只有较少的帧,这将导致更多的页面在磁盘和内存之间交换。这样不仅会影响性能,而且这个 bug 很难被发现。

        您将实现 BasicPageGuard 类,它存储指向 BufferPoolManager Page 对象的指针。一个页面保护器确保一旦超出作用域,就会调用 UnpinPage 来解除锁定页面。请注意,它仍然应该提供一个供程序员手动解除锁定页面的方法。

        由于 BasicPageGuard 隐藏了底层的页面指针,它还可以提供只读/写数据的 API,这些 API 在编译时进行检查,确保正确设置 is_dirty 标志。

        在这个和后续项目中,多个线程将会读取和写入相同的页面,因此需要使用读写锁来确保数据的正确性。请注意,在 Page 类中,已经为此目的提供了相关的锁方法。类似于解除锁定页面,程序员可能会忘记在使用后解除锁定。为了解决这个问题,您将实现 ReadPageGuard WritePageGuard,它们会在作用域结束时自动解除页面的锁定。

        由于在2024-fall的project1分析过了类似的,故实现这块不在赘述。说实话我个人还是更喜欢2023f版的,这个版本的对PageGuard进行拆分得恰到好处。先是给出BasicPageGuard让我们对PageGuard的作用有个具体的理解,再Basic的基础上可以进一步升级为ReadPageGuard(下面简写为RPG)WritePageGuard(简写为WPG)。这一套流程看下来2024f版对PageGuard的拆分程度太高了,上来就要求实现RPG和WPG,理解起来的难度要大不少。

        接下来具体分析关于如何使用PageGuard。我相信很多人可能会对As和AsMut的用法有所疑惑。但无论是RPG还是WPG,其As与AsMut都是调用BasicPG的函数。我们只要理解透彻下面四个函数那对于PageGuard的使用也就是手到擒来了。

        可以发现,GetData()与As()是配套的,而GetDataMut()和AsMut()也是配套的。而且GetDataMut这个函数中,修改了is_dirty_,这说明一旦我们选择调用AsMut,那最好是真的对page的内容进行了修改,自然也只有WPG才能调用AsMut的。

        那什么时候调用RPG与WPG的情况也就呼之欲出了。只要需要修改page时,才需要调用WPG。如果只是读取page,那调用RPG就可以了。但是!如果你懒得思考要不要修改page那直接调用WPG也不是不行,就是会多造成disk IO降低吞吐率。如果不清楚是否需要修改page,保险起见可以直接调用WPG。

 1.   auto GetData() -> const char * { return page_->GetData(); }
 2.  
 3.   template <class T>
 4.   auto As() -> const T * {
 5.     return reinterpret_cast<const T *>(GetData());
 6.   }
 7.  
 8.   auto GetDataMut() -> char * {
 9.     is_dirty_ = true;
10.     return page_->GetData();
11.   }
12.  
13.   template <class T>
14.   auto AsMut() -> T * {
15.     return reinterpret_cast<T *>(GetDataMut());
16.   }
17.  

Task #2 - Extendible Hash Table Pages

实验说明分析

哈希表头页面(Hash Table Header Page

头页面位于基于磁盘的可扩展哈希表的第一层,并且每个哈希表只有一个头页面。它存储指向目录页面的逻辑子指针(作为页面ID)。可以将其视为一个静态的第一层目录页面。头页面具有以下字段:

变量名

大小

描述

directory_page_ids_

2048

一个目录页面ID的数组

max_depth_

4

头页面可以处理的最大深度

请注意,尽管页面的大小是物理限制,您应该使用 max_depth_ 来确定 directory_page_ids_ 数组的上限大小。

您必须通过仅修改其头文件(src/include/storage/page/extendible_htable_header_page.h)和相应的源文件(src/storage/page/extendible_htable_header_page.cpp)来实现可扩展哈希表的头页面。

哈希表目录页面(Hash Table Directory Page

目录页面位于基于磁盘的可扩展哈希表的第二层。每个目录页面存储指向桶页面的逻辑子指针(作为页面ID),并包含用于处理桶映射以及动态扩展和收缩目录的元数据。目录页面具有以下字段:

变量名

大小

描述

max_depth_

4

头页面可以处理的最大深度

global_depth_

4

当前目录的全局深度

local_depths_

512

一个桶页面本地深度的数组

bucket_page_ids_

2048

一个桶页面ID的数组

请注意,尽管页面的大小是物理限制,您应该使用 max_depth_ 来确定 bucket_page_ids_ 数组的上限大小。

您必须通过仅修改其头文件(src/include/storage/page/extendible_htable_directory_page.h)和相应的源文件(src/storage/page/extendible_htable_directory_page.cpp)来实现可扩展哈希表的目

录页面。

哈希表桶页面(Hash Table Bucket Page

桶页面位于基于磁盘的可扩展哈希表的第三层。它们实际上存储键值对。桶页面具有以下字段:

变量名

大小

描述

size_

4

桶中当前存储的键值对的数量

max_size_

4

桶可以存储的最大键值对数量

array_

小于或等于4088

存储键值对的数组

请注意,尽管页面的大小是物理限制,您应该使用 max_size_ 来确定 array_ 数组的键值对的上限大小。

您必须通过仅修改其头文件(src/include/storage/page/extendible_htable_bucket_page.h)和相应的源文件(src/storage/page/extendible_htable_bucket_page.cpp)来实现可扩展哈希表的桶页面。

重要说明:

每个可扩展哈希表的头/目录/桶页面对应于通过缓冲池获取的内存页面的内容(即 data_ 部分)。每当您读取或写入页面时,必须先从缓冲池中获取页面(使用其唯一的 page_id,然后将其重新解释为相应的类型,在读取或写入后解锁该页面。我们强烈建议您利用在任务1中实现的 PageGuard API 来实现这一目标。

举例说明可扩展哈希的插入流程

接下来直接举个例子来解释可扩展哈希表的具体实现过程:

初始Global_Depth=0,每个Bucket的Local_Depth=0,Bucket的max_size_=2。假设我们需要插入0 1 2 3 4 5 6 7 8 。

咳咳,又到了展示我精湛画图功力的时候了。Show time!

①插入0,1。由于初始的Bucket空间大小足够,可以直接插入到Bucket0中。紫色笔记表示depth,左侧表示Directory,右侧表示Buckets。

③接下来插入2,发现B0已经满了,无法直接插入。

  1. 所以B0先尝试local_depth++来进行桶分裂,但是由于此时local_depth已经等于global_detph了,所以loc_d自增失败,需要glo_d先自增。此时glo_d=1
  2. Loc_d++,即loc_d=1,自增成功可以进行桶分裂。原桶与新桶(brother)的loc_d都是1
  3. 将原Bucket的元素进行重映射,即遍历B0的所有元素,进行key&loc_depth_mask将元素分配到原桶和新桶中

如下图。0&1=0,1&1=1。所以0依然放在原桶中,1重新放在下标为1的新桶中。

  • 重新尝试插入2。通过2&global_depth_mask即10&1=0来把2放入B0中。

③插入3。通过3&global_depth_mask即11&1=1来把3放入B1中。

④插入4。4&global_depth_mask即100&1=0,发现B0已经满了,故准备进行桶分裂。

  1. 所以B0先尝试local_depth++来进行桶分裂,但是由于此时local_depth已经等于global_detph了,所以loc_d自增失败,需要glo_d先自增。此时glo_d=2
  2. Loc_d++,即loc_d=2,自增成功可以进行桶分裂。原桶与新桶(brother)的loc_d都是2
  3. 将原Bucket的元素进行重映射,即遍历B0的所有元素,进行key&loc_depth_mask将元素分配到原桶和新桶中

如下图。0&11=00,10&11=10。所以0依然放在原桶B0中,2重新放在下标为2的新桶B10中。

  • 重新插入4。4&global_depth_mask即100&11=00,所以把4插入到B0中

⑤插入5,5& global_depth_mask即101&11=01,发现B1这个桶已经满了,准备桶分裂

  1. Glo_d > loc_d,loc_d++成功,故桶B1可以直接分裂,得到新桶B11
  2. 将原桶B1的元素进行重映射(key&local_depth_mask)分配到B01和B11中
  3. 重新尝试插入5,5& global_depth_mask即101&11=01,插入B01成功。如果插入失败继续尝试桶分裂。

⑥插入6,6& global_depth_mask即110&11=10,成功插入到B10中

⑦插入7,7& global_depth_mask即111&11=11,成功插入到B11中

⑧插入8,8& global_depth_mask即1000&11=00,发现桶B00已经满了,尝试进行桶分裂

  1. 所以B00先尝试local_depth++来进行桶分裂,但是由于此时local_depth已经等于global_detph了,所以loc_d自增失败,需要glo_d先自增。此时glo_d=3
  2. Loc_d++,即loc_d=2,自增成功可以进行桶分裂。原桶与新桶(brother)的loc_d都是3
  3. 将原Bucket的元素进行重映射,即遍历B0的所有元素,进行key&loc_depth_mask将元素分配到原桶和新桶中
  4. 重新尝试插入8,8& global_depth_mask即1000&111=000插入成功。如果插入失败继续尝试桶分裂。

最后可扩展哈希表如下:

其实这个例子中,应该用page_id来标记唯一的桶,为了方便理解我就直接用下标来标识每个Bucket。

通过上述例子,我们可以总结可扩展哈希表的插入流程

  1. 尝试插入key。进行key&global_depth_mask从而找到对应的Bucket下标。当前Bucket不满,直接插入成功,return。如果失败进行步骤2
  2. Bucket满了。
    1. 当前global_depth > local_depth,此Bucket进行local_depth++成功,可以直接进行桶分裂。进行步骤3
    2. 当前global_depth =local_depth,此Bucket进行local_depth++失败,不可以可以直接进行桶分裂。进行Global_depth++,所以此Bucket可以进行local_depth++,桶分裂成功。进行步骤3
  3. 重映射。桶分裂成功后,遍历原Bucket,对其中的每个元素elem进行elem&local_depth_mask,从而将原Bucket的元素分配到原桶和新桶中。重新进行步骤1

实现过程分析

我最近发现了个方法论,把实验需求看了几遍之后,把lab涉及到的头文件,在这里是header_page.hdirectory_page.hbucket_page.h重新归纳一遍。只是硬看的话在实现函数的过程中很容易前面忘了,后面忘了,全忘了~简称忘3

一言蔽之,这个过程就是将插入的key进行各种转换,最终找到相应bucket。我直接将lab给出的图例进行了一番改造,下图就是一个key进行映射的过程。

为了更好地理解lab要求,在粗略浏览一番代码后,我们应该弄清楚以下几个问题:

Q:在Header部分,核心函数HashToDirectoryIndex的目的是什么呢?

A:提取key的最高几位,从而尝试进入对应的Directory。

Q:在Directory部分,如何获得Global_Depth?

A:作为task2的核心部分,大部分功能都是在directory_page.cpp进行实现的。在init()直接将Global_Depth和每个Bucket的Local_Depth都初始化为0就行。

Q:在Directory部分,HashToBucketIndex的目的是什么呢?

A:提取key的最低几位,从而尝试进入对应的bucket。这里用page其实更合适,因为每个bucket本质都是一张page

Q:搞清楚bucket_indexpage_id的区别

A:bucket_index并不是我以为的这种,而仅仅是directory中的数组下标page_id才是真正的bucket序列号,用来区分唯一的bucket

Q:GetSplitImageIndex()这个函数的作用是什么?

A:这个函数感觉没描述清楚,通俗来说就是获得当前bucekt的兄弟bucket。将当前bucket在Directory的下标最高有效位取反,就可以得到bro。例如,B0的兄弟是B1,B101的兄弟是B001。因为B101和B001都是从B01分裂而来的,所以他俩互为bro。

值得一提的是,task2并不需要实现这个函数。这个函数在task3才需要用到。

理解上述几个问题后,可以先尝试粗糙地将函数功能实现,然后对着测试样例一步步改进细化

 

 Task #3 - Extendible Hashing Implementation

实现了task2后,对可扩展哈希表的工作流程就已经大体了解得差不多了,现在就是具体实施对(key, value)的处理流程。为了找好着手点,我们可以自行模拟插入三个元素时,hash_table的工作流程。

就GetValue()、Insert()和Remove()三个核心操作而言,有一套共同的前期流程:

1、先从bufferpool中获得Header Page。至于是以ReadGuadPage还是以WriteGuadPage获得就取决于这次操作是否会修改Header Page了。得到key的最高几位后,在Header Page中找到这个key对应的Directory 页表项。如果存在,进入步骤2。

        如果没找到对应的Directory,return false或者从bufferpool中申请一个新的page来存放新的Directory Page,即调用InsertToNewDirectory。

2、找到对应Directory的页表项后,以Page Guad的方式从bufferpool中获取这个Directory Page。然后得到key的最低几位后,在Directory Page中找到这个key对应的bucket页表项。如果存在,进入步骤3。

3、同样是以Page Guad的方式从bufferpool中获取这个Bucket Page,然后就可以进行具体的操作了。

4、至于桶分裂、桶合并等操作就需要自己具体实现了。

对于桶合并的问题,分为三种情况:

  1. 当前bucket并没有bro,即它的local_depth=0
  2. 当前bucket有bro,但是bro和它指向同一个bucket page,这种连体婴就不需要继续合并了
  3. 当前bucket有bro,且bro和它指向不同的bucket page,这种情况才可以正常合并

Q:什么时候需要合并呢?

A:当前bucket和bro有一方为空的时候进行合并。

OK,然后就是对着测试样例一步步修正自己的代码了。在提交到gradescope时,果不其然我遇到了两个bug。

遇到的bug

1、合并时机没领悟通透,像下面这种情况是需要继续合并的

2、没有及时释放页面的锁

        说实话这个问题过于隐晦了。测试样例将bufferpool_size设置为3。而访问Header Page需要占据一个frame,访问对应的Directory Page需要占据第二个frame,访问对应的Bucket Page需要占据第三个frame。

        这个时候,如果需要访问当前bucket的bro,就需要从当前bufferpool中逐出一个page。但是由于我没有及时释放Header Page,就导致前面三个page赖在bufferpool不走了,也就无法访问新的page,造成访问地址错误。

        其实我感觉设置bufferpool_size=3是真的过于刻意了,就是盯着只有Header Page只有1个才想出来的馊主意。如果最高级的是root page,而且Header Page不止1个呢,那估计就会设置个bufferpool_size=4来卡一手人了。

        总而言之,个人感觉Extendible Hash Index思维难度并不高,代码量也适中。给我的感觉是构建一个b+树demo工作量就能与之一战了。

最后放一张通过的图

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2307966.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java面试】JVM汇总

目录 1.JVM为什么能跨平台&#xff1f; 2.JVM由哪些部分构成&#xff1f;每个部分起到什么作用&#xff1f; 3.什么是双亲委派&#xff1f;双亲委派的两大作用是什么&#xff1f; 举个例子&#x1f330;&#xff1a; 为什么要有这种“家族规矩”&#xff1f; 破坏双亲委派…

【SpringBoot】Spring 一站式解决方案:融合统一返回结果、异常处理与适配器模式

前言 ???本期讲解关于统一功能处理的详细介绍~~~ ??感兴趣的小伙伴看一看小编主页&#xff1a;-CSDN博客 ?? 你的点赞就是小编不断更新的最大动力 ??那么废话不多说直接开整吧~~ 目录 ???1.适配器模式? ??1.1适配器模式定义 ?编辑 ??1.2适配器模式角…

STM32基础篇(三)------滴答定时器

滴答定时器简介 SysTick定时器&#xff08;STK&#xff09; 处理器有一个24位系统定时器SysTick&#xff0c;它从重新加载值倒计时到零&#xff0c;在下一个时钟沿重新加载&#xff08;换行&#xff09;LOAD寄存器中的值&#xff0c;然后对后续时钟倒计时。当处理器暂停调试时&…

Sublime Text4安装、汉化

-------------2025-02-22可用---------------------- 官方网址下载&#xff1a;https://www.sublimetext.com 打开https://hexed.it 点击打开文件找到软件安装目录下的 ctrlf 查找 8079 0500 0f94 c2右边启用替换替换为:c641 0501 b200 90点击替换按钮 替换完成后 另存为本地…

CameraX学习1-关于预览、拍照、对焦

关于CameraX是否可以打开多种特殊摄像头&#xff0c;例如广角、长焦、景深等等 虽然CameraSelector只简单定义了前置后置&#xff0c;没具体指明摄像头&#xff0c;但是可以跟Camera2 API的CameraCharacteristics结合使用&#xff0c;获取对应的cameraid&#xff0c;再传入Came…

【愚公系列】《Python网络爬虫从入门到精通》033-DataFrame的数据排序

标题详情作者简介愚公搬代码头衔华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,CSDN商业化专家,阿里云专家博主,阿里云签约作者,腾讯云优秀博主,腾讯云内容共创官,掘金优秀博主,亚马逊技领云博主,51CTO博客专家等。近期荣誉2022年度…

RBF神经网络+NSGAII多目标优化算法,工艺参数优化、工程设计优化(Matlab)

目录 效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.RBF神经网络NSGAII多目标优化算法&#xff08;Matlab完整源码和数据&#xff09; 多目标优化是指在优化问题中同时考虑多个目标的优化过程。在多目标优化中&#xff0c;通常存在多个冲突的目标&#xff0c;即改善一…

执行yum -y install npt 报错解决

Cannot find a valid baseurl for repo: base/7/x86_64 解决办法 一、检查网络连接 确保你的服务器可以访问互联网。你可以使用 ping 命令来测试&#xff1a; ping www.baidu.com 若能访问外网&#xff0c;则网络没问题&#xff0c;否则检查网络 二、修改CentOS-Base.rep…

Android Studio 新版本Gradle通过JitPack发布Maven仓库示例

发布本地仓库示例&#xff1a;https://blog.csdn.net/loutengyuan/article/details/145938967 以下是基于 Android Studio 24.2.2&#xff08;Gradle 8.10.2 AGP 8.8.0 JDK17&#xff09; 的通过JitPack发布Maven仓库示例&#xff0c;包含aar和jar的不同配置&#xff1a; 1.…

【官方配图】win10/win11 安装cuda 和 cudnn

文章目录 参考资料1.安装cuda toolkit1. 下载安装包2.安装验证 2. 安装cudnn下载cudnn安装包安装cudnn安装后的配置 参考资料 官方nvidia安装cuda官方nvidia安装cudnn 1.安装cuda toolkit 1. 下载安装包 下载地址 https://developer.nvidia.com/cuda-downloads?target_osW…

水滴tabbar canvas实现思路

废话不多说之间看效果图,只要解决了这个效果水滴tabbar就能做出来了 源码地址 一、核心实现步骤分解 布局结构搭建 使用 作为绘制容器 设置 width=600, height=200 基础尺寸 通过 JS 动态计算实际尺寸(适配高清屏) function initCanvas() {// 获取设备像素比(解决 Re…

神经网络 - 激活函数(Sigmoid 型函数)

激活函数在神经元中非常重要的。为了增强网络的表示能力和学习能力&#xff0c;激活函数需要具备以下几点性质: (1) 连续并可导(允许少数点上不可导)的非线性函数。可导的激活函数可以直接利用数值优化的方法来学习网络参数. (2) 激活函数及其导函数要尽可能的简单&#xff0…

2.5 运算符2

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的 2.5.3 赋值运算符 赋值运算符将值存储在左操作数指定的对象中。有两种赋值操作&#xff1a; 1、简单赋值&#xff0c;使用。其中第二…

DeepSeek + 自由职业 发现新大陆,从 0 到 1 全流程跑通商业 IP

DeepSeek 自由职业 发现新大陆&#xff0c;从 0 到 1 全流程跑通商业 IP 商业定位1. 商业定位分析提示词2. 私域引流策略提示词3. 变现模型计算器提示词4. 对标账号分析提示词5. 商业IP人设打造提示词6. 内容选题策略提示词7. 用户人群链分析提示词8. 内容布局与转化路径设计提…

【Python】网络爬虫——词云wordcloud详细教程,爬取豆瓣最新评论并生成各式词云

目录 一、功能介绍 二、关键技术 1、安装WordCloud 2、利用WordCloud 1、WordCloud的基础用法 **相关参数介绍** **WordCloud 提供的方法如下** 2、WordCloud的应用举例 3、设置停用词 4、WordCloud使用词频 三、程序设计的步骤 1、抓取网页数据 2、数据清洗 3、…

第39天:安全开发-JavaEE应用SpringBoot框架Actuator监控泄漏Swagger自动化

时间轴&#xff1a; Java知识点&#xff1a; 功能&#xff1a;数据库操作&#xff0c;文件操作&#xff0c;序列化数据&#xff0c;身份验证&#xff0c;框架开发&#xff0c;第三方组件使用等. 框架库&#xff1a;MyBatis&#xff0c;SpringMVC&#xff0c;SpringBoot&#xf…

综合练习 —— 递归、搜索与回溯算法

目录 一、1863. 找出所有子集的异或总和再求和 - 力扣&#xff08;LeetCode&#xff09; 算法代码&#xff1a; 代码思路 问题分析 核心思想 实现细节 代码解析 初始化 DFS 函数 时间复杂度 空间复杂度 示例运行 输入 运行过程 总结 二、 47. 全排列 II - 力扣&a…

【Java SE】Java中String的内存原理

参考笔记&#xff1a; Java String 类深度解析&#xff1a;内存模型、常量池与核心机制_java stringx、-CSDN博客 解析java中String的内存原理_string s1 new string("ab");内存分析-CSDN博客 目录 1.String初识 2.字符串字面量 3.内存原理图 4. 示例验证 4.…

IDEA提示将方法形参更改为(什么什么类型),要检查对应的实体类中的字段类型是否正确

IDEA提示inviteCodeId应该是字符串&#xff0c;明显不对&#xff0c;后来检查发现是FakeRegistration类中把inviteCodeId定义为String类型了。

【芯片设计】NPU芯片前端设计工程师面试记录·20250227

应聘公司 某NPU/CPU方向芯片设计公司。 小声吐槽两句,前面我问了hr需不需要带简历,hr不用公司给打好了,然后我就没带空手去的。结果hr小姐姐去开会了,手机静音( Ĭ ^ Ĭ )面试官、我、另外的hr小姐姐都联系不上,结果就变成了两个面试官和我一共三个人在会议室里一人拿出…