Project 2:EXTENDIBLE HASH INDEX

EXTENDIBLE HASH INDEX

概述

  • 第二个编程项目是为BusTub DBMS实现一个以磁盘为媒介的哈希表。

  • 需要使用可扩展的散列哈希方案实现一个哈希表。

  • 这个索引包括一个目录页,它包含指向桶状页的指针。该表将通过你的缓冲池从Project#1访问页面。该表包含一个目录页,它存储了该表和桶的所有元数据。你的哈希表需要支持满/空桶的桶分割/合并,以及全局深度必须改变时的目录扩展/收缩。

任务

技术坑点

这里先讲一下自己做实验2时所遇到的技术坑点,具体代码就不公开了,希望小伙伴萌避雷,

  1. 随着项目的逐渐变大,学会把握整个项目框架很重要,与之相关的头文件(.h)啥的不用多说了,其次是不相关的头文件,它里面包含了其他类的定义与一些宏定义,通过将它们include后,便可以将整个项目分为多个板块,便于开发与学习,为此把不相干的头文件也需要知晓将它引入的用处。

  2. 面对每个类定义的庞大的成员函数数量,要学会取其精华,简而言之就是要合理取用,需要的函数就实现,不需要的可以不管,删除也行(我就删了)

  3. 对于类中私有成员变量的定义一定要摸清楚每个的含义,千万在实现类的时候忘掉他们的作用与用途!!!(曾经的我就是忘掉了comparator_)

  4. 对于强制转换技术: reinterpret_cast 的窥探,说白了就是static_cast的升级版,能够重新从底层方面解释一个类,但是要保证两个类的大小相同,否则会出现问题

  5. 键和值都相同,插入才会错误,如果只是键相同也可以进行插入。

  6. 位操作要捡起来了。(假设有一个名为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));
      
  7. 在实现对bitmap中的1进行计数时,在网上了解到popcount,太牛逼了,有兴趣的可以看看Hacker‘s Delight第五章。

  8. IsOccupied()没啥用,几乎没有涉及到,应该后面的项目才会用,只需要按照要求来实现即可。

  9. 谨记遇到死锁一定要GDB,来看哪个函数在获取锁,一直消耗CPU。

TASK #1 - PAGE LAYOUTS

为了支持在页面之上读/写哈希表桶,你将实现两个Page类来存储哈希表的数据。这是要教你如何从BufferPoolManager中分配内存作为页。

HASH TABLE DIRECTORY PAGE

这个类保存了哈希表的所有元数据。它被划分为如下表所示的字段。

Variable NameSizeDescription
page_id_4 bytesSelf Page Id
lsn_4 bytesLog sequence number (Used in Project 4)
global_depth_4 bytesGlobal depth of the directory
local_depths_512 bytesarrayay of local depths for each bucket (uint8)
bucket_page_ids_2048 bytesarrayay 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持有三个数组。

  1. occupied_ : 如果arrayay_的第i个索引曾经被占用,则occupied_的第i位为1。
  2. readable_ : 如果arrayay_的第i个索引持有一个可读的值,则readable_的第i位为1。
  3. 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;
}

实验成绩

在这里插入图片描述
优化了三次结束啦,终于跟上大部队了,准备第三个项目了,转🐎人加油!

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值