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.pswp.cn/diannao/72063.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/72063.shtml
英文地址,請注明出處:http://en.pswp.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…