EXTENDIBLE HASH INDEX
概述
-
第二个编程项目是为BusTub DBMS实现一个以磁盘为媒介的哈希表。
-
需要使用可扩展的散列哈希方案实现一个哈希表。
-
这个索引包括一个目录页,它包含指向桶状页的指针。该表将通过你的缓冲池从Project#1访问页面。该表包含一个目录页,它存储了该表和桶的所有元数据。你的哈希表需要支持满/空桶的桶分割/合并,以及全局深度必须改变时的目录扩展/收缩。
任务
技术坑点
这里先讲一下自己做实验2时所遇到的技术坑点,具体代码就不公开了,希望小伙伴萌避雷,
-
随着项目的逐渐变大,学会把握整个项目框架很重要,与之相关的头文件(.h)啥的不用多说了,其次是不相关的头文件,它里面包含了其他类的定义与一些宏定义,通过将它们include后,便可以将整个项目分为多个板块,便于开发与学习,为此把不相干的头文件也需要知晓将它引入的用处。
-
面对每个类定义的庞大的成员函数数量,要学会取其精华,简而言之就是要合理取用,需要的函数就实现,不需要的可以不管,删除也行(我就删了)
-
对于类中私有成员变量的定义一定要摸清楚每个的含义,千万在实现类的时候忘掉他们的作用与用途!!!(曾经的我就是忘掉了comparator_)
-
对于强制转换技术: reinterpret_cast 的窥探,说白了就是static_cast的升级版,能够重新从底层方面解释一个类,但是要保证两个类的大小相同,否则会出现问题
-
键和值都相同,插入才会错误,如果只是键相同也可以进行插入。
-
位操作要捡起来了。(假设有一个名为array的char类型的数组)
-
定位到目标位:
int numIndex = i / sizeof(char);
int bitIndex = i / sizeof(char);
通过这两个数就可以访问目标了。
-
获取第i个bit:
int s = (array[numIndex] >> bitIndex) & 1;
-
把第i个bit置为1:
array[numIndex] = array[numIndex] | (1<<bitIndex);
-
把第i个bit置为0:
array[numIndex] = array[numIndex] & (~(1<<bitIndex));
-
-
在实现对bitmap中的1进行计数时,在网上了解到popcount,太牛逼了,有兴趣的可以看看Hacker‘s Delight第五章。
-
IsOccupied()没啥用,几乎没有涉及到,应该后面的项目才会用,只需要按照要求来实现即可。
-
谨记遇到死锁一定要GDB,来看哪个函数在获取锁,一直消耗CPU。
TASK #1 - PAGE LAYOUTS
为了支持在页面之上读/写哈希表桶,你将实现两个Page类来存储哈希表的数据。这是要教你如何从BufferPoolManager中分配内存作为页。
HASH TABLE DIRECTORY PAGE
这个类保存了哈希表的所有元数据。它被划分为如下表所示的字段。
Variable Name | Size | Description |
---|---|---|
page_id_ | 4 bytes | Self Page Id |
lsn_ | 4 bytes | Log sequence number (Used in Project 4) |
global_depth_ | 4 bytes | Global depth of the directory |
local_depths_ | 512 bytes | arrayay of local depths for each bucket (uint8) |
bucket_page_ids_ | 2048 bytes | arrayay of bucket page_id_t |
bucket_page_ids_
数组将桶的ID映射到page_id_t
的ID。bucket_page_ids_
中的第i个元素是第i个桶的page_id
(也就是说每个元素是一个哈希桶的页号)。
HASH TABLE BUCKET PAGE
哈希表Bucket Page持有三个数组。
occupied_
: 如果arrayay_
的第i个索引曾经被占用,则occupied_
的第i位为1。readable_
: 如果arrayay_
的第i个索引持有一个可读的值,则readable_
的第i位为1。arrayay_
: 保存键值对的数组。
每个哈希表的Directory/Bucket页都对应于由缓冲池获取的内存页的内容(即,字节数组data_)。每当你试图读取或写入一个页面时,你需要首先使用其唯一的page_id从缓冲池中获取该页面,然后reinterpret_cast
一个目录或一个桶的页面,并在任何写入或读取操作后取消该页面。
必须实现的唯一函数如下。
Directory Page: - GetGlobalDepth - IncrGlobalDepth - SetLocalDepth - SetBucketPageId - GetBucketPageId
Bucket Page: - Insert - Remove - IsOccupied - IsReadable - KeyAt - ValueAt
下面这些函数只是基本保证(用于通过本地测试):
Directory Page 部分代码
-
auto HashTableDirectoryPage::GetLocalDepth(uint32_t bucket_idx) -> uint32_t { return local_depths_[bucket_idx]; } void HashTableDirectoryPage::IncrGlobalDepth() { global_depth_++; } void HashTableDirectoryPage::SetLocalDepth(uint32_t bucket_idx, uint8_t local_depth) { local_depths_[bucket_idx] = local_depth; } auto HashTableDirectoryPage::GetBucketPageId(uint32_t bucket_idx) -> page_id_t { return bucket_page_ids_[bucket_idx]; } void HashTableDirectoryPage::SetBucketPageId(uint32_t bucket_idx, page_id_t bucket_page_id) { bucket_page_ids_[bucket_idx] = bucket_page_id; }
后面需要实现的函数:
-
auto HashTableDirectoryPage::CanShrink() -> bool { uint32_t dir_size = 1 << global_depth_; bool flag = true; for (uint32_t i = 0; i < dir_size; i++) { auto dep = GetLocalDepth(i); if (global_depth_ == dep) { flag = false; } } return flag; } auto HashTableDirectoryPage::GetLocalDepth(uint32_t bucket_idx) -> uint32_t { return local_depths_[bucket_idx]; } uint32_t HashTableDirectoryPage::GetSplitImageIndex(uint32_t bucket_idx) { return bucket_idx ^ (1 << (local_depths_[bucket_idx])); } void HashTableDirectoryPage::IncrLocalDepth(uint32_t bucket_idx) { local_depths_[bucket_idx]++; assert(local_depths_[bucket_idx] <= global_depth_); } void HashTableDirectoryPage::DecrLocalDepth(uint32_t bucket_idx) { assert(local_depths_[bucket_idx] > 0); local_depths_[bucket_idx]--; } auto HashTableDirectoryPage::GetGlobalDepthMask() -> uint32_t { return (1 << global_depth_) - 1; } auto HashTableDirectoryPage::GetLocalDepthMask(uint32_t bucket_idx) -> uint32_t { return (1 << local_depths_[bucket_idx]) - 1; }
Bucket Page 的部分代码
-
template <typename KeyType, typename ValueType, typename KeyComparator> auto HASH_TABLE_BUCKET_TYPE::GetValue(KeyType key, KeyComparator cmp, std::vector<ValueType> *result) -> bool { uint32_t arrayay_size = BUCKET_arrayAY_SIZE; for (uint32_t i = 0; i < arrayay_size; i++) { if (IsReadable(i)) { if (cmp(key, arrayay_[i].first) == 0) { result->push_back(arrayay_[i].second); } } } return (!result->empty()); } template <typename KeyType, typename ValueType, typename KeyComparator> auto HASH_TABLE_BUCKET_TYPE::Insert(KeyType key, ValueType value, KeyComparator cmp) -> bool { // arrayay_ size condition uint32_t arrayay_size = BUCKET_arrayAY_SIZE; uint32_t pos = arrayay_size; for (uint32_t i = 0; i < arrayay_size; i++) { if (IsReadable(i)) { if (cmp(key, arrayay_[i].first) == 0 && (value == arrayay_[i].second)) { return false; } } else { if (pos == arrayay_size) { pos = i; } if (!IsOccupied(i)) { break; } } } if (pos == arrayay_size) { return false; } arrayay_[pos].first = key; arrayay_[pos].second = value; SetReadable(pos); SetOccupied(pos); return true; } template <typename KeyType, typename ValueType, typename KeyComparator> auto HASH_TABLE_BUCKET_TYPE::Remove(KeyType key, ValueType value, KeyComparator cmp) -> bool { uint32_t i = 0; uint32_t j = NumReadable(); uint32_t arrayay_size = BUCKET_arrayAY_SIZE; uint32_t cnt = 0; for (; i < arrayay_size; i++) { if (IsReadable(i)) { if (cmp(key, arrayay_[i].first) == 0 && (value == arrayay_[i].second)) { break; } cnt++; } if (cnt >= j) { return false; } } Clearrayeadable(i); SetOccupied(i); return true; } template <typename KeyType, typename ValueType, typename KeyComparator> auto HASH_TABLE_BUCKET_TYPE::NumReadable() -> uint32_t { uint32_t j = (BUCKET_arrayAY_SIZE - 1) / 8 + 1; uint32_t cnt = 0; for (uint32_t i = 0; i < j; i++) { int n = readable_[i]; while (n != 0) { cnt++; n &= (n - 1); } } return cnt; }
TASK #2 - HASH TABLE IMPLEMENTATION
这里占了整个项目的80%,只能说代码量有点大,这部分主要内容是基于page layouts 实现可拓展哈希表。需要掌握其原理实现。
在ExtendibleHashTable的构造函数中,要用NewPage先创建directory page和第1个bucket page,创建完之后也要记得Unpin,否则会凭空多出一次pin count。
初始情况下global depth为0,directory大小为1<<0=1,因此只有一个bucket,此bucket的local depth为0。这时插入键值,取哈希函数中间结果的后0位,得到的directory下标总是为0,所有的元素全进入这个初始bucket。
现在需要分裂了,现在显然global_depth == local_depth,首先把directory扩大一倍,新项依次指向旧项,global depth加1:
split--伪代码
bucket_page = Fetch(bucket_idx);
if(bucket为空){
构造分裂映像
split_page = NewPage(&split_idx);
if(全局深度 == 局部深度){
需要增长目录;
产生的许多空索引需要进行填入page_id信息以及局部深度信息
指向相同的page_id的索引之间相差了1<<(全局深度);
修改目录的信息以及设置分裂后的桶的相关信息
}else{
仅需要分裂桶;
先增加局部深度,接着确定兄弟索引的个数;
修改分裂后兄弟索引的指向;
}
分配旧桶里面的数据,一般会均分到两个桶里面
}
执行插入即可
然后是重新分配页a中的键值。对于a中的每一个键值,使用KeyToPageId来确定它进入a还是b。因为Hash函数可以把key均匀的映射到它的值域之中,所以a和b各能分配到大约一半的键值。最后要把加1之后的local depth赋给a和b。
现在假设b满了,global和local都为1,把directory扩容一倍:
计算split_image_idx = 01 ^ (1<<1) = 11。(如果从11方向插入键值时检测到b满了,就会得到split image为01,但是结果是一样的)创建新页c,分配键值(KeyToPageId),local depth加1:
现在b满了,扩大directory:
分析一样,可以得到:
这时000,010,100,110都指向a。
现在向a插入数据,当从000方向插入一条键值时,a又满了。这个时候global和local不等,显然可以在directory内部完成分裂。计算可得a的split_image_idx是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:
** 插入操作到此就结束了,根据伪代码,应该不难实现,接下来是删除与合并操作**
在删除bucket_page 000 后,如果这里的000空了,空了就尝试merge。参考官方做法就是:
(1)两哈希桶均为空桶;
(2)目录项及其目标目录项(一个目录项的目标目录项可由其低第j
位反转得到)的局部深度相同且不为0。
满足上述两个条件后就可以进行合并了。
我的做法是,判断当前的page和merge_page 的局部深度是否为全局深度(一定大于零),使用bucket index ^ 1<<(global_depth-1)来指定它的merge对象,如果二者local depth相同且等于全局深度,就从二者中选一个较小者填上merge对象的page id,之后再将两个页面的局部深度减一。当所有bucket的local depth都小于global depth的时候就可以把global depth减去1,directory缩减一半了。
TASK #3 - CONCURRENCY CONTROL
这一部分属于多线程的并发控制,为了保证在桶分裂和合并的时候,全局数据的完整性。
在这里锁的争夺太严重了,如下图所示
锁的优化
- 减少锁持有时间
- 减小锁粒度
- 锁分离
- 锁粗化
- 锁消除
这里采用的是锁分离技术,将读锁和写锁分开,减少争夺。在桶的分裂,合并操作时加全局写锁,其余时候采用乐观锁。
典型例子:
/*****************************************************************************
* INSERTION
*****************************************************************************/
template <typename KeyType, typename ValueType, typename KeyComparator>
auto HASH_TABLE_TYPE::Insert(Transaction *transaction, const KeyType &key, const ValueType &value) -> bool {
auto directory_page = FetchDirectoryPage();
//----------------------
table_latch_.RLock();//乐观锁,假设不会更改目录
//----------------------
uint32_t bucket_idx = KeyToDirectoryIndex(key, directory_page);
page_id_t bucket_page_id = directory_page->GetBucketPageId(bucket_idx);
auto bucket_page = FetchBucketPage(bucket_page_id);
Page *pg = reinterpret_cast<Page *>(bucket_page);
pg->WLatch();
if (bucket_page->IsFull()) {
// target bucket full
pg->WUnlatch();
table_latch_.RUnlock();
assert(buffer_pool_manager_->UnpinPage(bucket_page_id, true));
assert(buffer_pool_manager_->UnpinPage(directory_page_id_, true));
return SplitInsert(transaction, key, value);
}
auto it = bucket_page->Insert(key, value, comparator_);
pg->WUnlatch();
table_latch_.RUnlock();
assert(buffer_pool_manager_->UnpinPage(bucket_page_id, true));
assert(buffer_pool_manager_->UnpinPage(directory_page_id_, true));
return it;
}
实验成绩
优化了三次结束啦,终于跟上大部队了,准备第三个项目了,转🐎人加油!