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

胡未灭,鬓已秋,泪空流

此生谁料

心在天山

身老沧州

——诉衷情

完整代码见:

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.mzph.cn/diannao/72063.shtml

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

相关文章

P1706 全排列问题

题目描述 按照字典序输出自然数 1 到 n 所有不重复的排列&#xff0c;即 n 的全排列&#xff0c;要求所产生的任一数字序列中不允许出现重复的数字。 输入格式 一个整数 n。 输出格式 由 1∼n 组成的所有不重复的数字序列&#xff0c;每行一个序列。 每个数字保留 5 个场宽。…

会话与会话管理:Cookie与Session的深度解析

一、什么是会话&#xff1f; 二、Cookie&#xff1a;客户端存储技术 1. Cookie的工作原理 2、在后端设置cookie 3、在前端设置cookie 三、浏览器开启了cookie禁用怎么办&#xff1f; 一、什么是会话&#xff1f; 会话&#xff08;Session&#xff09;是指一个用户与服务器之间…

【Linux系统】—— 冯诺依曼体系结构与操作系统初理解

【Linux系统】—— 冯诺依曼体系结构与操作系统初理解 1 冯诺依曼体系结构1.1 基本概念理解1.2 CPU只和内存打交道1.3 为什么冯诺依曼是这种结构1.4 理解数据流动 2 操作系统2.1 什么是操作系统2.2 设计OS的目的2.3 操作系统小知识点2.4 如何理解"管理"2.5 系统调用和…

算法-二叉树篇15-最大二叉树

最大二叉树 力扣题目链接 题目描述 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点&#xff0c;其值为 nums 中的最大值。 递归地在最大值 左边 的 子数组前缀上 构建左子树。 递归地在最大值 右边 的 子数组后缀上 构建…

运维Apache面试题及参考答案

目录 简述 Apache Web 服务器的主要特点及适用场景 Apache 的默认监听端口是什么?如何修改为其他端口? Apache 的主配置文件名称及路径是什么?不同 Linux 发行版的默认路径有何差异? 解释 Apache 的 MPM(Multi-Processing Module)机制,列举常见的工作模式(如 prefor…

51c自动驾驶~合集52

我自己的原文哦~ https://blog.51cto.com/whaosoft/13383340 #世界模型如何推演未来的千万种可能 驾驶世界模型&#xff08;DWM&#xff09;&#xff0c;专注于预测驾驶过程中的场景演变&#xff0c;已经成为追求自动驾驶的一种有前景的范式。这些方法使自动驾驶系统能够更…

用大白话解释缓存Redis +MongoDB是什么有什么用怎么用

Redis和MongoDB是什么&#xff1f; Redis&#xff1a;像你家的“小冰箱”&#xff0c;专门存高频使用的食物&#xff08;数据&#xff09;。它是基于内存的键值数据库&#xff0c;读写速度极快&#xff08;每秒超10万次操作&#xff09;。比如你每次打开手机App&#xff0c;用…

自然语言处理:词频-逆文档频率

介绍 大家好&#xff0c;博主又来给大家分享知识了。本来博主计划完成稠密向量表示的内容分享后&#xff0c;就开启自然语言处理中文本表示的讲解。可在整理分享资料的时候&#xff0c;博主发现还有个知识点&#xff0c;必须得单独拎出来好好说道说道。 这就是TF-IDF&#xf…

架构思维:架构的演进之路

文章目录 引言为什么架构思维如此重要架构师的特点软件架构的知识体系如何提升架构思维大型互联网系统架构的演进之路一、大型互联网系统的特点二、系统处理能力提升的两种途径三、大型互联网系统架构演化过程四、总结 引言 在软件开发行业中&#xff0c;有很多技术人可能会问…

DeepSeek-R1-Zero:基于基础模型的强化学习

注&#xff1a;此文章内容均节选自充电了么创始人&#xff0c;CEO兼CTO陈敬雷老师的新书《自然语言处理原理与实战》&#xff08;人工智能科学与技术丛书&#xff09;【陈敬雷编著】【清华大学出版社】 文章目录 DeepSeek大模型技术系列四DeepSeek大模型技术系列四》DeepSeek-…

Metal学习笔记八:纹理

到目前为止&#xff0c;您已经学习了如何使用片段函数和着色器为模型添加颜色和细节。另一种选择是使用图像纹理&#xff0c;您将在本章中学习如何操作。更具体地说&#xff0c;您将了解&#xff1a; • UV 坐标&#xff1a;如何展开网格&#xff0c;以便可以对其应用纹理。 •…

Dify使用和入门

第一步&#xff1a;了解 Dify 在开始之前&#xff0c;先简单了解一下 Dify 是什么&#xff1a; Dify 是一个开源的 LLM 应用开发平台&#xff0c;专注于帮助开发者快速构建生产级的生成式 AI 应用。它支持知识库集成、RAG&#xff08;检索增强生成&#xff09;技术、复杂工作…

threeJS——安装以及三要素

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、安装二、三要素1.场景1.1创建场景1.2向场景添加元素1.3场景属性 2.相机2.1相机特点2.2正交相机2.3空间布局2.4小姐操作 3.渲染器 总结 前言 本章简单介绍前…

毕业项目推荐:基于yolov8/yolo11的野生菌菇检测识别系统(python+卷积神经网络)

文章目录 概要一、整体资源介绍技术要点功能展示&#xff1a;功能1 支持单张图片识别功能2 支持遍历文件夹识别功能3 支持识别视频文件功能4 支持摄像头识别功能5 支持结果文件导出&#xff08;xls格式&#xff09;功能6 支持切换检测到的目标查看 二、数据集三、算法介绍1. YO…

【精华】为什么class在前端开发中不常用?

为什么class在前端开发中不常用&#xff1f; js是一种基于原型的语言。它的对象继承是通过 原型链&#xff08;prototype chain&#xff09;实现的&#xff0c;每个对象都有一个 proto 属性指向它的原型。&#xff08;大多数传统面向对象语言&#xff08;如 Java、C、Python、…

【六祎 - Note】SQL备忘录;DDL,DML,DQL,DCL

SQL备忘录 from to : 点击访问源地址

阿里云物联网获取设备属性api接口:QueryDevicePropertyData

阿里云物联网接口&#xff1a;QueryDevicePropertyData 说明&#xff1a;调用该接口查询指定设备或数字孪生节点&#xff0c;在指定时间段内&#xff0c;单个属性的数据 比如提取上传到物联网的温度数据 api文档&#xff1a;QueryDevicePropertyData_物联网平台_API文档-阿里…

需求和开发模型

文章目录 什么是需求&#xff1f;用户需求软件需求用户需求和软件需求的不同 开发模型什么是“模型”&#xff1f;软件的生命周期常见的开发模型瀑布模型&#xff08;Waterfall Model&#xff09;螺旋模型增量模型、迭代模型敏捷模型 测试模型V 模型W 模型&#xff08;双 V 模型…

21-发糖果

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需要按照以下要求&#xff0c;给这些孩子分发糖果&#xff1a; 每个孩子至少分配到 1 个糖果。 相邻两个孩子评分更高的孩子会获得更多的糖果。 请你给每个孩子分发糖果&#xff0c;计算并返回需要准备的 最…

sql深入学习

文章目录 前言知识学习注释的两种形式字符型注入万能密码 布尔盲注报错注入堆叠注入时间盲注二次注入 小技巧 前言 这次学习建立在对数据库有基本的认识&#xff0c;了解基础的增删改查语句&#xff0c;数字型注入和字符型注入的基础上&#xff0c;进一步深入学习知识&#xf…