在写个demo之前,专门学习了LRU:【LeetCode刷题】146. LRU 缓存-CSDN博客
使用哈希表 + 双向链表可以满足删除/增加的时间复杂度为O(1)。
在通读完15/445这块的说明之后,发现和LRU还是有些差别的。
官方文档中对LRU-K的解释是:LRU-K算法根据所谓的“后向k距离”来确定替换哪个缓存帧。后向k距离是指当前时间戳与第k次访问之间的时间差。如果某个缓存帧的历史访问次数少于k次,则其后向k距离被视为正无穷。当有多个缓存帧的后向k距离都是正无穷时,LRU-K算法会选择替换具有最早总体时间戳的缓存帧。换句话说,它会替换最近访问时间最早的缓存帧,从而使得系统更可能淘汰不常用的数据,而保留最常用的数据。
解释一下:之前学习的LRU算法是替换/最近最少使用的数据,在具体实现上采用的是哈希+双向链表的形式,将最近使用的数据放在双向链表表头,最少使用的放在尾部。
这里的LRU-K算法是以当前节点位基准,往前计数到第k个位置,将该位置的数据剔除;如果没有第k的位置,则后向距离视为正无穷(2147483647);当有多个缓存帧的后向k距离都是正无穷时,LRU-K算法会选择替换具有最早总体时间戳的缓存帧(即替换最先加进来的数据)。
15/445中的代码这里有两个类:LRUKNode类和LRUKReplacer类。
1 LRUKNode类
LRUKNode中有一些属性:
[[maybe_unused]] std::list<size_t> history_;
[[maybe_unused]] size_t k_;
[[maybe_unused]] frame_id_t fid_;
[[maybe_unused]] bool is_evictable_{false};
(1)history就是存放数据的节点,而C++中的list底层是双向链表,可以满足前插和删除操作;
(2)k_就是参数k;
(3)fid_是帧id,类型是using frame_id_t = int32_t; // frame id type;
(4)是否要剔除该节点,默认不剔除。
对照着LRUKReplacer的函数,给LRUKNode添加几个函数:
(1)构造函数肯定是要的;
(2)计算k距离的函数;(遍历到第k个相减即可,注意没有第k个情况时要返回无穷大)
(3)记录LRUKNode对象的访问历史函数;
开始可能就能想到这几个,增量式开发,后面写着写着需要哪些再添加。
2 LRUKReplacer类
对代码中的LRUKReplacer类各个函数一定要理解清楚。
(1)Size函数是最好实现的(::>_<::);
(2)Remove函数的实现思路:先查找,如果数据存在,再判断该数据是否是要被剔除的,如果是则删除;
(3)SetEvictable函数是指定帧是否可被替换的状态。这个函数还控制替换器的大小;
(4)RecordAccess是添加数据的,数据存在更新换时间戳;数据不存在则添加新的数据;
(5)Evict是最难的。【查找具有最大后向k距离的帧,并将其替换。只有被标记为可被替换的帧才是替换的候选对象。成功替换后,应该减少替换器的大小并移除帧的访问历史。】
LRU-K算法根据所谓的“后向k距离”来确定替换哪个缓存帧。后向k距离是指当前时间戳与第k次访问之间的时间差。如果某个缓存帧的历史访问次数少于k次,则其后向k距离被视为正无穷。当有多个缓存帧的后向k距离都是正无穷时,LRU-K算法会选择替换具有最早总体时间戳的缓存帧。换句话说,它会替换最近访问时间最早的缓存帧,从而使得系统更可能淘汰不常用的数据,而保留最常用的数据。
[[maybe_unused]] std::unordered_map<frame_id_t, LRUKNode> node_store_;
[[maybe_unused]] size_t current_timestamp_{0};
[[maybe_unused]] size_t curr_size_{0};
[[maybe_unused]] size_t replacer_size_;
[[maybe_unused]] size_t k_;
[[maybe_unused]] std::mutex latch_;
(6)这里也用到了哈希+list,符合前面LRU的思路,只是后面在测试的时候,发现unordered_map的value一直有问题,网上查了一下,大家都是用的指针形式,后面又使用了智能指针;
(7)std::mutex不允许拷贝构造,也不允许 move 拷贝,使用智能指针的形式;
c++ - 在 C++ 中使用用户定义的类型作为映射值 - IT工具网 (coder.work)
(8)宏的学习:这个宏的作用是确保 LRUKReplacer 类不能被拷贝构造、拷贝赋值、移动构造和移动赋值;
// 这个宏的作用是确保 LRUKReplacer 类不能被拷贝构造、拷贝赋值、移动构造和移动赋值。
DISALLOW_COPY_AND_MOVE(LRUKReplacer);
#define DISALLOW_COPY_AND_MOVE(cname) \
DISALLOW_COPY(cname); \
DISALLOW_MOVE(cname);
// Macros to disable copying and moving
#define DISALLOW_COPY(cname) \
cname(const cname &) = delete; /* NOLINT */ \
auto operator=(const cname &)->cname & = delete; /* NOLINT */
#define DISALLOW_MOVE(cname) \
cname(cname &&) = delete; /* NOLINT */ \
auto operator=(cname &&)->cname & = delete; /* NOLINT */
(9)还学习了一个之前没注意到的小知识:成员列表初始化属性的时候,要按照属性在类中出现的顺序初始化;
(10)删掉测试代码的DISABLED_
3 结果
这里只实现了LRUKdemo,所以得分是25分,看通过的case,LRUKReplacerTest是通过的。