本文章记录CMU15445 2023fall课程的实验内容,实验初始源代码链接,注意不要在github中下载最新的
bustub源代码,并采用官方提供的Autograde平台进行评测,邀请码KK5DVJ
Project0 C++ Primer
本实验要求为前缀树数据结构完善Get\Put\Remove三种方法,需要注意的是,为了提高前缀树的并发可访问性,我们需要用新的map构建新的节点,以返回新的前缀树Trie
Task1 写时复制前缀树
Get
实现Get读取方法需要考虑以下几点
存在以下两种边缘情况
Trie为空(root_=nullptr),default()构造或Remove删除尽有效值节点key字符串为空,直接查找根节点中的值
遍历节点时,以下两种情况直接返回
- 目标
key不存在,直接返回nullptr - 查找到目标节点,并使用
std::dynamic_pointer_cast方法成功转化为TrieNodeWithValue进行值读取,需要注意的是std::dynamic_pointer_cast方法在运行时检查类型转换
- 目标
Put
关于Put和Remove两种写方法,可以采用递归实现,每一层递归用于向子节点插入key字符串的去头子串,关于该递归方法需要考虑以下几点
1 | // 函数签名 |
两种边缘情况
key字符串为空,直接将值插入当前节点,但需要注意继承原先节点的children_子节点映射key字符串长度为1,考虑两种情况- 该字符已存在对应子节点,需要继承原节点的
children_映射并更新value_ - 该字符不存在对应子节点,则用
value_构造新的子节点,并与父节点连接
- 该字符已存在对应子节点,需要继承原节点的
递归插入,同样考虑两种情况
当前字符串
key的首字符已存在子节点,则需要向递归函数中传入已存在的子节点指针当前字符串首字符不存在子节点,则传入新构造的空子节点指针
此处存在缺陷是,函数传入的空子节点,进入函数体内部后又会被重新
Clone()一次,舍弃原先的空节点,造成无用的重复构造
Remove
1 | // 函数签名 |
- 三种边缘情况
key字符串为空,需要删除根节点中的值key字符串的首字符不存在,说明待删除的键不存在,直接返回原节点key字符串长度为1- 该字符对应的节点不存在值,说明该键值对不存在,直接返回原节点
- 键值对存在,但是值节点不是叶子节点,需要将值节点降级为内部节点
- 键值对存在,且值节点是叶子节点,则需要
children_.erase()删除节点
- 递归删除,考虑两种情况
- 返回的子节点既不是值节点,
children_也为空,则需要摒弃该子节点 - 否则,将新的子节点连接到当前节点
- 返回的子节点既不是值节点,
Task2 并发键值存储
在前缀树的并发访问中,题目要求读之间兼容、写操作互斥、写操作进行时,读操作能够从旧根中开始。该并发设计最难在于最后一点,但是Task1的正确实现能够满足这一点
如上图所示,当读写操作并行时,只要读操作能够获取原先的旧根指针,就能在Get方法的逻辑中正确读下去,原先逻辑删除的节点仍然物理存在
Project1 Buffer Pool
Task1 LRUKReplacer
在测试LRUKReplacer类型中LRU-K算法时,重点在于Evict()驱逐方法的实现。
关于LRU算法,我们只关注缓冲池中每一个页帧最近一次访问的时间,所谓least recently used算法,即LRU-1算法,最近一次使用距离现在最远的页帧是我们需要淘汰的对象,但LRU-1不能体现LRU算法最近最少使用的思想,因为页帧的淘汰都仅考虑最近一次的访问时间戳(而不是最近多次),其存在弊端就是未能考虑到页帧访问的频率:假设页帧2的访问频率远低于页帧1,而页帧2和页帧1同时存在于缓冲池中,但是页帧2最近一次访问时间近于页帧1,那么在驱逐时可能会将访问频率高得多的页帧1驱逐出去。
因此,在LRU-K算法中,我们跟踪了缓冲池中每一个页帧最近K次访问的时间戳,每当要进行淘汰时,我们驱逐倒数第K次访问距离现在最远的页帧,在这样巧妙的比较下,该算法隐式地将页帧访问频率考虑了进去。
参考以下图片,可以看到容量为2的缓冲池中缓存了A,B的访问频率远小于A,但是B最近的访问近于A,在LRU算法中,此时我们淘汰A;但是在LRU-5算法中,我们将当前时间戳和页帧倒数第5次访问的时间戳做差,发现B的差值更大,说明B的访问频率低于A,淘汰对象为B

