于2022年5月30日2022年5月30日由Sukuna发布
本实验我只完成2.1和2.2 2.3不会写,算了.
左边那个就是directory page,它有一个参数叫做global depth,1<<global depth为directory的大小。它存储了指向各个bucket page的指针。bucket page里面存储的则是实际的数据(在本实验中是std::pair类型的键值),每个bucket都有一个自己的local depth。
插入一个键值的过程是:先把key代入hash函数计算得到一个中间结果,取这个中间结果的最后global depth位(这就是global depth mask的作用,就是 数据&global mask(具体请看下面的例子) ),得到一个数组下标,bucket的page id就在这个下标里。根据page id调入bucket,然后把这个键值插入到bucket里面。
提示:完成两种页的设计,一个是桶一个是目录.
修改src/include/storage/page/hash_table_bucket_page.h、src/storage/page/hash_table_bucket_page.cpp、src/include/storage/page/hash_table_directory_page.h和src/storage/page/hash_table_directory_page.cpp
我们先来看看头文件(头文件不需要修改)
page_id_t page_id_; // 页的id
lsn_t lsn_;
uint32_t global_depth_{0};//全局深度,也就是说下面的数组有几个元素.2^i的关系,所有局部深度不能比全局深度大
uint8_t local_depths_[DIRECTORY_ARRAY_SIZE];//每一个页的局部深度,这个类似三级页表.第一级的页表局部深度就是2.局部深度代表你作为目录项离叶结点(bucket有多远)
page_id_t bucket_page_ids_[DIRECTORY_ARRAY_SIZE];//下一级的页号
1、GetGlobalDepthMask函数
这个函数要求我们返回一个掩码,掩码的概念我们在计算机网络中学过,对于网络地址,网络地址有几位,就有几个1,在这里,全局深度为几就有几个1.
uint32_t HashTableDirectoryPage::GetGlobalDepthMask() {
uint32_t global_depth_mask = ((1 << GetGlobalDepth()) - 1);
return global_depth_mask;
}
2、IncrGlobalDepth函数
这需要我们增长全局深度,根据CMU的PPT里面介绍,我们不仅要增加变量的值(value),还需要进行local_depths和bucket的初始化.初始化的方法就是1:1复制,因为这种增长是成倍增长的,对于1~N的节点,给N+1~2N赋值的时候直接完全复制就可以了.
void HashTableDirectoryPage::IncrGlobalDepth() {
int original_depth = 1 << global_depth_;
int new_index = original_depth;
for(int i = 0;i < original_depth;i++){
local_depths_[new_index] = local_depths_[i];
bucket_page_ids_[new_index] = bucket_page_ids_[i];
new_index++;
}
global_depth_++;
}
3、CanShrink函数
能否缩小全局变量,那就判断有没有一个局部深度比全局深度深.
bool HashTableDirectoryPage::CanShrink() {
int original_depth = 1 << global_depth_;
for(int i = 0;i < original_depth;i++){
if(local_depths_[i] >= global_depth_) return false;
}
return true;
}
其他的函数都是set和get,相信各位都可以做出来.
首先我们来看头文件:
// For more on BUCKET_ARRAY_SIZE see storage/page/hash_table_page_defs.h
char occupied_[(BUCKET_ARRAY_SIZE - 1) / 8 + 1];
// 0 if tombstone/brand new (never occupied), 1 otherwise.
char readable_[(BUCKET_ARRAY_SIZE - 1) / 8 + 1];
MappingType array_[0];
首先就是MappingType,保存了一系列键值对,还有一个bitmap,来表示占用与存储有效信息.
1、可读和占用的判断和设置
这个bitmap是一个char[]的数组,首先我们要判断出它们是在数组的哪一个元素,然后从这个元素中进行恰当的位运算.int A = id/8(哪一个元素) int B = id % 8(这个元素的哪一位),这个就很像xv6文件系统中的bitmap.
然后就是设置,设置的思路其实也是一样的,找到位置和具体哪一位,然后置0或者是1,大体上类似于get和set,但是有一点不一样的就是set和get的粒度是具体到bit的,这个需要我们使用正确且高效的位运算.
template <typename KeyType, typename ValueType, typename KeyComparator>
bool HASH_TABLE_BUCKET_TYPE::IsOccupied(uint32_t bucket_idx) const {
size_t c = occupied_[bucket_idx / 8];
c = c & (1 << (bucket_idx % 8));
return c != 0;
}
template <typename KeyType, typename ValueType, typename KeyComparator>
void HASH_TABLE_BUCKET_TYPE::SetOccupied(uint32_t bucket_idx) {
size_t c = occupied_[bucket_idx / 8];
c = c | (1 << (bucket_idx % 8));
occupied_[bucket_idx / 8] = static_cast<char>(c);
}
template <typename KeyType, typename ValueType, typename KeyComparator>
bool HASH_TABLE_BUCKET_TYPE::IsReadable(uint32_t bucket_idx) const {
size_t c = readable_[bucket_idx / 8];
c = c & (1 << (bucket_idx % 8));
return c != 0;
}
template <typename KeyType, typename ValueType, typename KeyComparator>
void HASH_TABLE_BUCKET_TYPE::SetReadable(uint32_t bucket_idx) {
size_t c = readable_[bucket_idx / 8];
c = c | (1 << (bucket_idx % 8));
readable_[bucket_idx / 8] = static_cast<char>(c);
}
还记得我们在大一做的C语言位运算的题目嘛?这下用上了.
2、Remove系列函数
首先基础的函数就是RemoveAt函数,RemoveAt函数其实类似于SetReadable(i,0).接着就是Remove,基本思路就是比较,找到匹配的值然后删除掉即可.
其实可以思考一下,为什么Key类型的需要使用比较器,但是Value的值不需要比较器,为什么设置的模版只有3个参数.
template <typename KeyType, typename ValueType, typename KeyComparator>
bool HASH_TABLE_BUCKET_TYPE::Remove(KeyType key, ValueType value, KeyComparator cmp) {
for(size_t i = 0; i < BUCKET_ARRAY_SIZE ; i++){
if(IsReadable(i)){
if(cmp(key,array_[i].first) == 0 && value == array_[i].second){
RemoveAt(i);
return true;
}
}
}
return false;
}
template <typename KeyType, typename ValueType, typename KeyComparator>
void HASH_TABLE_BUCKET_TYPE::RemoveAt(uint32_t bucket_idx) {
size_t c = readable_[bucket_idx / 8];
c = c & (~(1 << (bucket_idx % 8)));
readable_[bucket_idx / 8] = static_cast<char>(c);
}
3、是否全满,是否全空.
这类思路都是一样的,对于bitmap的全部数据,进行遍历.只不过这一次遍历的粒度不需要下降到位的级别,只需要看看,char[]里面的值是不是全0或者是全1,这下我们可以使用mask=0xFF来进行与或运算.
这时候有小伙伴会问,万一桶的元素不为8的整数,剩下的5个怎么比较呢?我们可以把最后一个char类型的元素取出来,然后可以利用10000000取高位来进行取值.
template <typename KeyType, typename ValueType, typename KeyComparator>
bool HASH_TABLE_BUCKET_TYPE::IsEmpty() {
u_int8_t mask = 0;
size_t times = BUCKET_ARRAY_SIZE / 8;
for (size_t i = 0; i < times; i++) {
char c = readable_[i];
uint8_t ic = static_cast<uint8_t>(c);
if ((ic | mask) != mask) {
return false;
}
}
//剩下的
size_t remain = BUCKET_ARRAY_SIZE % 8;
if (remain > 0) {
char c = readable_[times];
uint8_t ic = static_cast<uint8_t>(c);
for (size_t i = 0; i < remain; i++) {
//每次取1位
if ((ic & 1) != 1) {
return false;
}
//迭代
ic = ic >> 1;
}
}
return true;
}
0、概要
这一个部分我们需要结合之前的BufferPoolManager来完善哈希表的增删改查的操作.
初始情况下global depth为0,directory大小为1<<0=1,因此只有一个bucket,此bucket的local depth为0。这时插入键值,取哈希函数中间结果的后0位,得到的directory下标总是为0,所有的元素全进入这个初始bucket。
如果一个桶满了,就需要分裂成两个桶,分裂的伪代码如下:
split--伪代码
bucket_page = Fetch(bucket_idx);
if(bucket为空){
构造分裂映像
split_page = NewPage(&split_idx);
if(全局深度 == 局部深度){
需要增长目录;
产生的许多空索引需要进行填入page_id信息以及局部深度信息
指向相同的page_id的索引之间相差了1<<(全局深度);
修改目录的信息以及设置分裂后的桶的相关信息
}else{
仅需要分裂桶;
先增加局部深度,接着确定兄弟索引的个数;
修改分裂后兄弟索引的指向;
}
分配旧桶里面的数据,一般会均分到两个桶里面
}
执行插入即可
下面用图片介绍一下分裂的内容.
假设global_depth=1,就是两个桶:(目录桶对应的是key的最后一位)
假设桶b满了,这个时候local_depth=1=global_depth.这个时候目录页需要扩充.
桶b的内容(1),平均分成01和11.桶a的内容被00和10指着
这个时候local_depth变成了2,原来的a桶depth还是1.
如果a满了,也是一样分裂,但是local_depth=1<2,目录页是不用分裂的.
插入讲完了,现在我们需要讲一讲合并.
在删除bucket_page 000 后,如果这里的000空了,空了就尝试merge。参考官方做法就是:
(1)两哈希桶均为空桶;
(2)目录项及其目标目录项(一个目录项的目标目录项可由其低第j
位反转得到)的局部深度相同且不为0。
满足上述两个条件后就可以进行合并了。
1、查(getValue)
了解一下函数的做法首先第一步获取目录页,然后根据目录页调用KeyToPageId获取具体位于哪一个桶里面.然后调用getdata和getvalue函数进行查找.查找的返回值是有没有查找到相关信息,查找的结果返回在result变量里面.(注意page只是一个指针,你还是需要通过GetData来获得指针里面具体指向的内容),注意在最后需要调用unpin函数取消对页的pin.
template <typename KeyType, typename ValueType, typename KeyComparator>
bool HASH_TABLE_TYPE::GetValue(Transaction *transaction, const KeyType &key, std::vector<ValueType> *result) {
table_latch_.RLock();
HashTableDirectoryPage *dir_page = FetchDirectoryPage();
page_id_t bucket_page_id = KeyToPageId(key,dir_page);
Page* page = FetchBucketPage(bucket_page_id);
page->RLatch();
HASH_TABLE_BUCKET_TYPE *bucket_page = reinterpret_cast<HASH_TABLE_BUCKET_TYPE*>(page->GetData());
bool res = bucket_page->GetValue(key,comparator_,result);
page->RUnlatch();
buffer_pool_manager_->UnpinPage(directory_page_id_,false);
buffer_pool_manager_->UnpinPage(bucket_page_id,false);
table_latch_.RUnlock();
return res;
}
2、增(Insert)
这是基本的插入操作.基本的操作还是和之前一样,获取目录,根据目录来获取桶,如果这个桶没有满的话,那么就可以直接调用桶页的相关操作,然后Unpin,返回即可.但是万一桶是满的怎么办,请往下看.
template <typename KeyType, typename ValueType, typename KeyComparator>
bool HASH_TABLE_TYPE::Insert(Transaction *transaction, const KeyType &key, const ValueType &value) {
table_latch_.WLock();
HashTableDirectoryPage* dir_page = FetchDirectoryPage();
page_id_t bucket_page_id = KeyToPageId(key,dir_page);
Page* page = FetchBucketPage(bucket_page_id);
page->WLatch();
HASH_TABLE_BUCKET_TYPE* bucket_page = reinterpret_cast<HASH_TABLE_BUCKET_TYPE*>(page->GetData());
bool flag = bucket_page->IsFull();
if(!flag){
bool ret = bucket_page->Insert(key,value,comparator_);
page->WUnlatch();
buffer_pool_manager_->UnpinPage(bucket_page_id,true);
buffer_pool_manager_->UnpinPage(dir_page->GetPageId(),false);
table_latch_.WUnlock();
return ret;
}
page->WUnlatch();
buffer_pool_manager_->UnpinPage(bucket_page_id,false);
buffer_pool_manager_->UnpinPage(dir_page->GetPageId(),false);
table_latch_.WUnlock();
return SplitInsert(transaction,key,value);
}
3、可扩展的增(Insert)
对于一个比较大而且满的页,一个比较直观的想法就是分裂成两个页,这两个页都可以继续执行插入操作.这个操作比较复杂,在这里我就只用分步的形式来进行描述.
4、删(Remove)
删除一个键值对.
首先还是基础操作,获取桶页,然后调用remove函数就可以了.
template <typename KeyType, typename ValueType, typename KeyComparator>
bool HASH_TABLE_TYPE::Remove(Transaction *transaction, const KeyType &key, const ValueType &value) {
table_latch_.RLock();
HashTableDirectoryPage* dir_page = FetchDirectoryPage();
uint32_t bucket_page_id = KeyToPageId(key,dir_page);
Page* page = FetchBucketPage(bucket_page_id);
page->WLatch();
HASH_TABLE_BUCKET_TYPE* bucket_page = reinterpret_cast<HASH_TABLE_BUCKET_TYPE*>(page->GetData()) ;
bool ret = bucket_page->Remove(key,value,comparator_);
page->WUnlatch();
buffer_pool_manager_->UnpinPage(dir_page->GetPageId(),false);
buffer_pool_manager_->UnpinPage(bucket_page_id,true);
table_latch_.RUnlock();
if(bucket_page->IsEmpty()){
Merge(transaction,key,value);
return ret;
}
return ret;
}
5、合并(Merge)
删除了一个元组之后有可能产生空页,空页是没有意义的,要把空页和相对应的页做一个merge操作,合并在一起.