15-445(2021) PROJECT #2 - EXTENDIBLE HASH INDEX


TASK #1 - PAGE LAYOUTS

先看一下這張圖片,留一個對extendible hashing的印象:

左邊那個就是directory page,它有一個參數叫做global depth,1<<global depth為directory的大小。它存儲了指向各個bucket page的指針。bucket page里面存儲的則是實際的數據(在本實驗中是std::pair類型的鍵值),每個bucket都有一個自己的local depth。

插入一個鍵值的過程是:先把key代入hash函數計算得到一個中間結果,取這個中間結果的最后global depth位(這就是global depth mask的作用),得到一個數組下標,bucket的page id就在這個下標里。根據page id調入bucket,然后把這個鍵值插入到bucket里面。

1.HASH TABLE DIRECTORY PAGE

如圖是directory page的結構。注意,這個類是單獨占用buffer pool的一個page的:

里面存儲了這個類的page、global depth、以及指向各個bucket的指針以及它們的local depth。類里定義了一大堆方法等着我們實現,好在都有注釋,先照着做就可以了。注意看一下注釋里global_depth_mask和local_depth_mask的說明。

2.HASH TABLE BUCKET PAGE

這個類很奇怪,為了保證“Memory Safety”一上來就把自己的構造函數刪掉了。看一下它的成員:

這個array_[0]是什么?在MSVC里,這么寫是直接報錯的,但是gcc支持這種寫法,文檔里稱:

"Declaring zero-length arrays is allowed in GNU C as an extension. A zero-length array can be useful as the last element of a structure that is really a header for a variable-length object"。

這里就很明顯了:一個HashTableBucketPage類占用一個整頁,這個頁的開頭是兩個bitmap數組,存儲每個鍵值的位信息,之后的就全都歸鍵值了。訪問第i個鍵值的時候,可以直接用array_[i]的形式,因為array_[i]完全等價於*(array_ + i)。

每個鍵值對應兩個位,array_[i]是不是空就看它的readable_位是不是0。occupied_的作用是提前結束遍歷(可以看一下PrintBucket()這個成員函數)。在插入的時候occupied_位和readable_位要設為1,刪除的時候就只需要把readable_置為0了。

插入、刪除、讀取的時候沒有什么好辦法,直接遍歷搜索出所需位置就可以了。有一點需要注意,本實驗是要求支持both unique and non-unique keys。key是可以重復的。所以,需要key和value同時相等才可以說兩個鍵值相同。再有就是需要注意一下bitmap的操作。

假設有一個bitmap名為arr,是char類型的數組:

  1. 定位到目標位:

    通過這兩個數就可以訪問目標了。

  2. 獲取第i個bit:

  3. 把第i個bit置為1:

  4. 把第i個bit置為0:

雖然課程網站上沒有提到,但是test/container目錄下有一個hash_page_table_test.cpp,專門用來測試這兩個類的實現,寫完了之后可以跑一下這個測試。

TASK #2 - HASH TABLE IMPLEMENTATION

1.什么是extendible hashing

這個實驗還是比較折磨人的,我做的時候感覺最難的地方不是代碼實現,而是在於不知道什么是extendible hashing,因為這門課的Lecture和《數據庫系統概念》都只是給了一些模糊不清的圖示,原理什么的都不存在的,更別說偽代碼了。我在查資料的過程中發現這個講義還是比較靠譜的,一定要先看一下里面的insert操作偽代碼。

www.mathcs.emory.edu/~cheung/Courses/554/Syllabus/3-index/extensible-hashing-new.html

初始情況下global depth為0,directory大小為1<<0=1,因此只有一個bucket,此bucket的local depth為0。這時插入鍵值,取哈希函數中間結果的后0位,得到的directory下標總是為0,所有的元素全進入這個初始bucket。

現在需要分裂了,現在顯然global_depth == local_depth,我們要“Double the logical hash table”。把directory擴大一倍,新項依次指向舊項,global depth加1:

然后開始“Re-hash bucket[j] disk block (physical bucket) into 2 block (physical buckets) using i'+1 bits”。首先需要確定它的split image放在哪個下標。把頁a的下標的最高位取反即可得到split image下標。取反某一個比特可以使用異或的方法:

split image index = bucket index ^ (1<<local_depth)

這里split image index顯然是0^(1<<0)=1,所以新創建一個數據頁,把它的page id覆蓋到bucket_page_id_[1]中。

然后是重新分配頁a中的鍵值。對於a中的每一個鍵值,使用Hash(key) & local_depth_mask來確定它進入a還是b。因為Hash函數可以把key均勻的映射到它的值域之中,所以a和b各能分配到大約一半的鍵值。最后要“Label each block (physical buckets) with i'+1”,把加1之后的local depth賦給a和b。

現在假設b滿了,global和local都為1,把directory擴容一倍:

計算split image index = 01 ^ (1<<1) = 11。(如果從11方向插入鍵值時檢測到b滿了,就會得到split image為01,但是結果是一樣的)創建新頁c,分配鍵值,local depth加1:

現在b滿了,擴大directory:

同上,得到頁d:

這時000,010,100,110都指向a。這里就體現了local depth的作用“bits in RandonNumGen(key) used to find the physical bucket”。訪問a的時候,只看哈希值的后1位,只要哈希值的最后一位是0,就會訪問到a。

現在向a插入數據,當從000方向插入一條鍵值時,a又滿了。這個時候global和local不等,顯然可以在directory內部完成分裂。計算可得a的split image是010,執行rehash操作,然后把a和split image的local depth各加1。

但是100和110也指向a,怎么辦?這種情況的做法是,從000開始向上下兩個方向遍歷,每隔1<<local depth=4,就在現在這格里填上a的page id。split image也是同理,從010開始上下遍歷,每隔1<<local depth=4就填上它的page id:

2.實現

(1) Insert

實現直接參照前面偽代碼即可。這里就總結一下幾個坑點:

  1. 使用FetchPage之后要記得UnpinPage。insert操作會先載入directory page,然后載入需要的bucket page,在函數的任何地方return之前都要先把這兩個頁還回去。在ExtendibleHashTable的構造函數中,要用NewPage先創建directory page和第1個bucket page,創建完之后也要記得Unpin,否則會憑空多出一次pin count。
  2. 建議使用assert操作來檢測所有的Unpin,這樣可以快速定位錯誤。
  3. 在rehash的時候,我一開始的做法是,向buffer pool再申請一個臨時頁,把分配到原bucket的鍵值暫存到臨時頁里面,然后清空bucket,把臨時頁里的內容再插回bucket,最后刪除臨時頁。這么做讓我調試了兩天,鍵值稍微一多就刪除錯誤。這里建議是,不需要浪費一個buffer pool的位置,聲明一個STL容器,把分配到原bucket的鍵值放到這個容器里就可以了。因為待分裂的bucket的大小是固定的,所以STL容器的大小不會超過BUCKET_ARRAY_SIZE,空間復雜度O(1),還是可以接受的。

(2) Remove

這里刪除鍵值之后檢測一下此bucket是否為空,空了就嘗試merge。我的做法是,不看local depth,使用bucket index ^ 1<<(global_depth-1)來指定它的merge對象,如果二者local depth相同,直接把bucket index填上merge對象的page id即可。當所有bucket的local depth都小於global depth的時候就可以把global depth減去1,directory縮減一半了。

TASK #3 - CONCURRENCY CONTROL

這個實驗里設置了讀寫鎖,最簡單的方法就是把getvalue全局加讀鎖,insert和remove加寫鎖。我不太懂多線程編程,所以就是這么寫的,能滿分通過gradescope測試,但是耗時70多秒。看了一下leaderboard前排能做到30秒左右,竟然還看到了一個嘉心糖哈哈,甚至有神人做到0.2s,果然我還是太菜了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM