Lru在Rust中的实现, 源码解析
LRU(Least Recently Used)是一种常用的页面置换算法,其核心思想是选择最近最久未使用的页面予以淘汰。
LRU算法原理
基本思想:LRU算法基于一个假设,即如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很低。因此,当缓存空间不足时,算法会选择最久未使用的数据进行淘汰。
实现方式:LRU算法通常通过维护一个队列或链表来实现。当访问一个页面时,如果该页面已经在队列中,则将其移动到队列的头部(最近使用);如果该页面不在队列中,则将其添加到队列的头部,并检查队列长度是否超过预设的阈值。如果队列长度超过阈值,则淘汰队列尾部的页面(最久未使用)。
LRU算法的优缺点
- 优点:
- LRU算法能够利用时间局部性原理,保留最近使用过的页面,提高缓存命中率。
- 算法简单,易于实现。
- 缺点:
- 需要维护一个队列或数组,占用额外的空间。
- 当页面访问模式具有循环周期时,LRU算法可能会淘汰掉正在使用的页面,导致缓存命中率下降。
- 对于随机访问的页面输入序列,LRU算法的表现可能不如其他算法。
结构设计
在Lru的结构中,我们要避免key或者val的拷贝。
因为key此时需要在双向列表中保存也需要在HashMap中保存,所以我们要以下方案:
Rc<K>引用计数
通过引用计数来控制生命周期
优点:不用处理不安全的代码
缺点:因为Val可能在遍历中被更改,所以不能存储在双向列表里,取得值的时候需要进行一次Hash
*mut K 裸指针
通过unsafe编码来实现
优点:在双向列表及HashMap中均存储一份数值,遍历或者根据key取值均只需一次操作
缺点:需要引入ptr,即用指针的方式来进行生命周期管理
节点设计
此时我们用的是裸指针的方式,让我们先来定义节点数据,数据将存储在该节点里面,key及val的生命周期随节点管理,在删除的时候需同时在列表及在HashMap中进行删除
/// Lru节点数据
struct LruEntry<K, V> {
/// 头部节点及尾部结点均未初始化值
pub key: mem::MaybeUninit<K>,
/// 头部节点及尾部结点均未初始化值
pub val: mem::MaybeUninit<V>,
pub prev: *mut LruEntry<K, V>,
pub next: *mut LruEntry<K, V>,
}
类设计
接下来需要设计LruCache结构,将由一个map存储数据结构,一个双向链表存储访问优先级,cap表示缓存的容量。
pub struct LruCache<K, V, S> {
/// 存储数据结构
map: HashMap<KeyRef<K>, NonNull<LruEntry<K, V>>, S>,
/// 缓存的总容量
cap: usize,
/// 双向列表的头
head: *mut LruEntry<K, V>,
/// 双向列表的尾
tail: *mut LruEntry<K, V>,
}
其中KeyRef仅仅只是表示裸指针的一层包装,方便重新实现Hash,Eq等trait
#[derive(Clone)]
struct KeyRef<K> {
pub k: *const K,
}
首先初始化对象,初始化map及空的双向链表:
impl<K, V, S> LruCache<K, V, S> {
/// 提供hash函数
pub fn with_hasher(cap: usize, hash_builder: S) -> LruCache<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(LruEntry::new_empty()));
let tail = Box::into_raw(Box::new(LruEntry::new_empty()));
unsafe {
(*head).next = tail;
(*tail).prev = head;
}
Self {
map,
cap,
head,
tail,
}
}
}
元素插入
插入对象,分已在缓存内和不在缓存内:
pub fn capture_insert(&mut self, k: K, mut v: V) -> Option<(K, V)> {
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))
}
None => {
let (_, 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);
}
None
}
}
}
存在该元素时,将进行替换
unsafe {
mem::swap(&mut *(*entry_ptr).val.as_mut_ptr(), &mut v);
}
并且重新维护访问队列,需要detach然后重新attach使其在队列的最前面,保证最近访问最晚淘汰,从而实现Lru。
如果元素不存在,那么将判断是否缓存队列为满,如果满了将要淘汰的数据进行替换,如果未满创建新的元素,即replace_or_create_node。
元素删除
在将元素删除时,需要维护好我们的队列,防止出现队列错误及野指针等
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,
}
}
这里因为移除时,仅仅需要一个可以转化成K的引用即可以,并不需要严格的K,利于编程。例如:
let mut map = LruCache::new(2);
map.insert("aaaa".to_string(), "bbb");
map.remove("aaaa");
assert!(map.len() == 0);
在此处我们就不需要严格的构建String对象。由于map中的key我们用的是KeyRef,在这里,我们构建一个KeyWrapper进行转化。
// 确保新类型与其内部类型的内存布局完全相同
#[repr(transparent)]
struct KeyWrapper<Q: ?Sized>(Q);
impl<K, Q> Borrow<KeyWrapper<Q>> for KeyRef<K>
where
K: Borrow<Q>,
Q: ?Sized,
{
fn borrow(&self) -> &KeyWrapper<Q> {
let key = unsafe { &*self.k }.borrow();
KeyWrapper::from_ref(key)
}
}
如果移除成功,那么将从双向链表中同步移除,并且将指针中的值重新变成Rust中的对象,以便可以及时被drop,避免内存泄漏。
self.detach(l.as_ptr());
let node = *Box::from_raw(l.as_ptr());
Some((node.key.assume_init(), node.val.assume_init()))
其它操作
pop移除栈顶上的数据,最近使用的pop_last移除栈尾上的数据,最久未被使用的contains_key判断是否包含key值raw_get直接获取key的值,不会触发双向链表的维护get获取key的值,并维护双向链表get_mut获取key的值,并可以根据需要改变val的值retain根据函数保留符合条件的元素
如何使用
在cargo.toml中添加
[dependencies]
algorithm = "0.1"
示例
use algorithm::LruCache;
fn main() {
let mut lru = LruCache::new(3);
lru.insert("now", "ok");
lru.insert("hello", "algorithm");
lru.insert("this", "lru");
lru.insert("auth", "tickbh");
assert!(lru.len() == 3);
assert_eq!(lru.get("hello"), Some(&"algorithm"));
assert_eq!(lru.get("this"), Some(&"lru"));
assert_eq!(lru.get("now"), None);
}
完整项目地址
https://github.com/tickbh/algorithm-rs
结语
Lru在缓存场景中也是极其重要的一环,但是普通的Lru容易将热点数据进行移除,如果短时间内大量的数据进入则会将需要缓存的数据全部清空,后续将介绍改进算法Lru-k及Lfu算法。
Lru在Rust中的实现, 源码解析的更多相关文章
- Scala 深入浅出实战经典 第65讲:Scala中隐式转换内幕揭秘、最佳实践及其在Spark中的应用源码解析
王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...
- Scala 深入浅出实战经典 第61讲:Scala中隐式参数与隐式转换的联合使用实战详解及其在Spark中的应用源码解析
王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载: 百度云盘:http://pan.baidu.com/s/1c0noOt ...
- Scala 深入浅出实战经典 第60讲:Scala中隐式参数实战详解以及在Spark中的应用源码解析
王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...
- Scala 深入浅出实战经典 第48讲:Scala类型约束代码实战及其在Spark中的应用源码解析
王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-64讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...
- 解析jQuery中extend方法--源码解析以及递归的过程《二》
源码解析 在解析代码之前,首先要了解extend函数要解决什么问题,以及传入不同的参数,会达到怎样的效果.extend函数内部处理传入的不同参数,返回处理后的对象. extend函数用来扩展对象,增加 ...
- .Net Core中的配置文件源码解析
一.配置简述 之前在.Net Framework平台开发时,一般配置文件都是xml格式的Web.config,而需要配置其他格式的文件就需要自己去读取内容,加载配置了..而Net Core支持从命令行 ...
- flask 中 session的源码解析
1.首先请求上下文和应用上下文中已经知道session是一个LocalProxy()对象 2.然后需要了解整个请求流程, 3.客户端的请求进来时,会调用app.wsgi_app(),于此此时,会生成一 ...
- Spring中AOP相关源码解析
前言 在Spring中AOP是我们使用的非常频繁的一个特性.通过AOP我们可以补足一些面向对象编程中不足或难以实现的部分. AOP 前置理论 首先在学习源码之前我们需要了解关于AOP的相关概念如切点切 ...
- 机器学习:weka中Evaluation类源码解析及输出AUC及交叉验证介绍
在机器学习分类结果的评估中,ROC曲线下的面积AOC是一个非常重要的指标.下面是调用weka类,输出AOC的源码: try { // 1.读入数据集 Instances data = new Inst ...
- Springboot Actuator之七:actuator 中原生endpoint源码解析1
看actuator项目的包结构,如下: 本文中的介绍Endpoints. Endpoints(端点)介绍 Endpoints 是 Actuator 的核心部分,它用来监视应用程序及交互,spring- ...
随机推荐
- JS - JavaScript 主要知识点(基础夯实)
纲要 基本类型和引用类型 类型判断 强制类型转换 作用域 执行上下文 理解函数的执行过程 this 指向 闭包 原型和原型链 js 的继承 event loop 基本类型和引用类型 js中数据类型分为 ...
- 顺通鞋服进销存OA管理系统
鞋服进销存OA管理系统通过十几年的积淀与创新,顺通与众多鞋服企业一起共创,形成了涵盖协同办公.移动办公.知识管理.数据运营.多维门户等领域,以鞋服新品研发管理.生产排班管理.门店一体化管理.市场费用管 ...
- 【Oracle】使用PL/SQL快速查询出1-9数字
[Oracle]使用PL/SQL快速查询出1-9数字 简单来说,直接Recursive WITH Clauses 在Oracle 里面就直接使用WITH result(参数)即可 WITH resul ...
- 力扣619(MySQL)-只出现一次的最大数字(简单)
题目: MyNumbers 表: 单一数字 是在 MyNumbers 表中只出现一次的数字. 请你编写一个 SQL 查询来报告最大的 单一数字 .如果不存在 单一数字 ,查询需报告 null . 查询 ...
- 跨模态学习能力再升级,EasyNLP电商文图检索效果刷新SOTA
简介: 本⽂简要介绍我们在电商下对CLIP模型的优化,以及上述模型在公开数据集上的评测结果.最后,我们介绍如何在EasyNLP框架中调用上述电商CLIP模型. 作者:熊兮.欢夏.章捷.临在 导读 多模 ...
- 「直播回顾」Mars应用与最佳实践
简介: 本文首先对Mars的概念.功能.优势进行了介绍,随后,对Mars几个典型的应用场景进行介绍,并通过两个Demo展示了在使用Mars后数据科学性能的提升,最后总结了Mars的最佳实践,让使用Ma ...
- 一不小心,它成为了 GitHub Alibaba Group 下 Star 最多的开源项目
简介: 随着微服务的流行,应用更加轻量和高效,但是带来的困境是线上问题排查越来越复杂困难.传统的 Java 排查问题,需要重启应用再进行调试,但是重启应用之后现场会丢失,问题难以复现. 来源 | 阿里 ...
- Git 版本控制:构建高效协作和开发流程的最佳实践
引言 版本控制是开发中不可或缺的一部分,他允许多人同时协作,通过记录每一次代码的变更,帮助开发者理解何时.为什么以及谁做了修改.这不仅有助于错误追踪和功能回溯,还使得团队能够并行工作,通过分支管理实现 ...
- NopCommerce支持多种类型的数据库
本文章的内容是根据本人阅读NopCommerce源码的理解,如有不对的地方请指正,谢谢. 阅读目录 1.类结构关系图 2.分析 3.NopCommerce应用 类结构关系图 分析 NopObjectC ...
- 8.k8s之调动pod到指定节点与创建多容器pod并查找pod日志
官方文档:将pod分配给节点题目1:调度pod到指定节点 设置配置环境kubectl config use-context k8s 按如下要求创建并调度一个pod: - 名称:nginx-kusc00 ...