CMU 15-445(Fall 2023) Project1 Buffer Pool Manager 个人笔记
PROJECT #1 - BUFFER POOL
总结#
Project1没有做针对排行榜的优化。Project1基础部分不算难,但Bustub
中只提供了简单的测试样例,通过了本地的测试后提交到gradescope
可能拿不了满分,需要根据gradescope
的提示信息再进行更正。
建议在函数中多打印一些调试信息,这些调试信息在gradescope
的报告中是能看到的。
例如在函数入口处打印函数调用信息和参数信息,在实现Task1
时,把当前replacer
管理的frame
信息打印出来。
auto LRUKReplacer::Evict(frame_id_t *frame_id) -> bool {
std::cout << "Before evict: ";
PrintNodeStore();
// ...
std::cout << "After evict: ";
PrintNodeStore();
}
auto BufferPoolManager::FetchPage(page_id_t page_id, [[maybe_unused]] AccessType access_type) -> Page * {
std::cout << "FetchPage: " << page_id << "\n";
// ...
}
其它经验#
头文件中包含了要实现的方法的详细注释,结合这些注释以及测试文件中的代码,基本就能够很好的理解方法要实现什么功能了。
Task #1 - LRU-K Replacement Policy#
Evict方法的补充测试函数#
这里补充一个Evict
方法的测试函数,可以直接放到测试文件中用,直接把它粘贴到lru_k_replacer_test.cpp
中即可。
TEST(LRUKReplacerTest, SecondSimpleTest) {
LRUKReplacer lru_replacer(7, 3);
// [4, 3, 2, 1]
lru_replacer.RecordAccess(1);
lru_replacer.RecordAccess(2);
// 3的最早访问时间
lru_replacer.RecordAccess(3);
// 4的最早访问时间
lru_replacer.RecordAccess(4);
// [4, 1, 2, 3]
lru_replacer.RecordAccess(1);
lru_replacer.RecordAccess(2);
lru_replacer.RecordAccess(3);
// [4, 3, 1, 2]
lru_replacer.RecordAccess(1);
lru_replacer.RecordAccess(2);
// 把1,2,3,4都标记为Evictable
lru_replacer.SetEvictable(1, true);
lru_replacer.SetEvictable(2, true);
lru_replacer.SetEvictable(3, true);
lru_replacer.SetEvictable(4, true);
// 此时4被访问过1次,3被访问过2次, 4和3的访问次数都小于k
// 但是3的第一次访问时间早于4, 所以被淘汰的应该是3,而不是4
frame_id_t frame_id;
lru_replacer.Evict(&frame_id);
ASSERT_EQ(3, frame_id);
}
LRU算法简介#
Task1
的要求是实现一个LRU-K
算法,在做实验之前要先做一下leetcode
的146题(LRU缓存),了解一下LRU
算法的实现,做完leetcode
中的习题,再结合头文件中的注释以及测试文件中的代码,应该很快就有一个大体的思路了。
下面简单介绍下LRU
算法,具体的讲解请参考其他资料。
LRU(Least Recently Used)
算法即"最近最少使用算法",通常应用于缓存的管理,当缓存区域已满,且有新的数据需要进入缓存时,使用LRU
算法来决定淘汰缓存中的哪部分数据,而LRU
算法的思想就是优先淘汰最近一段时间最少使用的数据。
LRU
算法的一种实现方式是通过哈希表+双向链表的方式,每次淘汰的都是链表头部的节点,当数据被访问时,就将数据移动到链表的尾部。
图例:
淘汰元素
访问元素
题目链接: 146. LRU 缓存
LRU-K算法简介#
题目中要求的LRU-K
算法,相比于LRU
算法,多了一个访问次数的属性,题目中还规定了一个evictable
属性,来标记一个元素是否是可淘汰的。其实实现方式和LRU
算法基本一致,只不过需要多维护一些信息,以及在淘汰算法的策略上有所区别。
Evict方法的分情况讲解#
Case1: 所有元素都不可以驱逐,那么直接返回False
Case2: 存在可以驱逐的元素且这些可驱逐元素中存在访问次数小于k次的元素,那么将从这些访问次数小于k且可驱逐的元素中选择一个元素来驱逐。
驱逐策略: 获取这些元素的第一次访问时间,选择出第一次访问时间最早的那一个来驱逐即可。LRUKReplacer
类中提供了一个curr_timestamp_
属性,每当我们访问一个元素时,将curr_timestamp_
的值记录到其访问历史中,当需要比较时,取出访问历史中的第一个元素进行比较即可。
例: 对于下面这种情况,f1
、f2
访问次数都小于3并且都可驱逐,但是f1
的最早访问时间是3,所以这里应该驱逐f1
,而不是f2
。
k = 3;
f4: 不可驱逐 访问历史: <1>
f2: 可驱逐 访问历史: <4>
f1: 可驱逐 访问历史: <3, 4>
f3: 可驱逐 访问历史: <1, 2, 3>
Case3: 存在可以驱逐的元素,并且这些可驱逐的元素的访问次数都大于等于k,那么此时算法就退化成了LRU
算法,按照LRU
算法的策略来实现即可,对于使用哈希表+双链表的实现方式,遍历链表,淘汰第一个可驱逐的元素即可。
Task3#
task3要实现缓冲池管理器,这个task并不难,这里记录几个我自己遇到的问题。
BufferPoolManager的pages_属性用法#
pages_
用于存储缓冲器管理器的所有页面,在构造函数中就申请好了内存,因此,在NewPage
方法中,并不需要真正的创建新的页面,而是找到空闲的frame_id
,复用pages_[frame_id]
这个页面。
BufferPoolManager::BufferPoolManager(size_t pool_size, DiskManager *disk_manager, size_t replacer_k,
LogManager *log_manager)
: pool_size_(pool_size), disk_scheduler_(std::make_unique<DiskScheduler>(disk_manager)), log_manager_(log_manager) {
// ...
pages_ = new Page[pool_size_];
// ...
}
class BufferPoolManager {
/** Array of buffer pool pages. */
Page *pages_;
}
UnpinPage方法#
该方法有一个is_dirty
参数,注意不要把该参数直接赋值给页面的is_dirty_
属性,这个参数的含义是调用UnpinPage
方法的线程是否修改了页面(是否使得页面变脏了)。
如果这个is_dirty
参数是false
,我们并不应该把对应页面的is_dirty_
属性置为false
,因为它仅仅代表该线程没有修改页面。
所有,只有is_dirty
参数是true
时,才把对应页面的is_dirty_
属性置为true
,其它时候忽略该参数即可。
FetchPage方法#
在实现FetchPage
方法时,一开始没有完全理解方法的作用。
Fetch the requested page from the buffer pool
这句话意味着,如果要Fetch
的页面已经在缓存池中,那就并不需要将硬盘中的内容读至缓存中,而仅仅是更新一下缓冲池中该页面的状态信息即可。
/**
* @brief Fetch the requested page from the buffer pool. Return nullptr if page_id needs to be fetched from the disk
*/
auto FetchPage(page_id_t page_id, AccessType access_type = AccessType::Unknown) -> Page *;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通