CMU15445 2023project1详细过程(上)lru-k替换算法

Task1 LRU-K替换算法

1、知识了解

1.1LRU介绍:

LRU是内存满了,选择驱逐的页面时,选取最久没使用过的页面。这样的缺点是,比如一共五个位置,我们要做的操作是123123451222113123456,序号6的页面要把内存中的一个页面驱逐,可以看到123是经常需要用的页面,而45是偶然才用的。但使用LRU算法,把1逐出后,之后如果下一个要用1(很大概率),那就要驱逐2。为了防止出现这种情况,我们用LRU-K。

1.2LRU-K介绍

LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”
具体:维护两个队列,一个是历史记录,一个是缓存。页面来了先在历史记录队列存,访问次数达到K次后,把历史记录队列里的该页面转移到缓存队列中。
如果历史队列满了,删除规则自己定(LRU、先进先出等);
如果缓存队列满了,找倒数第K次访问离现在最久的数据删掉。

2、代码详解

2.1 头文件 (lru_k_replacer.h)

2.1.1各种变量名含义(添加了一些自己定义的)

变量名含义
current_timestamp_当前的时间戳,每进行一次record操作加一
curr_size_当前存放的可驱逐页面数量
max_size最多可驱逐页面数量
replacer_size_整个主存大小(用于判断页是否非法越界)
k_lru-k的k
lock_guard_加锁标志(std::mutex)
k_time页号对应的第k次的时间戳
timestamp用于记录访问时间
time_frame_页号与记录的访问时间的映射
recorded_cnt_记录访问次数
evictable_记录一个页面是否可以被驱逐
new_frame_记录不满足k次访问页的页号(上面说的历史访问队列)
new_locate_页号到历史访问队列的哈希表
cache_frame到达k次页的链表的页号(上面说的缓存队列)
cache_locate_页号到缓存队列的哈希表

2.1.2方法列表

方法名作用
LRUKReplacer::LRUKReplacer(size_t num_frames, size_t k) :初始化,定义越界范围和k值的
auto LRUKReplacer::Evict(frame_id_t *frame_id)驱逐一个页面,并保存到frame_id中
void RecordAccess(frame_id_t frame_id);增加一个页面的访问记录
void SetEvictable(frame_id_t frame_id, bool set_evictable);设置一个页面是否可以被驱逐
void Remove(frame_id_t frame_id);移除指定页面(仅在BufferPoolManager中删除页面时调用。)
auto Size() -> size_t;返回可驱逐页面的大小
auto LRUKReplacer::CmpTimestamp比较时间大小的

2.2Evict方法(驱除帧)

2.2.1思路
分两种情况,在历史队列里和在缓存队列里

如果在历史队列里
(1)页号和访问时间对应的容器内的东西要删掉。time_frame_[frame].clear();
(2)把历史访问列表相关的删掉(不要忘记记录访问次数的变量也要置为0)。recorded_cnt_[frame] = 0;new_locate_.erase(frame); new_frame_.remove(frame);
(3)统计量更改,可驱逐页面-1。 curr_size_–;
(4)找到的删除的页面要赋值给调用者提供的frame_id指针所指向的变量,以便调用者能够获取和使用这个标识符。*frame_id = frames;

如果在缓存队列里也是类似的,这里不多说了

注意!! 这里frame_id是一个指针,所以他驱逐的不是给定的一个帧,而是遍历历史列表和缓存列表,去找到哪个帧可以被驱逐,找到后,frame_id就会指向那个帧。因为访问历史列表和缓存列表都是按照时间由旧到新排列的,所以每次遍历都是从前往后遍历(即最久未使用的先看能不能替换)

2.2.2 代码

// 驱逐帧
auto LRUKReplacer::Evict(frame_id_t *frame_id) -> bool {
  std::lock_guard<std::mutex> lock(lock_guard_);
  // 如果没有可以驱逐元素
  if (curr_size_ == 0) {
    return false;
  }
  // 看访问历史列表里,有无帧可以删除
  for (auto it = new_frame_.rbegin(); it != new_frame_.rend(); it++) {
    auto frame = *it;
    // 如果可以被删除
    if (evictable_[frame]) {
      recorded_cnt_[frame] = 0;
      new_locate_.erase(frame);
      new_frame_.remove(frame);
      curr_size_--;
      time_frame_[frame].clear();
      *frame_id = frame;
      return true;
    }
  }
  // 看缓存队列里有无帧可以删除
  for (auto its = cache_frame_.begin(); its != cache_frame_.end(); its++) {
    auto frames = (*its).first;
    if (evictable_[frames]) {
      recorded_cnt_[frames] = 0;
      cache_frame_.erase(its);
      cache_locate_.erase(frames);
      curr_size_--;
      time_frame_[frames].clear();
      *frame_id = frames;
      return true;
    }
  }
  return false;
}

2.3 RecordAccess方法(添加页面)

2.3.1思路
在这里插入图片描述
这里最下面的一行写反了!!,是旧————————>新
在这里插入图片描述
和删除一样也是分三种情况:
新加入的;
要从历史队列到缓存队列的;
已经在缓存队列里的
小知识:
std::upper_bound(ForwardIt first, ForwardIt last, const T& value,comp);

• first 和 last:定义了搜索范围的迭代器,即 [first, last)。
• value:要搜索的值或对象。
• comp:一个比较函数或可调用对象,用于比较搜索范围内的元素和 value。

std::upper_bound 返回一个迭代器,指向序列中第一个大于 value 的元素。如果序列中没有这样的元素,则返回 last。

2.3.2代码

// 访问逻辑:不满k次放访问历史列表。。。。
void LRUKReplacer::RecordAccess(frame_id_t frame_id, [[maybe_unused]] AccessType access_typ) {
  std::lock_guard<std::mutex> lock(lock_guard_);
  // 如果越界
  if (frame_id > static_cast<frame_id_t>(replacer_size_)) {
    throw std::exception();
  }
  current_timestamp_++;
  recorded_cnt_[frame_id]++;
  auto cnt = recorded_cnt_[frame_id];
  // 在访问时间列表尾部加新的时间戳,旧时间在前,新时间在后
  time_frame_[frame_id].push_back(current_timestamp_);
  // 如果是新加入的记录
  if (cnt == 1) {
    if (curr_size_ == max_size_) {
      frame_id_t frame;
      Evict(&frame);
    }
    evictable_[frame_id] = true;
    curr_size_++;
    // 添加新节点
    new_frame_.push_front(frame_id);
    // 该节点下维护链表
    new_locate_[frame_id] = new_frame_.begin();
  }
  // 如果记录达到k次,则需要从新队列中加入到老队列中
  if (cnt == k_) {
    new_frame_.erase(new_locate_[frame_id]);  // 从新队列中删除
    new_locate_.erase(frame_id);
    auto kth_time = time_frame_[frame_id].front();  // 获取当前页面的倒数第k次出现的时间
    k_time new_cache(frame_id, kth_time);
    auto it = std::upper_bound(cache_frame_.begin(),    	cache_frame_.end(), new_cache, CmpTimestamp);  
    // 找到该插入的位置
    it = cache_frame_.insert(it, new_cache);
    cache_locate_[frame_id] = it;
    return;
  }
  // 如果记录在k次以上,需要将该frame放到指定的位置
  if (cnt > k_) {
    time_frame_[frame_id].erase(time_frame_[frame_id].begin());
    // 去除原来的位置
    cache_frame_.erase(cache_locate_[frame_id]);
    // 获取当前页面的倒数第k次出现的时间
    auto kth_time = time_frame_[frame_id].front();
    k_time new_cache(frame_id, kth_time);
    // 找到该插入的位置
    auto it = std::upper_bound(cache_frame_.begin(), cache_frame_.end(), new_cache, CmpTimestamp);
    it = cache_frame_.insert(it, new_cache);
    cache_locate_[frame_id] = it;
    return;
  }
}

2.4 SetEvictable方法(设置是否可以被驱逐)

2.4.1思路
只有两种情况需要改,原本是不驱逐,要改为驱逐的;和原本驱逐,要改为不驱逐的。
小知识:++、–最好放前面,因为放前面是值先加,加完放寄存器里。放后面是先放寄存器,加完后再放一遍寄存器。

2.4.2 代码

// 页面设置为驱逐 or 不驱逐 set_evictable
void LRUKReplacer::SetEvictable(frame_id_t frame_id, bool set_evictable) {
  std::lock_guard<std::mutex> lock(lock_guard_);
  if (recorded_cnt_[frame_id] == 0) {
    return;
  }
  // true是保留,false是驱逐
  if (!evictable_[frame_id]) {
    // 原本不扔,要求扔
    if (set_evictable) {
      ++max_size_;
      ++curr_size_;
    }
  } else {
    if (!set_evictable) {
      // 原本扔,要求不扔
      --max_size_;
      --curr_size_;
    }
  }
  evictable_[frame_id] = set_evictable;
}

2.5Remove方法(移除指定页面)

2.5.1 思路
和evit的思路很像,区别是这个方法不返回值。逻辑都是相同的

2.5.2 代码

// 移除页面
void LRUKReplacer::Remove(frame_id_t frame_id) {
  std::lock_guard<std::mutex> lock(lock_guard_);
  if (frame_id > static_cast<frame_id_t>(replacer_size_)) {
    throw std::exception();
  }
  auto cnt = recorded_cnt_[frame_id];
  if (cnt == 0) {
    return;
  }
  if (!evictable_[frame_id]) {
    throw std::exception();
  }
  // 在访问历史列表里
  if (cnt < k_) {
    recorded_cnt_[frame_id] = 0;
    new_frame_.erase(new_locate_[frame_id]);
    new_locate_.erase(frame_id);
    --curr_size_;
    time_frame_[frame_id].clear();
  } else {  // 在缓存队列里
    recorded_cnt_[frame_id] = 0;
    cache_frame_.erase(cache_locate_[frame_id]);
    cache_locate_.erase(frame_id);
    --curr_size_;
    time_frame_[frame_id].clear();
  }
}

Size()方法返回可驱逐页面的大小就可以,不写了

下面是比较大小的CmpTimestamp方法

auto LRUKReplacer::CmpTimestamp(const LRUKReplacer::k_time &f1, const LRUKReplacer::k_time &f2) -> bool {
  return f1.second < f2.second;
}

随便说点:这个代码整体是我从网上找的现成的,稍微改了点但只有非常非常少,直接就是一个面向结果编程。c++中间学了半个月结果还是毛都不会,笑死,进步了但不多。后面的项目代码网上越来越少看着令人绝望,我当时做这个一定是脑袋被踢了,救命!!有没有佬!救救我!!

参考文章
[1]https://blog.youkuaiyun.com/zhanglong_4444/article/details/88344953(LRU . LFU 和 LRU-K 的解释与区别)
[2]https://blog.youkuaiyun.com/AntiO2/article/details/128439155?spm=1001.2014.3001.5506(缓存替换策略:LRU-K算法详解及其C++实现 CMU15-445 Project#1)
[3]https://blog.youkuaiyun.com/albertsh/article/details/106976688(C++中的std::lower_bound()和std::upper_bound()函数)

### 关于 Carnegie Mellon University CMU 15-445 Project 2 的文档和资源 #### 文档概述 CMU 15-445 是一门关于数据库系统的课程,Project 2 主要聚焦于实现一个简单的缓冲池管理器 (Buffer Pool Manager, BPM)[^1]。此项目旨在让学生深入理解内存管理和磁盘I/O之间的交互机制。 #### 实现细节 在该项目中,学生需构建能够处理固定大小页面的缓存系统,并支持基本的操作如读取、写入以及刷新脏页到磁盘等功能。具体来说: - **Page Replacement Policy**: 学生可以选择不同的页面替换策略来优化性能,比如LRU(Least Recently Used) 或者 FIFO(First In First Out)- **Concurrency Control**: 需要考虑并发访问控制问题,确保多线程环境下数据的一致性和安全性。 - **Disk I/O Simulation**: 提供模拟环境用于测试BPM的行为,在不影响实际硬件的情况下验证算法的有效性。 ```cpp // C++ code snippet demonstrating a simple buffer pool manager interface. class BufferPoolManager { public: PageHandle FetchPage(page_id_t page_id); bool UnpinPage(page_id_t page_id, bool is_dirty); void FlushPage(page_id_t page_id); private: std::vector<Frame> frames_; // Array of frame objects representing the buffer pool }; ``` #### 教学目标 通过完成这个项目,学习者可以获得有关如何设计高效的数据结构和技术以应对大规模存储挑战的第一手经验。这不仅限于理论知识的学习,还包括实践技能的发展,例如调试复杂程序的能力和团队协作能力等软实力培养。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值