在计算机的世界里,缓存就像一个“快速仓库”,它存储着我们频繁访问的数据,大大提升了数据的读取速度。但这个 “仓库” 空间有限,当它被装满时,就得决定舍弃一些数据,为新数据腾出位置,这个决策过程就由缓存置换算法来掌控。今天,就让我们探索常见的缓存置换算法中的最近最少使用算法(LRU),并使用c++实现它。

一、LRU 算法原理

        LRU 算法的核心思想非常简单:当缓存已满,需要淘汰数据时,优先淘汰那些最近最少使用的数据。想象一下你在整理书架,那些很久都没被翻阅过的书籍(最近最少使用的数据),往往会被你放到更靠后的位置,甚至在书架空间不足时,你会优先选择将它们清理出去,以便为新的书籍腾出空间。LRU 算法就是基于这样的逻辑,确保缓存中始终保留最有可能被再次访问的数据。

        为了实现这一目标,LRU 算法需要记录每个数据的访问时间或者访问顺序。每当访问一个数据时,它就会被标记为 “最近使用”,而那些长时间没有被访问的数据则会逐渐成为 “最近最少使用” 的数据。当缓存空间不足时,这些数据就会被替换掉。

二、实现 LRU 算法的数据结构选择

        要实现 LRU 算法,选择合适的数据结构至关重要。通常,我们会结合双向链表和哈希表来实现 LRU 缓存。

(一)双向链表

         双向链表用于维护数据的访问顺序。链表中的每个节点代表一个缓存数据,链表的尾部表示最近使用的数据,头部表示最近最少使用的数据。当访问一个数据时,对应的节点会被移动到链表尾部;当需要淘汰数据时,直接删除链表头部的节点即可。双向链表的优势在于,插入和删除操作的时间复杂度都是 O(1) ,这对于频繁的缓存操作来说非常高效。例如,在一个频繁读写的数据库缓存中,数据的插入和删除操作会经常发生,双向链表的这种特性可以保证缓存操作的快速执行。

(二)哈希表

        哈希表则用于快速定位数据在双向链表中的位置。它以数据的键作为索引,存储对应数据在双向链表中的节点指针。这样,在查找数据时,通过哈希表可以在 O(1) 的时间复杂度内找到对应的数据节点,然后根据节点指针在双向链表中进行操作。例如,在一个基于键值对存储的内存缓存中,通过哈希表可以快速找到特定键对应的数据,提高缓存的查询效率。

        不清楚哈希表的可以看我以前的文章c++ 手写STL篇(三)实现hashtable

        结合双向链表和哈希表,我们可以在 O(1) 的时间复杂度内完成数据的查找、插入和删除操作,满足 LRU 算法高效性的要求。

三、LRU 算法的操作流程

        下面详细介绍 LRU 算法在缓存操作中的具体流程。

(一)查询操作

        当进行查询操作时,首先根据数据的键在哈希表中查找对应的节点。如果找到了,说明数据在缓存中,将该节点从双向链表中移除,并插入到链表尾部,表示该数据是最近使用的;如果在哈希表中未找到,则说明数据不在缓存中,需要从其他数据源获取数据(例如从磁盘读取数据),然后将数据插入到缓存链表尾部。

(二)插入操作

        插入操作分为两种情况。如果缓存未满,直接将数据插入到双向链表头部,并在哈希表中记录数据的键和对应的节点指针;如果缓存已满,则先删除双向链表头部的节点(即最近最少使用的数据),同时在哈希表中删除对应的键值对,然后再将新数据插入到双向链表尾部,并更新哈希表。

四、LRU 算法的应用场景

        LRU 算法在许多实际场景中都发挥着重要作用,以下是一些常见的应用场景。

  1. 操作系统内存管理:以 Ubuntu 系统为例,在其内存管理中,LRU 算法发挥着重要作用。Ubuntu 基于 Linux 内核,系统会将常用的数据和程序页面缓存到物理内存中,以提升访问速度。当物理内存不足时,就需要淘汰部分页面。此时,LRU 算法会优先淘汰那些最近最少使用的页面。假设用户在 Ubuntu 系统上同时运行多个程序,如浏览器、文本编辑器和音乐播放器。一段时间内,用户频繁操作浏览器,而文本编辑器和音乐播放器处于后台且较少被使用。那么,根据 LRU 算法,文本编辑器和音乐播放器相关的内存页面(如果长时间未被访问)就更有可能被淘汰,从而为其他更活跃的程序腾出内存空间,保证系统的稳定运行和高效性能。

  2. 浏览器缓存优化:主流浏览器如 Chrome、Firefox 等均采用 LRU 算法管理缓存。当用户浏览网页时,浏览器会将网页的 HTML、CSS、图片等资源缓存起来。以在 Ubuntu 系统上使用 Chrome 浏览器访问网页为例,当用户再次访问相同或相关页面时,若资源在缓存中,浏览器可直接从缓存读取,减少网络请求,加快页面加载速度(背过八股文的应该清楚这一部分)。随着用户浏览的网页增多,缓存空间会逐渐被占满。此时,LRU 算法会将最近最少访问的网页资源淘汰,确保缓存中始终保留最有可能再次被访问的内容。比如,用户之前浏览过多个新闻页面,一段时间后又频繁访问购物网站,那么那些较早浏览且长时间未再次访问的新闻页面缓存资源就可能被 LRU 算法淘汰,为购物网站的缓存资源腾出空间。

  3. CPU 缓存协同工作:在 计算机硬件层面,CPU 缓存同样应用了 LRU 算法或类似策略。CPU 缓存用于存储 CPU 频繁访问的数据和指令,分为一级缓存(L1 Cache)、二级缓存(L2 Cache)和三级缓存(L3 Cache)(关于cpu的三级缓冲可以参考这篇文章:小林coding 图解系统)。当 CPU 需要读取数据或指令时,首先在缓存中查找。若缓存命中,则快速获取数据;若缓存未命中,则从主内存读取,并将数据存入缓存。由于缓存容量有限,当新数据需要存入缓存时,会根据类似 LRU 的算法淘汰最近最少使用的数据。比如在 Ubuntu 系统上运行复杂的图像渲染程序,CPU 需要频繁读取图像数据和相关指令。随着程序的运行,缓存中的数据不断更新,那些近期较少被使用的图像数据和指令就会被淘汰,为新的数据腾出空间,确保 CPU 能够快速访问到最需要的数据,提高图像渲染的效率。

五、LRU 算法的优缺点

(一)优点

  1. 简单高效:LRU 算法的原理和实现相对简单,并且在许多场景下都能表现出良好的性能。它能够快速地淘汰最近最少使用的数据,保证缓存中始终保留最有价值的数据,从而提高系统的整体性能。
  2. 符合局部性原理:在计算机系统中,数据访问往往具有局部性特征,即最近访问过的数据在未来一段时间内再次被访问的概率较高。LRU 算法正好符合这一原理,通过优先保留最近使用的数据,能够有效地提高缓存命中率,减少数据的重复读取。

(二)缺点

  1. 无法准确预测未来访问模式:LRU 算法仅仅根据过去的数据访问历史来决定淘汰哪些数据,而不能准确预测未来的数据访问模式。在某些情况下,一次性遍历大量不重复数据(或者突然访问大量冷数据),可能会将真正的热点数据清空(缓存污染),导致命中率低。
  2. 实现成本较高:虽然 LRU 算法的基本思想简单,但要实现一个高效的 LRU 缓存,需要结合双向链表和哈希表等数据结构,这增加了代码的复杂性和实现成本。此外,在高并发环境下,还需要考虑数据结构的线程安全性,进一步增加了实现的难度,同时锁的加入会在高并发下消耗大量资源用于线程的同步等待。

六、c++实现LRU

        LRU 缓存置换算法是一种经典且实用的算法,在众多领域都有广泛的应用。通过深入理解其原理、数据结构选择、操作流程以及应用场景,我们可以更好地将其应用到实际项目中,提升系统的性能。接下来,我们将通过手撕代码的方式,详细展示如何实现一个 LRU 缓存。

代码参考自:https://github.com/youngyangyang04/KamaCache

接口类,对缓存置换算法设计统一接口实现隔离 :

#ifndef CACHEPOLICY_H
#define CACHEPOLICY_Htemplate<typename Key, typename Value>
class CachePolicy
{
public:virtual ~CachePolicy() {};virtual void put(Key key, Value value) = 0;virtual bool get(Key key,Value& value) = 0;virtual Value get(Key key) = 0;
};#endif

 节点类,LRU中的双向链表,每个节点保存缓存数据的键值对、访问次数和前后节点指针,同时封装类方法用于访问和修改私有成员变量

//前置声明LRU类,具体实现看下文
template<typename Key, typename Value> class LruCache;template<typename Key, typename Value>
class LruNode
{
private:Key key_;Value value_;size_t accessCount_;std::shared_ptr<LruNode<Key,Value>> next_;std::shared_ptr<LruNode<Key,Value>> prev_;public://初始化列表方法对类成员变量赋初值LruNode(Key key, Value value):key_(key),value_(value),accessCount_(0),next_(nullptr),prev_(nullptr) {}Key getKey() const {return key_;}//const声明方法不对成员变量修改Value getValue() const {return value_;}void setValue(const Value& value) {value_ = value;}size_t getAccexxCount() const {return accessCount_;}void incrementAccessCount() {++accessCount_;}//LRU类作为节点类友元,用于在LRU类中访问节点类的私有成员friend class LruCache<Key, Value>;
};

LRU类,其内部包含链表虚拟头节点(虚拟头节点的下一个节点就是最不经常使用节点)指针,虚拟尾节点(虚拟尾节点的上一个节点最近访问过的节点)指针。使用unordered_map作为哈希表(unordered_map底层是哈希表实现,不清楚哈希表的可以看我以前的文章c++ 手写STL篇(三)实现hashtable)。

template<typename Key, typename Value> 
class LruCache : public CachePolicy<Key,Value>
{
public:	using LruNodeType = LruNode<Key,Value>;using NodePtr = std::shared_ptr<LruNodeType>;using NodeMap = std::unordered_map<Key,NodePtr>;LruCache(int capacity): capacity_ (capacity){initializeList();}~LruCache() override = default;void put(Key key, Value value) override {if(capacity_ <= 0) return;std::lock_guard<std::mutex> lock(mutex_);//互斥锁,避免临界区竞争//先使用哈希表查找缓存中是否存在key,//存在则将节点取出,并插入链表尾部,//不存在则新建节点并插入链表尾部auto it = nodeMap_.find(key);if(it != nodeMap_.end()){updateExistingNode(it->second, value);return ;}addNewNode(key,value);}bool get(Key key,Value& value) override{std::lock_guard<std::mutex> lock(mutex_);
//先查找是否存在,存在则把节点移到链表尾部,不存在直接返回falseauto it = nodeMap_.find(key);if(it != nodeMap_.end()){moveToMostRecent(it->second);value = it->second->getValue();return true;}return false;}//get的重载Value get(Key key) override{Value value{};get(key,value);return value;}void remove(Key key){std::lock_guard<std::mutex> lock(mutex_);auto it = nodeMap_.find(key);if(it != nodeMap_.end()){removeNode(it->second);nodeMap_.erase(it);}}private://初始化就是构建虚拟头节点和虚拟尾节点,然后头尾相连void initializeList(){dummyHead_ = std::make_shared<LruNodeType>(Key(),Value());dummyTail_ = std::make_shared<LruNodeType>(Key(),Value());dummyHead_->next_ = dummyTail_;dummyTail_->prev_ = dummyHead_;}//修改节点值,然后移动到链表尾部void updateExistingNode(NodePtr node, const Value& value){node->setValue(value);moveToMostRecent(node);}//新增节点,这里会判断缓存容量是否已满,满了删除最不经常使用的节点(头部节点)void addNewNode(const Key& key, const Value& value){if(nodeMap_.size() >= capacity_){evictLeastNode();}NodePtr newNode = std::make_shared<LruNodeType>(key,value);insertNode(newNode);nodeMap_[key] = newNode;}//节点移动到链表尾部(删除-》插入)void moveToMostRecent(NodePtr node){removeNode(node);insertNode(node);}void removeNode(NodePtr node){node->prev_->next_ = node->next_;node->next_->prev_ = node->prev_;}void insertNode(NodePtr node){node->next_ = dummyTail_;node->prev_ = dummyTail_->prev_;dummyTail_->prev_->next_ = node;dummyTail_->prev_ = node;}//删除最不经常使用节点(头部节点)void evictLeastNode(){NodePtr leastRecent = dummyHead_->next_;removeNode(leastRecent);nodeMap_.erase(leastRecent->getKey());}private:int capacity_;//容量NodeMap nodeMap_;//哈希表NodePtr dummyHead_;//虚拟头节点,它的next指向真正的头节点(最不经常使用节点)NodePtr dummyTail_;//虚拟尾节点,它的prev指向真正的为尾节点(最近使用过的节点)std::mutex mutex_;//互斥锁
};

        通过以上代码不难看出,锁的粒度很大,这样的话高并发下会消耗大量资源用于线程同步等待,这个问题怎么解决呢?其实可以把一个大的缓存池分成几个小的缓存池,就好比多个人都要从一个仓库取货,但仓库每次只能进一个人,如果多个人要取货那就必须得排队等待,哪怕每个人要取的货物不同,既然这样那不如把一个大仓库分成若干个小仓库,每个人取得货物可能分布在不同仓库内,哪怕仍然有好几个人要的货物碰巧在同一个仓库,那队伍也不会排的像原先那么长。这样就实现了分流。

template<typename Key, typename Value>
class HashLruCaches
{
public:	//把lurcache进行分片,计算每一个小的cache容量大小HashLruCaches(size_t capacity, int sliceNum):	capacity_(capacity),sliceNum_(sliceNum > 0 ? sliceNum : std::thread::hardware_concurrency()){size_t sliceSize = std::ceil(capacity / static_cast<double>(sliceNum_));for(int i = 0; i < sliceNum_; i++){lruSliceCaches_.emplace_back(new LruCache<Key,Value>(sliceSize));}}//使用哈希函数计算要找的节点在哪一个cache中void put(Key key,Value value){size_t sliceIndex = Hash(key) % sliceNum_;return lruSliceCaches_[sliceIndex]->put(key, value);}bool get(Key key, Value& value){size_t sliceIndex = Hash(key) % sliceNum_;return lruSliceCaches_[sliceIndex]->get(key, value);}Value get(Key key){Value value{};memset(&value, 0, sizeof(Value));get(key, value);return value;}private:size_t Hah(Key key){std::hash<Key> hashFunc;return hashFunc(key);}private:size_t capacity_;int sliceNum_;std::vector<std::unique_ptr<LruCache<Key,Value>>> lruSliceCaches_;
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/78628.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/78628.shtml
英文地址,请注明出处:http://en.pswp.cn/web/78628.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【YOLO11改进】改进Conv、颈部网络STFEN、以及引入PIOU用于小目标检测!

改进后的整体网络架构 改进一:RFD模块(Conv) YOLOv11模型的跨步卷积下采样虽然快速聚合了局部特征,并且实现了较高的计算效率,但其固有的信息压缩机制会导致细粒度特征的不可逆丢失。针对特征保留与计算效率的平衡问题,本文采用RFD模块替换跨步卷积下采样模块。RFD模块通…

设计模式每日硬核训练 Day 18:备忘录模式(Memento Pattern)完整讲解与实战应用

&#x1f504; 回顾 Day 17&#xff1a;中介者模式小结 在 Day 17 中&#xff0c;我们学习了中介者模式&#xff08;Mediator Pattern&#xff09;&#xff1a; 用一个中介者集中管理对象之间的通信。降低对象之间的耦合&#xff0c;适用于聊天系统、GUI 控件联动、塔台调度等…

java单元测试代码

import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; import java.util.List;public class UserServiceTest {Testpublic void testSearchUserByTags() {// 模拟标签列表List<String> tagNameList List.of("tag1", "…

前端面经-VUE3篇(一)--vue3基础知识- 插值表达式、ref、reactive

目录 一、 插值表达式 1、插值表达式 ({{}}) 的本质与作用&#xff1a; 2、与 Vue 响应式系统关系&#xff1a; 二、指令 1、什么是 Vue 指令&#xff1f; 2、指令的分类 1、内置指令 ① 内容绑定&#xff1a;v-text 和 v-html ② 属性绑定&#xff1a;v-bind ③ 事件绑定…

矩阵置零(中等)

可以用两个标记数组分别记录每一行和每一列是否有零出现。 首先遍历该数组一次&#xff0c;如果某个元素为 0&#xff0c;那么就将该元素所在的行和列所对应标记数组的位置置为 true。然后再次遍历该数组&#xff0c;用标记数组更新原数组。 class Solution {public void set…

Android 实现一个隐私弹窗

效果图如下&#xff1a; 1. 设置同意、退出、点击用户协议、点击隐私协议的函数参数 2. 《用户协议》、《隐私政策》设置成可点击的&#xff0c;且颜色要区分出来 res/layout/dialog_privacy_policy.xml 文件 <?xml version"1.0" encoding"utf-8"?&…

TCP概念+模拟tcp服务器及客户端

目录 一、TCP基本概念 二、ser服务器代码 三、cil客户端代码 四、面试常问问题 4.1 TCP的可靠性怎么保证或怎么实现? 4.2 具体说一下滑动窗口 一、TCP基本概念 TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;是一种面向连接的、可…

Cocos Creator 自动图集资源 (Auto Atlas)使用注意事项

1、游戏打包时&#xff0c;自动图集设置选项中&#xff0c;默认会删除无关联的图片 2、自动图集设置中&#xff0c;就算勾除(Remove unused ImageAsset from the Bundle)的功能&#xff0c;无关联的图片也不会打包进入图集之中&#xff0c;会独立存在打包的游戏中。 3、使用自动…

PyTorch 2.0编译器技术深度解析:如何自动生成高性能CUDA代码

引言&#xff1a;编译革命的范式转移 PyTorch 2.0的torch.compile不仅是简单的即时编译器&#xff08;JIT&#xff09;&#xff0c;更标志着深度学习框架从‌解释执行‌到‌编译优化‌的范式跃迁。本文通过逆向工程编译过程&#xff0c;揭示PyTorch如何将动态图转换为高性能CU…

【AI面试准备】从0-1搭建人工智能模型自动化评估理论与测试,掌握测试数据集建立与优化,熟练数据处理和模型评测工作

面试要求&#xff1a;从0-1搭建人工智能模型自动化评估理论与测试&#xff0c;掌握测试数据集建立与优化&#xff0c;熟练数据处理和模型评测工作。 以下是针对从0-1搭建AI模型自动化评估体系的系统化知识总结&#xff0c;涵盖核心方法论、技术栈、高频考点及面试回答模板&…

【Linux应用】在PC的Linux环境下通过chroot运行ARM虚拟机镜像img文件(需要依赖qemu-aarch64、不需要重新安装iso)

【Linux应用】在PC的Linux环境下通过chroot运行ARM虚拟机镜像img文件&#xff08;需要依赖qemu-aarch64、不需要重新安装iso&#xff09; qemu提供了运行ARM虚拟机的方法 具体的操作方式就是建立一个硬盘img 然后通过iso安装到img 最后再运行img即可 这种方式教程很多 很简单 …

OpenCv实战笔记(1)在win11搭建opencv4.11.1 + qt5.15.2 + vs2019_x64开发环境

一. 准备工作 Visual Studio 2019&#xff08;安装时勾选 C 桌面开发 和 Windows 10 SDK&#xff09; CMake 3.20&#xff08;官网下载&#xff09; Qt 5.15.2&#xff08;下载 Qt Online Installer&#xff09;安装时勾选 MSVC 2019 64-bit 组件。 opencv 4.11.1 源码下载 git…

springboot+mysql+element-plus+vue完整实现汽车租赁系统

目录 一、项目介绍 二、项目截图 1.项目结构图 三、系统详细介绍 管理后台 1.登陆页 2.管理后台主页 3.汽车地点管理 4.汽车类别 5.汽车品牌 6.汽车信息 7.用户管理 8.举报管理 9.订单管理 10.轮播图管理 11.交互界面 12.图表管理 汽车租赁商城 1.首页 2.汽…

【算法笔记】动态规划基础(二):背包dp

目录 01背包例题状态表示状态计算初始化AC代码 完全背包例题状态表示状态计算初始化TLE代码 多重背包例题状态表示状态计算初始化AC代码 分组背包例题状态表示状态计算初始化AC代码 二维费用背包例题状态表示状态计算初始化AC代码 混合背包问题例题状态表示状态计算初始化TLE代…

Qt Quick Design 下载社区版

官方地址&#xff1a;Qt Design Studio - UI Development Tool for Applications & Devices 社区版只能用于开源软件的开发 按图所示下载或直接跳转到下载页面&#xff1a;Download Qt OSS: Get Qt Online Installerhttps://www.qt.io/download-qt-installer-oss 选Try …

深入理解CSS盒子模型

一、盒子模型的核心概念 CSS盒子模型&#xff08;Box Model&#xff09;是网页布局的基石&#xff0c;每个HTML元素都可以看作一个矩形盒子&#xff0c;由四个同心区域构成&#xff1a; 内容区&#xff08;Content&#xff09; 内边距&#xff08;Padding&#xff09; 边框&a…

Python项目源码57:数据格式转换工具1.0(csv+json+excel+sqlite3)

1.智能路径处理&#xff1a;自动识别并修正文件扩展名&#xff0c;根据转换类型自动建议目标路径&#xff0c;实时路径格式验证&#xff0c;自动补全缺失的文件扩展名。 2.增强型预览功能&#xff1a;使用pandastable库实现表格预览&#xff0c;第三方模块自己安装一下&#x…

数据库MySQL学习——day9(聚合函数与分组数据)

文章目录 1. 聚合函数1.1 COUNT() 函数1.2 SUM() 函数1.3 AVG() 函数1.4 MIN() 函数1.5 MAX() 函数 2. GROUP BY 子句2.1 使用 GROUP BY 进行数据分组2.2 结合聚合函数 3. HAVING 子句3.1 使用 HAVING 过滤分组数据3.2 HAVING 和 WHERE 的区别 4. 实践任务4.1 创建一个销售表4.…

数据管理能力成熟度评估模型(DCMM)全面解析:标准深度剖析与实践创新

文章目录 一、DCMM模型的战略价值与理论基础1.1 DCMM的本质与战略定位1.2 DCMM的理论基础与创新点 二、DCMM模型的系统解构与逻辑分析2.1 八大能力域的有机关联与系统架构2.2 五级成熟度模型的内在逻辑与演进规律 三、DCMM八大能力域的深度解析与实践创新3.1 数据战略&#xff…

Docker搜索镜像报错

科学上网最方便。。。。 主要是镜像的问题 尝试一&#xff1a; 报错处理 Error response from daemon: Get https://index.docker.io/v1/search?qmysql&n25: dial tcp 31.13.84.2:443: i/o timeout Error response from daemon: Get https://index.docker.io/v1/se…