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

LRU缓存

        有人从网络读数据,有人从磁盘读数据,机智的人懂得合理利用缓存加速数据的读取效率,提升程序的性能,搏得上司的赏识,赢得白富美的青睐,进一步走向人生巅峰~

LRU假说

        LRU缓存(Least Recently Used Cache)即最近最少使用缓存算法,是一种常用的缓存淘汰策略,它基于这样一个假设:

如果数据最近被访问过,那么它在未来被访问的可能性也更高。

        因此,当缓存空间不足时,LRU缓存会优先移除最长时间未被访问的数据项。        

LRU是怎么干活的

新访问的数据添加到缓存

        当一个数据项被访问时,它会被添加到缓存中。如果该数据项已经在缓存中,它会被更新,并且移动到缓存的最前面,表示最近被访问过。

缓存满时移除最老的数据

        如果缓存已满(达到预设的容量限制),最久未被访问的数据项(位于缓存的最后面)会被移除,以便为新的数据项腾出空间。

维护访问顺序

        缓存需要维护数据项的访问顺序,以便快速确定哪些数据项是最近被访问的,哪些是最久未被访问的。

为了有效地实现LRU缓存,通常需要以下两种数据结构:
        双向链表:用于维护数据项的访问顺序。最近访问的数据项位于链表一头,最久未访问的数据项位于链表另一头。当数据项被访问时,它会被移动到链表最近访问那一头。当需要移除数据项时,最久未访问的末尾数据项会被移除。
        哈希表:用于存储键和指向双向链表中相应节点的指针,以便快速定位缓存中的数据项。这样可以在O(1)时间复杂度内访问缓存项。

LRU的简单示例

如下是一个简单的LRU实现

#include <iostream>
#include <list>
#include <string>
#include <unordered_map>
#include <vector>using namespace std;template <typename K, typename V>
class LRUCache {
public:LRUCache(int capacity) {cap = capacity;}V get(const K& key) {auto it = hash.find(key);if (it == hash.end()) {return V();}auto val = it->second->second;put(key, val);return val;}void put(const K& key, const V& value) {auto it = hash.find(key);if (it == hash.end()) {if (hash.size() >= cap) {auto d_it = data_list.begin();auto h_it = d_it->first;data_list.erase(d_it);hash.erase(h_it);}} else {auto d_it = it->second;data_list.erase(d_it);hash.erase(it);}data_list.emplace_back(key, value);hash[key] = --data_list.end();}private:int cap;list<pair<K, V> > data_list;using LIST_IT = typename list<pair<K, V> >::iterator;unordered_map<K, LIST_IT> hash;
};int main() {LRUCache<int, int> lru(2);vector<pair<string, vector<int> > > test_case = {{"put", {1, 1}},{"put", {2, 2}},{"get", {1}},{"put", {3, 3}},{"get", {2}},{"put", {4, 4}},{"get", {1}},{"get", {3}},{"get", {4}},};for (const auto& [opt, param] : test_case) {if (opt == "get") {auto val = lru.get(param.front());cout << val << endl;} else {lru.put(param.front(), param.back());}}return 0;
}

运行测试用例可以得到如下结果:

code % g++ lru.cpp -std=c++17
code % ./a.out       
1
0
0
3
4
code % 

        如上,实现一个LRU的代码量并不算多,并且简单易懂,性能也很不错,毕竟时间复杂度为O(1)。但LRU也有其缺点,例如它没有考虑数据的访问频率。这可能会导致一些不经常使用的数据被缓存,而一些经常使用的数据被淘汰

LRU的改进-LFU

        LFU(Least Frequently Used),即最少使用频率缓存,考虑到访问频率,而不是最近一次访问时间。其可以与LRU结合,形成其他变种,以更好地适应不同的数据访问模式。

LFU的简单示例

        例如,可以通过给LRU缓存数据项加上访问频率,当缓存满需要淘汰时,取尾部的数据选一个访问频次最低的来淘汰

#include <iostream>
#include <list>
#include <string>
#include <unordered_map>
#include <vector>using namespace std;template <typename K, typename V>
class LRUCache {
public:LRUCache(int capacity) {cap = capacity;}V get(const K& key) {auto it = hash.find(key);if (it == hash.end()) {return V();}const auto& data_tuple = *(it->second);auto val = std::get<1>(data_tuple);auto cnt = std::get<2>(data_tuple);put(key, val, cnt + 1);return val;}void put(const K& key, const V& value, int cnt = 1) {auto it = hash.find(key);if (it == hash.end()) {if (hash.size() >= cap) {remove_one_elem();}} else {auto d_it = it->second;data_list.erase(d_it);hash.erase(it);}data_list.emplace_back(key, value, cnt);hash[key] = --data_list.end();}private:void remove_one_elem() {auto need_rm = data_list.begin();auto it = need_rm;for (int i = 1; i < 3 && it != data_list.end(); ++i, ++it) {if (std::get<2>(*it) < std::get<2>(*need_rm)) {need_rm = it;}}hash.erase(std::get<0>(*need_rm));data_list.erase(need_rm);}private:int cap;list<tuple<K, V, int> > data_list;using LIST_IT = typename list<tuple<K, V, int> >::iterator;unordered_map<K, LIST_IT> hash;
};int main() {LRUCache<int, int> lru(2);vector<pair<string, vector<int> > > test_case = {{"put", {1, 1}},{"put", {2, 2}},{"get", {1}},{"put", {3, 3}},{"get", {2}},{"put", {4, 4}},{"get", {1}},{"get", {3}},{"get", {4}},};for (const auto& [opt, param] : test_case) {if (opt == "get") {auto val = lru.get(param.front());cout << val << endl;} else {lru.put(param.front(), param.back());}}return 0;
}

运行测试用例可以得到如下结果:

code % g++ lfu.cpp -std=c++17
code % ./a.out       
1
0
1
0
4
code % 

        与LRU示例的差异点在于,当缓存满时,LFU为从最近未使用的一头,挑选一个访问频次最小的元素进行淘汰。值得注意的是,挑选最少频次并不需要遍历所有的数据,而是针对具体的业务场景,设定一个合适的值即可。

        虽然LRU开销很小,时间复杂度又是O(1),但毕竟每次访问都需要调整链表,对于一些性能要求高的场景,负担还是有点重的,实际的使用场景中,又会根据具体的业务场景,做一些响应的改变。

衍生一下

Clock算法

        Clock算法是一种用于页面置换的缓存淘汰策略,它是LRU算法的一种近似实现,旨在降低实现LRU的开销。Clock算法有时也被称为Second-Chance算法,因为它给了每个页面一个“第二次机会”来避免被置换。

Clock是怎么干活的

        Clock算法维护一个循环链表:所有的页面都被组织成一个循环链表(或称为时钟结构),每个页面都有一个关联的访问位(通常是一个标志位),用于表示该页面自上次检查以来是否被访问过。

        使用指针指向链表中的一个页面:有一个指针(称为时钟指针)指向循环链表中的某个页面。

        维护访问位:当一个页面被访问时,其访问位被设置为1,表示该页面最近被使用过。

        缓存满时检查访问位:当有新需要加载到缓存中,但缓存已满,算法会检查当前时钟指针指向的页面的访问位。如果访问位为1,则将其清零(给予第二次机会),并将时钟指针移动到下一个页面。如果访问位为0,则选择该页面进行置换

        Clock算法的优点是实现简单,开销较低,因为它不需要像真正的LRU算法那样在每次页面访问时都对链表进行调整。它只需要在页面置换时检查和更新访问位。这使得Clock算法特别适合于大规模的缓存系统,如操作系统的页面缓存。

        Clock算法的缺点是它不是完全精确的LRU实现,因为它可能会保留一些不太常用的页面(如果它们在时钟指针到达之前刚好被访问过)。然而,对于许多实际应用来说,Clock算法提供了一个很好的折中方案,既保留了LRU的大部分优点,又显著降低了实现的复杂性和开销。

相关文章:

  • 图灵日记之java奇妙历险记--抽象类和接口
  • Redis核心技术与实战【学习笔记】 - 31.番外篇:Redis客户端如何与服务器端交换命令和数据
  • k8s 部署java应用 基于ingress+jar包
  • 【iOS ARKit】3D 人体姿态估计
  • 三丰云免费云服务器测评
  • Kubernetes基础(十五)-k8s网络通信
  • 《Python 网络爬虫简易速速上手小册》第1章:Python 网络爬虫基础(2024 最新版)
  • 在windows的控制台实现贪吃蛇小游戏
  • Qt之漂亮的地球
  • Android编程权威指南(第四版)- 第 4 章 UI状态的保存与恢复
  • 记录学习--java abstract与interface使用区别
  • Vivado-IP核
  • vue3+vite+ts 配置commit强制码提交规范配置 commitlint
  • React+Antd+tree实现树多选功能(选中项受控+支持模糊检索)
  • c++之说_10|自定义类型 union 联合体
  • Google 是如何开发 Web 框架的
  • 《剑指offer》分解让复杂问题更简单
  • 【跃迁之路】【519天】程序员高效学习方法论探索系列(实验阶段276-2018.07.09)...
  • crontab执行失败的多种原因
  • flutter的key在widget list的作用以及必要性
  • Git同步原始仓库到Fork仓库中
  • Idea+maven+scala构建包并在spark on yarn 运行
  • in typeof instanceof ===这些运算符有什么作用
  • Just for fun——迅速写完快速排序
  • node-glob通配符
  • SegmentFault 技术周刊 Vol.27 - Git 学习宝典:程序员走江湖必备
  • Spring Cloud中负载均衡器概览
  • Vue2.0 实现互斥
  • webpack项目中使用grunt监听文件变动自动打包编译
  • 初探 Vue 生命周期和钩子函数
  • 订阅Forge Viewer所有的事件
  • 买一台 iPhone X,还是创建一家未来的独角兽?
  • 前端代码风格自动化系列(二)之Commitlint
  • 前端性能优化--懒加载和预加载
  • 手机app有了短信验证码还有没必要有图片验证码?
  • 我这样减少了26.5M Java内存!
  • 协程
  • 在Docker Swarm上部署Apache Storm:第1部分
  • ​如何防止网络攻击?
  • ![CDATA[ ]] 是什么东东
  • # 数论-逆元
  • #我与Java虚拟机的故事#连载15:完整阅读的第一本技术书籍
  • (C语言)strcpy与strcpy详解,与模拟实现
  • (Python第六天)文件处理
  • (编程语言界的丐帮 C#).NET MD5 HASH 哈希 加密 与JAVA 互通
  • (三)Honghu Cloud云架构一定时调度平台
  • (提供数据集下载)基于大语言模型LangChain与ChatGLM3-6B本地知识库调优:数据集优化、参数调整、Prompt提示词优化实战
  • (转) Android中ViewStub组件使用
  • (转)Linux下编译安装log4cxx
  • (转)linux下的时间函数使用
  • (转)Spring4.2.5+Hibernate4.3.11+Struts1.3.8集成方案一
  • (转)淘淘商城系列——使用Spring来管理Redis单机版和集群版
  • (转载)Google Chrome调试JS
  • (转载)OpenStack Hacker养成指南
  • .net core 控制台应用程序读取配置文件app.config