当前位置: 首页 > news >正文

高阶数据结构——LRU Cache

1.什么是LRU Cache

LRU是Least Recently Used的缩写,意思是最近最少使用,它是一种Cache替换算法。 什么是Cache?狭义的Cache指的是位于CPU和主存间的快速RAM, 通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术。 广义上的Cache指的是位于速度相差较大的两种硬件之间, 用于协调两者数据传输速度差异的结构。除了CPU与主存之间有Cache, 内存与硬盘之间也有Cache,乃至在硬盘与网络之间也有某种意义上的Cache── 称为Internet临时文件夹或网络内容缓存等。

例如:我们现在要打开微信,操作系统会从赋存中将微信相关的代码数据加载到内存中,CPU读取内存中的数据即可,但是CPU的速度是非常快的,相对于CPU来说,内存的速度就很慢了。

为了解决这个问题,会在CPU与内存之间增加一个Cache来协调两者的速度差异。当我们在使用微信的视频聊天功能时,CPU下次读取的指令大概率还是视频聊天,所以可以预先加载一部分视频聊天的数据或者代码到Cache中去。Cache要比内存快得多,能更好的配合CPU工作。
空间局部性原理:现在正在使用的数据和将来要使用的数据在存储位置上可能是相邻的。

而Cache就是建立在局部性原理上工作的。

2. Cache的替换算法

计算机的硬件设计上,速度越快的,造价越高,容量越小,Cache的速度虽然快,但是容量较小,如果Cache满了,现在需要将新的内容加载到Cache中,则需要进行替换,有四种比较常见的Cache替换算法。

  • 1.随机算法(RAND)
  • 2.先进先出算法(FIFO)
  • 3.近期最少使用(LRU)
  • 4.最近不经常使用(LFU)

2.1 随机算法(Rand)

在Cache满了时,随机选择一块进行替换。

假设现在一共有4个Cache块,以下是CPU访问的主存块号顺序。{1,2,3,4,1,2,5,1,2,3,4,5}

1,2,3,4号主存块直接插入即可,因为此时Cache还没有满,接下来的1,2号主存块因为Cache中已经存在,所以直接访问Cache中的即可。

到访问5号内存块时,因为Cache中没有,所以要进行替换,由于当前是随机替换,这里就替换3号Cache。

再接下来的1,2号都可以直接命中。

3号主存块未命中,所以需要替换一个Cache块,我们这里替换4号。

4号主存块也是一样,未命中随机替换一个Cache块

最后一个5命中了,不需要替换。

随机算法很简单,但是不稳定,没有考虑到局部性原理,命中率较低。

2.2 先进先出算法(FIFO)

顾名思义,就是先进入Cache的内存块先被替换掉。

还是和上面一样,将主存块号为{1,2,3,4,1,2,5,1,2,3,4,5}依次插入Cache中。

前面的1,2,3,4可以直接插入,因为此时的Cache还未满。

接下来的1,2号主存块命中了。

访问5号主存块时,5号主存块未命中,所以此时需要进行替换,这里的算法是先进先出,所以5号主存块将1号Cache块替换掉了。

接下来的2号结点,因为也未命中Cache,所以需要进行替换,目前最新进入Cache的是3号,所以我们将3号替换成2号。

接下来和前面一样,这里不过多赘述。

和随机算法一样,实现简单,没有考虑局部性原理,最先进入的也有可能是最频繁访问的。

2.3 近期最少使用算法(LRU)

为每一个Cache设置一个计数器,记录每个Cache多久没有被访问了,替换时优先选择计数值最大的那个Cache。

每次访问Cache时,有以下三种情况。

  • 命中时,所命中的行的计算器清零,比其低的计算器加1,其余不变
  • 未命中且还有空闲行时,新装入的计数器为0,其余非空闲行全加1;
  • 未命中且无空闲行时,将计数值最大的行淘汰,新装行的计数器置为0,其余全加1

CPU依次访问主存块号为{1,2,3,4,1,2,5,1,2,3,4,5}。

前面4个主存块直接插入即可,因为Cache未满,并且还要更新计数器。

接下俩访问的是1号主存块,由于Cache中已经存在1号主存块,所以不需要替换,我们再来看看当命中时需要怎么做。

命中时,所命中的行的计算器清零,比其低的计算器加1,其余不变

所以我们需要更改计数器的值,如上图所示。

接下来访问的是2号主存块,和前面一样,Cache命中,将对应的计数器清零,其他计数器加1。

访问5号主存块时,Cache未命中,需要进行替换,找到计数器最大的Cache进行替换,也就是将3号替换成5,并将对应的计数器清零,其他的加1。

访问1号主存块,Cache命中,将对应计数器置为0,并将比他低的计数器加1,所以4号主存块的计数器不需要加1。

我们当然可以将4号主存块也加1,但是没有必要,因为4号主存块的计数器已经是最大了,没有必要再加1,这样子设计有个好处,就是如果一共有2^n个Cache块,那么只需要n位技术器就可以,在顶层设计硬件时更加简单。

此时访问2号主存块也是一样,Cache命中,将对应计数器置0,将比2号小的计数器值加1。

访问3号时,Cache没有命中,将计数器最大的主存块,也就是4号替换成3号,其余的计数器加1.

访问4号主存块,Cache未命中,将计数器最大的5号替换成4号,其余计数器加1

访问5号主存块,Cache未命中,将计数器最大的1号换成5号,其余的计数器加1。

LRU算法是基于局部性原理的,近期被访问过的主存块,在将来很有可能再次被访问。LRU算法实际运行的效果优秀,Cache命中率高。

但是如果频繁访问的主存块数量 > Cache行的数量时,可能会发生"抖动",例如Cache行的数量只要4时,遇到如下{1,2,3,4,5,1,2,3,4,5,1,2,3,4,5....}。虽然每个内存块都会频繁访问,但是Cache行的数量较小,所以也会被替换。

2.4 最不经常使用算法(LFU)

为每一个Cache设置一个计数器,记录每个Cache被访问的次数,满了后替换计数器最小的。

设一共有4个Cache块,依次访问主存块{1,2,3,4,1,2,5,1,2,3,4,5}。

前面几个还是一样。

访问5号主存块时,Cache未命中需要进行替换,找到计数器值最小的进行替换,也就是将3号替换成5号。

接下来的1,2都会命中,直接将计数器+1即可。

访问3号主存块时,Cache未命中需要进行替换,找到计数器值最小的进行替换,也就是将5号替换成3号。

Cache命中,计数器+1

访问5号主存块时,Cache未命中需要进行替换,找到计数器值最小的进行替换,也就是将3号替换成5号。

LFU是基于曾经被经常访问,在未来不一定会用到,没有很好遵循局部性原理,所以实际效率不如LRU。

3. LRU Cache的实现

实现LRU Cache的方法和思路很多,但是要保持高效实现O(1)的put和get,那么使用双向链表和哈希表的搭配是最高效和经典的。使用双向链表是因为双向链表可以实现任意位置O(1)的插入和删除,使用哈希表是因为哈希表的增删查改也是O(1)。

下面以一道oj题带大家自己实现一个Cache。

题目链接:146. LRU 缓存 - 力扣(LeetCode)

要让get和put都在O(1)时间内完成,我们可以借助哈希表来完成,因为哈希表的查找和插入效率都是O(1),也就是说get时可以做到O(1),而put的大多数场景也可以做到,之所以说大多数就是因为put时可能Cache未命中,此时需要替换出计数器最大的那个数,如果只有一个哈希表,是无法在O(1)的时间内找到计数器最大的值,所以我们添加一个链表。

链表的尾部保存的是最近最少使用的key,如果一个数被使用了,那么我们就将这个数提到链表的头部,哈希表保存的是key和key在链表中的位置。

代码如下:

class LRUCache {
public:LRUCache(int size) {capacity = size;}int get(int key) {auto find = hashMap.find(key);if (find == hashMap.end()){return -1;}else {//更新key的位置。Iterator it = find->second;//将结点挪到链表头部LRUList.splice(LRUList.begin(), LRUList, it);return it->second;}}void put(int key, int value) {auto find = hashMap.find(key);if (find == hashMap.end()){//没找到,将这个结点插入Cache中。if (LRUList.size() == capacity){//如果Cache满了,就将使用最少的删除。pair<int, int> end = LRUList.back();LRUList.pop_back();hashMap.erase(end.first);}//将key插入Cache中LRUList.push_front({key, value});hashMap.insert({key, LRUList.begin()});}else {//找到了,更新他的值auto it = find->second;it->second = value;//将该结点放至链表开头LRUList.splice(LRUList.begin(), LRUList, it);}}typedef list<pair<int, int>>::iterator Iterator;unordered_map<int, Iterator> hashMap;list<pair<int, int>> LRUList;size_t capacity;
};

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • pod详解 list-watch机制 预选优选策略 如何指定节点调度pod
  • 10.1 使用ansible部署 redis-exporter
  • Python 机器学习求解 PDE 学习项目 基础知识(3)matplotlib 画函数热图
  • 十六、【Python】基础教程 - 【Flask】网络编程开发
  • SpringBoot可以同时处理多少请求?
  • WHAT - xmlhttprequest vs fetch vs wretch
  • YOLO系列:从yolov1至yolov8的进阶之路 持续更新中
  • 【数据结构】队列,你必须知道的内部原理!!!
  • 大数据Flink(一百零九):阿里云Flink的基本名称概念
  • 保障速度与安全合规的前提下,如何传文件到国外?
  • 【解压既玩】PS3模拟器v0.0.32+战神3+战神升天+各存档 整合包 ,完美不死机,没有BUG,旷世神作,强力推荐
  • AI编程工具合集整理优缺点
  • HarmonyOS Developer之生命周期
  • Java设计模式-单例模式最佳实践
  • 第26课 Scratch入门篇:乘坐公交车
  • php的引用
  • [NodeJS] 关于Buffer
  • “寒冬”下的金三银四跳槽季来了,帮你客观分析一下局面
  • gops —— Go 程序诊断分析工具
  • Java比较器对数组,集合排序
  • leetcode378. Kth Smallest Element in a Sorted Matrix
  • MQ框架的比较
  • 第13期 DApp 榜单 :来,吃我这波安利
  • 基于阿里云移动推送的移动应用推送模式最佳实践
  • 设计模式(12)迭代器模式(讲解+应用)
  • 问题之ssh中Host key verification failed的解决
  • 一天一个设计模式之JS实现——适配器模式
  • MPAndroidChart 教程:Y轴 YAxis
  • 不要一棍子打翻所有黑盒模型,其实可以让它们发挥作用 ...
  • ​人工智能之父图灵诞辰纪念日,一起来看最受读者欢迎的AI技术好书
  • # Redis 入门到精通(七)-- redis 删除策略
  • ### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTr
  • #include
  • #vue3 实现前端下载excel文件模板功能
  • #使用清华镜像源 安装/更新 指定版本tensorflow
  • #我与Java虚拟机的故事#连载04:一本让自己没面子的书
  • $(function(){})与(function($){....})(jQuery)的区别
  • (4)(4.6) Triducer
  • (C++二叉树05) 合并二叉树 二叉搜索树中的搜索 验证二叉搜索树
  • (二十九)STL map容器(映射)与STL pair容器(值对)
  • (非本人原创)我们工作到底是为了什么?​——HP大中华区总裁孙振耀退休感言(r4笔记第60天)...
  • (附源码)springboot“微印象”在线打印预约系统 毕业设计 061642
  • (排序详解之 堆排序)
  • (入门自用)--C++--抽象类--多态原理--虚表--1020
  • (三十五)大数据实战——Superset可视化平台搭建
  • (幽默漫画)有个程序员老公,是怎样的体验?
  • (原创)攻击方式学习之(4) - 拒绝服务(DOS/DDOS/DRDOS)
  • (原創) 博客園正式支援VHDL語法著色功能 (SOC) (VHDL)
  • (转)http协议
  • (转)JAVA中的堆栈
  • .NET 4.0网络开发入门之旅-- 我在“网” 中央(下)
  • .NET 8 跨平台高性能边缘采集网关
  • .net core docker部署教程和细节问题
  • .NET国产化改造探索(一)、VMware安装银河麒麟
  • .NET企业级应用架构设计系列之开场白