Proj1实验记录,感谢Copilot老师以及Chatgpt老师在此过程中给予的所有支持。
Task1: Adaptive Replacement Cache (ARC) Replacement Policy
ARC Replacer
ARC(Adaptive Replacement Cache)通过同时维护“最近性”和“频繁性”的历史信息,在 LRU 倾向与 LFU 倾向之间自适应调整缓存空间分配。
在 proj1 中可对应为四个队列:
- 活跃队列:mru_(类似论文 T1,按最近访问保留),mfu_(类似论文 T2,代表被重复访问后更“热”的页)
- 幽灵队列:mru_ghost_(类似 B1,记录从 mru_ 被逐出的 page_id),mfu_ghost_(类似 B2,记录从 mfu_ 被逐出的 page_id)
核心参数:
- mru_target_size_ = p,表示希望 mru_(T1)占用的目标容量,p ∈ [0, cache_size]。
自适应调整(p 的更新时机)
当请求的页面 命中幽灵队列,说明“我们刚刚把它逐出过,但现在又需要它”,ARC 用这个信号来调 p:
命中 mru_ghost_:说明最近性(MRU/LRU)这边被挤压得不够,应增大 p(让 mru_ 变大一些)
命中 mfu_ghost_:说明频繁性(MFU/LFU)更重要,应减小 p(让 mfu_ 相对变大)
直观理解:
B1 hit ⇒ 最近性不够 ⇒ 给 MRU 多分点空间
B2 hit ⇒ 频繁性更重要 ⇒ 给 MFU 多分点空间
逐出策略(Replace 规则)
当需要腾出 frame 时,ARC 的目标是让 |mru_| 尽量贴近 p:
如果当前 |mru_| 相对 p 偏大,优先从 mru_ 逐出,并把被逐出的 page_id 记录到 mru_ghost_
否则从 mfu_ 逐出,并记录到 mfu_ghost_
这样,p 就成为一个可被历史访问模式动态调整的可变参数,让系统在偏 LRU(p 大)与偏 LFU(p 小)之间动态切换。
RecordAccess 性能优化
原始代码结构中将mru_(T1)、mfu_(T2)、mru_ghost_(B1)、mfu_ghost_(B2)都规定为std::list,其中std::list底层为双向链表实现。
第一次实现,没有动任何结构,包括FrameStatus。
在RecordAccess里主要涉及到如下方法调用:
- std::list::push_front():由于底层结构为双向链表,O(1);
- std::list::back():O(1);
- std::list::remove(frame_id/page_id):O(n)高费,链表随机访问,每次remove必须从头到尾遍历寻找要删除的值。
此时perfromance test结果如下:
第二次实现主要针对remove进行优化,思路为:
- 在 FrameStatus 中新增指向所在链表节点的迭代器(alive: MRU/MFU;ghost: MRU_GHOST/MFU_GHOST)。std::list 的迭代器对其他元素的插入/删除保持稳定,仅当节点自身被 erase 删除时才失效;
- 在节点被插入对应列表、或在 alive/ghost 间迁移时,同步更新其迭代器与状态标记;
- 在 RecordAccess 中将原先按值删除的 list::remove(id)(需线性扫描,O(n))替换为按迭代器删除/移动:erase(iter) 或 splice(…)(O(1)),从而将热点路径从“扫描定位 + 删除”降为“直接定位 + 常数时间操作”。
优化后performance test结果如下:
TODO:实际上我们只需要在FrameStatus中维护一个迭代器就可以了,因为Frame在同一时刻只能位于一个列表中。
Task2: Disk Scheduler
前置
记录一些不太熟悉的地方。
Channel
channel类目的是实现多个线程之间安全的数据共享。本质上被实现为一个多生产者-多消费者模型。
其内部成员变量由条件变量(std::condition_variable)、队列(std::queue)、锁(std::mutex)组成。
提供:
- Get:从共享队列中获取队首元素,如果队列为空则循环阻塞等待;
- Put:向共享队列中写入数据,写入后唤醒被等待的线程。
Task3: Buffer Pool Manager
前置
RAII(Resource Acquisition Is Initialization)
RAII要求资源有效期与持有资源的对象生命周期严格绑定,即资源在构造函数中完成分配/获取,在析构函数中完成资源释放。
Bug记录:PageGuard移动语义与Pin计数
在BufferPoolManager中:
- ReadPageGuard/WritePageGuard都遵循RAII原则;
- 构造时:
- 获取frame latch
- pin_count++
- 析构时:
- pin_count–
- 释放frame latch
- 更新replacer evictable,标记该frame可以驱逐
移动语义理解:移动语义负责转移Drop责任,即资源所有权。move调用时当前对象将释放手中原有资源,接管目标对象资源所有权,并令目标对象失效。
1 | guard0 = std::move(guard1); |
在上述测试代码中,期望达成的目标是:
- 赋值前:
- guard0持有pid0(pin=1)
- guard1持有pid1(pin=1)
- 移动后:
- guard0持有pid1 (pin=1)
- guard1 状态无效
移动语义过程:
- guard0首先释放原先持有资源(即pid0的latch,以及pin_count–);
- guard0接管guard1的资源(持有pid1);
- guard1被置为invalid(这样避免析构时重复释放资源)。
Bug记录:脏页标记
is_dirty位没有维护好,导致大多数时间都在对FlushPage*进行debug。
false → true
发生在:
- WritePageGuard 创建(写访问允许时)
- WritePageGuard Drop (确保脏页刷盘)
true → false
发生在:
- flush 成功,但在unsafe时需要注意,flush成功也受到释放bpm_latch之后有无其他线程pin住此页并作出修改的限制,因此置dirty位false时需要额外检查。
- ReadPageGuard 创建