lyorz's blog

Proj1 Buffer Pool Manager

2026/02/06
loading

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
2
guard0 = std::move(guard1);
ASSERT_EQ(0, bpm->GetPinCount(pid0));

在上述测试代码中,期望达成的目标是:

  • 赋值前:
    • guard0持有pid0(pin=1)
    • guard1持有pid1(pin=1)
  • 移动后:
    • guard0持有pid1 (pin=1)
    • guard1 状态无效

移动语义过程:

  1. guard0首先释放原先持有资源(即pid0的latch,以及pin_count–);
  2. guard0接管guard1的资源(持有pid1);
  3. guard1被置为invalid(这样避免析构时重复释放资源)。

Bug记录:脏页标记

is_dirty位没有维护好,导致大多数时间都在对FlushPage*进行debug。

false → true

发生在:

  • WritePageGuard 创建(写访问允许时)
  • WritePageGuard Drop (确保脏页刷盘)

true → false

发生在:

  • flush 成功,但在unsafe时需要注意,flush成功也受到释放bpm_latch之后有无其他线程pin住此页并作出修改的限制,因此置dirty位false时需要额外检查。
  • ReadPageGuard 创建

CATALOG
  1. 1. Task1: Adaptive Replacement Cache (ARC) Replacement Policy
    1. 1.1. ARC Replacer
    2. 1.2. RecordAccess 性能优化
  2. 2. Task2: Disk Scheduler
    1. 2.1. 前置
  3. 3. Task3: Buffer Pool Manager
    1. 3.1. 前置
    2. 3.2. Bug记录:PageGuard移动语义与Pin计数
    3. 3.3. Bug记录:脏页标记