LRU-K 是一种缓存淘汰算法,旨在改进传统的LRU(Least Recently Used,最近最少使用)算法的性能。将其中高频的数据达到K次访问移入到另一个队列进行保护。

算法思想

  • LRU-K中的“K”代表最近使用的次数。因此,LRU可以认为是LRU-1的特例。
  • LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题。其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。

工作原理

  • LRU-K需要维护两个队列:历史队列和缓存队列。

    1. 普通队列:保存着每次访问的页面。当页面访问次数达到K次时,该页面从历史队列中移除,并添加到K次队列中。
    2. K次队列:保存已经访问K次的页面。当缓存队列满了之后,需要淘汰页面时,会淘汰最后一个页面,即“倒数第K次访问离现在最久”的那个页面。
  • 详细说明:
    1. 页面第一次被访问时,添加到普通队列中。
    2. 普通队列中的页面满了,根据一定的缓存策略(如FIFO、LRU、LFU)进行淘汰。
    3. 当历史队列中的某个页面第K次访问时,该页面从历史队列中移除,并添加到K次队列中。
    4. K次队列中的页面再次被访问时,会重新排序。

优缺点

  • 优点

    1. LRU-K降低了“缓存污染”带来的问题,因为只有当页面被访问K次后才会被加入缓存队列。
    2. LRU-K的命中率通常比LRU要高。
  • 缺点
    1. LRU-K需要维护一个普通队列,因此内存消耗会比LRU多。
    2. LRU-K需要基于次数进行排序(可以需要淘汰时再排序,也可以即时排序),因此CPU消耗比LRU要高。
    3. 当K值较大时(如LRU-3或更大的K值),虽然命中率会更高,但适应性较差,需要大量的数据访问才能将历史访问记录清除掉。

实际应用

  • 在实际应用中,LRU-2通常被认为是综合各种因素后最优的选择。

综上所述,LRU-K通过引入“最近使用过K次”的判断标准,有效地解决了LRU算法中的“缓存污染”问题,提高了缓存的命中率。然而,它也需要更多的内存和CPU资源来维护历史队列和进行排序操作。

结构设计

与Lru的结构类似,K与V均用指针方式保存,避免在使用过程中出现Copy或者Clone的可能,提高性能。

注:该方法用了指针会相应的出现许多unsafe的代码,因为在Rsut中,访问指针都被认为是unsafe。我们也可以使用数组坐标模拟指针的方式来模拟。

节点设计

相对普通的Lru节点,我们需要额外存储次数数据。

/// LruK节点数据
struct LruKEntry<K, V> {
pub key: mem::MaybeUninit<K>,
pub val: mem::MaybeUninit<V>,
pub times: usize,
pub prev: *mut LruKEntry<K, V>,
pub next: *mut LruKEntry<K, V>,
}

类设计

由于有K次的列表,所以需要维护两个列表,在空间占用上会比Lru多一些,主要多一个字段访问次数的维护

pub struct LruKCache<K, V, S> {
map: HashMap<KeyRef<K>, NonNull<LruKEntry<K, V>>, S>,
cap: usize,
/// 触发K次数,默认为2
times: usize,
/// K次的队列
head_times: *mut LruKEntry<K, V>,
tail_times: *mut LruKEntry<K, V>,
/// 普通队列
head: *mut LruKEntry<K, V>,
tail: *mut LruKEntry<K, V>,
/// 普通队列的长度
lru_count: usize,
}

首先初始化对象,初始化map及空的双向链表:

impl<K, V, S> LruCache<K, V, S> {
/// 提供hash函数
pub fn with_hasher(cap: usize, times: usize, hash_builder: S) -> LruKCache<K, V, S> {
let cap = cap.max(1);
let map = HashMap::with_capacity_and_hasher(cap, hash_builder);
let head = Box::into_raw(Box::new(LruKEntry::new_empty()));
let tail = Box::into_raw(Box::new(LruKEntry::new_empty()));
unsafe {
(*head).next = tail;
(*tail).prev = head;
}
let head_times = Box::into_raw(Box::new(LruKEntry::new_empty()));
let tail_times = Box::into_raw(Box::new(LruKEntry::new_empty()));
unsafe {
(*head_times).next = tail_times;
(*tail_times).prev = head_times;
}
Self {
map,
cap,
times,
head_times,
tail_times,
head,
tail,
lru_count: 0,
}
}
}

元素插入及删除

插入对象,分已在缓存内和不在缓存内:

pub fn capture_insert(&mut self, k: K, mut v: V) -> Option<(K, V, bool)> {
let key = KeyRef::new(&k);
match self.map.get_mut(&key) {
Some(entry) => {
let entry_ptr = entry.as_ptr();
unsafe {
mem::swap(&mut *(*entry_ptr).val.as_mut_ptr(), &mut v);
}
self.detach(entry_ptr);
self.attach(entry_ptr); Some((k, v, true))
}
None => {
let (val, entry) = self.replace_or_create_node(k, v);
let entry_ptr = entry.as_ptr();
self.attach(entry_ptr);
unsafe {
self.map
.insert(KeyRef::new((*entry_ptr).key.as_ptr()), entry);
}
val.map(|(k, v)| (k, v, false))
}
}
} pub fn remove<Q>(&mut self, k: &Q) -> Option<(K, V)>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
match self.map.remove(KeyWrapper::from_ref(k)) {
Some(l) => unsafe {
self.detach(l.as_ptr());
let node = *Box::from_raw(l.as_ptr());
Some((node.key.assume_init(), node.val.assume_init()))
},
None => None,
}
}

与Lru的操作方式类似,但是主要集中在attachdetach因为有两个队列,需要正确的附着在正确的队列之上。

attach
/// 加到队列中
fn attach(&mut self, entry: *mut LruKEntry<K, V>) {
unsafe {
(*entry).times = (*entry).times.saturating_add(1);
if (*entry).times < self.times {
self.lru_count += 1;
(*entry).next = (*self.head).next;
(*(*entry).next).prev = entry;
(*entry).prev = self.head;
(*self.head).next = entry;
} else {
(*entry).next = (*self.head_times).next;
(*(*entry).next).prev = entry;
(*entry).prev = self.head_times;
(*self.head_times).next = entry;
}
}
}

在加入到队列的时候,需将访问次数+1,然后判断是否达到K次的次数,如果达到将其加入到head_times队列中。

其中使用了saturating_add,这里说个Rust与其它语言的差别。

因为在Rust中不像c语言,如果在c语言中,定义一个uchar类型

uchar times = 255;
times += 1; //此时times为0,不会有任何异常

但是在Rust中

let mut times: u8 = 255;
times = times.overflowing_add(1); // 此时times为0,因为上溢出了
times = times.saturating_add(1); // 此时times为255,因为达到了最大值
times += 1; // 此时将会发生panic

此时这函数的效率基本上等同于Lru的,相对仅仅是多维护times字段lru_count字段

detach
fn detach(&mut self, entry: *mut LruKEntry<K, V>) {
unsafe {
(*(*entry).prev).next = (*entry).next;
(*(*entry).next).prev = (*entry).prev; if (*entry).times < self.times {
self.lru_count -= 1;
}
}
}

与Lru中的类似,仅仅如果次数在k次以下的时候维护lru_count,效率基本一致。

replace_or_create_node
fn replace_or_create_node(&mut self, k: K, v: V) -> (Option<(K, V)>, NonNull<LruKEntry<K, V>>) {
if self.len() == self.cap {
let old_key = if self.lru_count > 0 {
KeyRef {
k: unsafe { &(*(*(*self.tail).prev).key.as_ptr()) },
}
} else {
KeyRef {
k: unsafe { &(*(*(*self.tail_times).prev).key.as_ptr()) },
}
};
let old_node = self.map.remove(&old_key).unwrap();
let node_ptr: *mut LruKEntry<K, V> = old_node.as_ptr();
unsafe {
(*node_ptr).times = 0;
}
let replaced = unsafe {
(
mem::replace(&mut (*node_ptr).key, mem::MaybeUninit::new(k)).assume_init(),
mem::replace(&mut (*node_ptr).val, mem::MaybeUninit::new(v)).assume_init(),
)
}; self.detach(node_ptr); (Some(replaced), old_node)
} else {
(None, unsafe {
NonNull::new_unchecked(Box::into_raw(Box::new(LruKEntry::new(k, v))))
})
}

淘汰数据,优先淘汰普通队列的数据,如果普通队列为空,将进入淘汰K次队列。区别就是在于淘汰时多选择一次数据。效率上也基本上可以忽略不计。

其它操作

  • pop移除栈顶上的数据,最近使用的
  • pop_last移除栈尾上的数据,最久未被使用的
  • contains_key判断是否包含key值
  • raw_get直接获取key的值,不会触发双向链表的维护
  • get获取key的值,并维护双向链表
  • get_mut获取key的值,并可以根据需要改变val的值
  • retain 根据函数保留符合条件的元素
  • get_or_insert_default 获取或者插入默认参数
  • get_or_insert_mut 获取或者插入对象,可变数据

如何使用

在cargo.toml中添加

[dependencies]
algorithm = "0.1"
示例
use algorithm::LruKCache;
fn main() {
let mut lru = LruKCache::with_times(3, 3);
lru.insert("this", "lru");
for _ in 0..3 {
let _ = lru.get("this");
}
lru.insert("hello", "algorithm");
lru.insert("auth", "tickbh");
assert!(lru.len() == 3);
lru.insert("auth1", "tickbh");
assert_eq!(lru.get("this"), Some(&"lru"));
assert_eq!(lru.get("hello"), None);
assert!(lru.len() == 3);
}

完整项目地址

https://github.com/tickbh/algorithm-rs

结语

Lru-k与lru的区别在于多维护一个队列,及每个元素多维护一个次数选项,对于性能的影响不大,仅仅多耗一点cpu,但是可以相应的提高命中率,下一章将介绍LFU按频次的淘汰机制。

Lru-k在Rust中的实现及源码解析的更多相关文章

  1. Django框架rest_framework中APIView的as_view()源码解析、认证、权限、频率控制

    在上篇我们对Django原生View源码进行了局部解析:https://www.cnblogs.com/dongxixi/p/11130976.html 在前后端分离项目中前面我们也提到了各种认证需要 ...

  2. 6.2 dubbo在spring中自定义xml标签源码解析

    在6.1 如何在spring中自定义xml标签中我们看到了在spring中自定义xml标签的方式.dubbo也是这样来实现的. 一 META_INF/dubbo.xsd 比较长,只列出<dubb ...

  3. MapReduce中一次reduce方法的调用中key的值不断变化分析及源码解析

    摘要:mapreduce中执行reduce(KEYIN key, Iterable<VALUEIN> values, Context context),调用一次reduce方法,迭代val ...

  4. Netty中NioEventLoopGroup的创建源码分析

    NioEventLoopGroup的无参构造: public NioEventLoopGroup() { this(0); } 调用了单参的构造: public NioEventLoopGroup(i ...

  5. JDK中String类的源码分析(二)

    1.startsWith(String prefix, int toffset)方法 包括startsWith(*),endsWith(*)方法,都是调用上述一个方法 public boolean s ...

  6. Java中的容器(集合)之ArrayList源码解析

    1.ArrayList源码解析 源码解析: 如下源码来自JDK8(如需查看ArrayList扩容源码解析请跳转至<Java中的容器(集合)>第十条):. package java.util ...

  7. Java中的容器(集合)之HashMap源码解析

    1.HashMap源码解析(JDK8) 基础原理: 对比上一篇<Java中的容器(集合)之ArrayList源码解析>而言,本篇只解析HashMap常用的核心方法的源码. HashMap是 ...

  8. Springboot中mybatis执行逻辑源码分析

    Springboot中mybatis执行逻辑源码分析 在上一篇springboot整合mybatis源码分析已经讲了我们的Mapper接口,userMapper是通过MapperProxy实现的一个动 ...

  9. 在Xcode中使用Git进行源码版本控制

    http://www.cocoachina.com/ios/20140524/8536.html 资讯 论坛 代码 工具 招聘 CVP 外快 博客new 登录| 注册   iOS开发 Swift Ap ...

  10. Apache Spark源码走读之23 -- Spark MLLib中拟牛顿法L-BFGS的源码实现

    欢迎转载,转载请注明出处,徽沪一郎. 概要 本文就拟牛顿法L-BFGS的由来做一个简要的回顾,然后就其在spark mllib中的实现进行源码走读. 拟牛顿法 数学原理 代码实现 L-BFGS算法中使 ...

随机推荐

  1. [FAQ] VScode 用户代码片段设置

      以PHP为示例,配置如下: { "dpe": { "prefix": "dpe", "body": [ " ...

  2. [Py] Python 接口数据用 pandas 高效写入 csv

    通过 pandas 把 dict 数据封装,调用接口方法写入 csv 文件. import pandas as pd data = [{"name": "a"} ...

  3. [Gin] 单文件极简 HTTP Server 流程分析 ( gin-gonic/gin )

    /** * example.go * * @link https://cnblogs.com/farwish */package main import "github.com/gin-go ...

  4. [FAQ] web3js, Error: [number-to-bn] while converting number 0.1 to BN.js instance, error: invalid number value

    我们在调用合约方法时,都可以传一些参数的,比如转账金额 value. value 的单位是 wei,这是一个很小的单位,所以一般数值很大. 如果误把 ether 当成 wei 传参,就会报标题中的错误 ...

  5. 2024 年最值得推荐的 7 个 Vue3 组件库

    你好,我是 Kagol. Vue 是一款易学易用,性能出色,适用场景丰富的渐进式 JavaScript 框架,深受广大开发者的喜爱,Vue3 更是推出了 Composition API,让逻辑复用更友 ...

  6. WPF 警惕使用 Dispatcher.InvokeShutdown 方法退出应用 将不触发 Application.Exit 事件

    这是一个比较让人困惑的一个点,我一直都以为 Dispatcher.InvokeShutdown 和 Application.Current.Shutdown 是完全等价的.但是后面发现了其实这两者还是 ...

  7. VisualStudio 如何快速添加一个 Git Tag 推送

    在 VisualStudio 的团队管理功能,提供了方便的添加 Tag 的方法,可以新建一个 Tag 添加 Tag 信息,同时推送某个特定的 Tag 到服务器.配合推 Tag 打包 NuGet 的方法 ...

  8. 如何在 Linux 上部署 RabbitMQ

    如何在 Linux 上部署 RabbitMQ 目录 如何在 Linux 上部署 RabbitMQ 安装 Erlang 从预构建的二进制包安装 从源代码编译 Erlang RabbitMQ 的安装 使用 ...

  9. CMDB开发(一)

    一.CMDB前戏 # 项目开发流程 1.需求分析 产品经理 开发人员 客户等三方会议 2.架构设计 框架的选择 语言选择 数据库选择 3.分组开发 小组成员各自开发各自的功能(可能也会有交集) 4. ...

  10. vue应用el-tabel封装

    <template> <div class="table"> <el-table :data="tableList" style= ...