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- ...
随机推荐
- vue项目如何部署?有遇到布署服务器后刷新404问题吗?
一.如何部署 前后端分离开发模式下,前后端是独立布署的,前端只需要将最后的构建物上传至目标服务器的web容器指定的静态目录下即可 我们知道vue项目在构建后,是生成一系列的静态文件 常规布署我们只需要 ...
- 3.CSS三种基本选择器
三种选择器的优先级: id选择器 > class选择器 > 标签选择器 1.标签选择器:会选择到页面上所有的该类标签的元素 格式: 标签{} 1 <!DOCTYPE html> ...
- Beetl 源码解析:GroupTemplate 类
本文首发于公众号:腐烂的橘子 前言 Beetl 是一款 Java 模板引擎,在公司的项目中大量运用,它的作用是写通用代码时,有一些差异化的逻辑需要处理,这时可以把这些差异化的逻辑写在模板里,程序直接调 ...
- Redis 数据类型list以及使用场景
简介: Redis 数据类型list以及使用场景 数据存储需求:存储多个数据,并对数据进入存储空间的顺序进行区分需要的存储结构:一个存储空间保存多个数据,且通过数据可以体现进入顺序list类型:保存多 ...
- OpenKruise v1.0:云原生应用自动化达到新的高峰
简介:OpenKruise 是针对 Kubernetes 的增强能力套件,聚焦于云原生应用的部署.升级.运维.稳定性防护等领域. 云原生应用自动化管理套件.CNCF Sandbox 项目 -- Op ...
- 阿里云 EventBridge 事件驱动架构实践
简介:我们认为 EventBridge 是云原生时代新的计算驱动力,这些数据可以驱动云的计算能力,创造更多业务价值. 作者:周新宇 本文内容整理自 中国开源年会 演讲 首先做一个自我介绍,我是 Ro ...
- 2024-05-01:用go语言,给定两个长度为偶数n的整数数组nums1和nums2, 分别移除它们各自的一半元素, 将剩下的元素合并成集合s。 找出集合s中可能包含的最多元素数量。 输入:nums
2024-05-01:用go语言,给定两个长度为偶数n的整数数组nums1和nums2, 分别移除它们各自的一半元素, 将剩下的元素合并成集合s. 找出集合s中可能包含的最多元素数量. 输入:nums ...
- 《Effective C++》第三版-4. 设计与声明(Design and Declarations)
目录 条款17:让接口容易被正确使用,不易被误用(Make interfaces easy to use correctly and hard to use incorrectly) 限制类型和值 规 ...
- C#/C++ 通过ODBC连接OceanBase Oracle租户
概述 近期我们项目正处于将Oracle数据库迁移到OceanBase Oracle租户模式的阶段.考虑到我们项目采用了C++和C#混合开发,并且使用了多种技术,因此存在多种数据库连接方式.然而,针对C ...
- 01 Xpath简明教程(十分钟入门)
目录 Xpath简明教程(十分钟入门) Xpath表达式 Xpath节点 节点关系 Xpath基本语法 1) 基本语法使用 2) xpath通配符 3) 多路径匹配 Xpath内建函数 Xpath简明 ...