Lfu缓存在Rust中的实现及源码解析

一个 lfu(least frequently used/最不经常使用页置换算法 ) 缓存的实现,其核心思想是淘汰一段时间内被访问次数最少的数据项。与LRU(最近最少使用)算法不同,LFU更侧重于数据的访问频率而非访问的新鲜度。

LFU的原理与实现机制

  1. 普通队列:LFU算法通过记录数据项的访问频次来工作。当缓存容量达到上限时,系统将会淘汰访问频次最低的数据项。这种方法基于一个假设,即在一段时间内被访问频次较少的数据,未来被访问的几率同样较小。

  1. 数据结构选择:为实现O(1)的时间复杂度,这里LFU通常使用哈希表(存储key与节点数据)和双向链表(存储次数与key结构关系)结合的方式来实现。哈希表用于快速查找节点是否存在,而双向链表则用于根据访问频次组织数据项。此处双向链表用一个无限长度的LruCache代替。在remove或者改变频次的时候可以用O(1)的复杂度进行操作。一开始用HashSet<Key>来设计,因为在Rust中HashSet并不存在pop函数,在数据大量触发替代的时候随机选择一个元素效率太低。

  1. 节点管理:每个节点除了存储键值之外,还需附带访问频次信息。每次数据项被访问时,其对应的节点频次会增加;当需要淘汰时,寻找频次最低的节点进行移除或替换。

LFU与LRU的对比及使用场景

  • 算法侧重点差异:LRU侧重于数据的访问新鲜度,即最近未被访问的数据更容易被淘汰;而LFU更关注数据项的总访问频次,不频繁访问的数据被认为是低优先级的。

  • 适用场景的不同:LRU适合应对具有时间局部性的数据访问模式,例如某些顺序读取的场景;LFU则更适合数据访问模式较为平稳,且各个数据项访问频率差异明显的环境。

  • 实现复杂性对比:LRU的实现相对简单,通常只需要维护一个按照时间顺序排列的链表即可;而LFU需要同时考虑访问频次和时间两个维度,因此实现上更为复杂。

LFU算法的实际案例

  • 缓存系统中的应用:许多现代缓存系统中,如Redis,都实现了LFU作为缓存逐出策略之一,允许用户根据具体需求选择合适的淘汰算法。在数据负载高的时候可以允许配置maxmemory-policyvolatile-lru|allkeys-lru|volatile-random|allkeys-random|volatile-ttl|volatile-lfu|allkeys-lfu|noeviction

  • 负载均衡算法:在分布式系统中,LFU也可以作为一种简单的负载均衡策略,将请求分散到不同的服务器上,避免单点过载。

  • 数据库查询优化:数据库管理系统中,LFU可以用来优化查询计划的缓存,减少磁盘I/O次数,提高重复查询的性能。

结构设计

与Lru的结构类似,K与V均用指针方式保存,避免在使用过程中出现Copy或者Clone的可能,提高性能。注:该方法用了指针会相应的出现许多unsafe的代码,因为在Rsut中,访问指针都被认为是unsafe。我们也可以使用数组坐标模拟指针的方式来模拟。

节点设计

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

/// Lfu节点数据
pub(crate) struct LfuEntry<K, V> {pub key: mem::MaybeUninit<K>,pub val: mem::MaybeUninit<V>,/// 访问总频次pub counter: usize,/// 带ttl的过期时间,单位秒/// 如果为u64::MAX,则表示不过期#[cfg(feature = "ttl")]pub expire: u64,
}

类设计

Lfu相对复杂度会比较高,这里维护了最大及最小的访问频次,方便遍历的时候高效

pub struct LfuCache<K, V, S> {map: HashMap<KeyRef<K>, NonNull<LfuEntry<K, V>>, S>,/// 因为HashSet的pop耗时太长, 所以取LfuCache暂时做为平替times_map: HashMap<u8, LruCache<KeyRef<K>, (), DefaultHasher>>,cap: usize,/// 最大的访问频次 max_freq: u8,/// 最小的访问频次min_freq: u8,/// 总的访问次数visit_count: usize,/// 初始的访问次数default_count: usize,/// 每多少次访问进行一次衰减reduce_count: usize,/// 下一次检查的时间点,如果大于该时间点则全部检查是否过期#[cfg(feature = "ttl")]check_next: u64,/// 每次大检查点的时间间隔,如果不想启用该特性,可以将该值设成u64::MAX#[cfg(feature = "ttl")]check_step: u64,/// 所有节点中是否存在带ttl的结点,如果均为普通的元素,则过期的将不进行检查#[cfg(feature = "ttl")]has_ttl: bool,
}

频次的设计

这此处频次我们设计成了一个u8类型,但是实际上我们访问次数肯定远远超过u8::MAX即255的数值。因为此处将访问总次数与频次做了一个映射,防止数据碎片太高及变动频次太频繁。比如初始操作比较频繁的0-10分别映射成0-6如2或者3均映射到2,10-40映射到7-10。其本质的原理就是越高的访问频次越不容易被淘汰,相对来说4次或者5次很明显,但是100次和101次其实没多少差别。这样子我们就可以将很高的梯度映射成一颗比较小的树,减少碎片化的操作。

/// 避免hash表爆炸, 次数与频次映射
fn get_freq_by_times(times: usize) -> u8 {lazy_static! {static ref CACHE_ARR: Vec<u8> = {let vec = vec![(0, 0, 0u8),(1, 1, 1u8),(2, 3, 2u8),(4, 4, 3u8),(5, 5, 4u8),(6, 7, 5u8),(8, 9, 6u8),(10, 12, 7u8),(13, 16, 8u8),(16, 21, 9u8),(22, 40, 10u8),(41, 79, 11u8),(80, 159, 12u8),(160, 499, 13u8),(500, 999, 14u8),(999, 1999, 15u8),];let mut cache = vec![0;2000];for v in vec {for i in v.0..=v.1 {cache[i] = v.2;}}cache};static ref CACHE_LEN: usize = CACHE_ARR.len();};if times < *CACHE_LEN {return CACHE_ARR[times];}if times < 10000 {return 16;} else if times < 100000 {return 17;} else if times < 1000000 {return 18;} else {return 19;}
}

这里用懒初始化,只有该函数第一次被调用的时候才会初始化这static代码,且只会初始化一次,增加访问的速度。

reduce_count的设计

假设一段时间内某个元素访问特别多,如algorithm-rs访问了100000次,接下来很长的一段时间内他都没有出现过,如果普通的Lfu的淘汰规则,那么他将永远的保持在访问频次100000次,基本上属于很难淘汰。那么他将长久的占用了我们的数据空间。针对这种情况此处设计了降权的模式,假设reduce_count=100000,那么就每10w次访问,将对历史的存量数据访问次数进行降权即新次数=原次数/2,那么在第一次降权后,algorithm-rs将变成了50000,其的权重将被削减。在一定访问的之后如果都没有该元素的访问最后他将会被淘汰。visit_count将当前访问的次数进行记录,一旦大于reduce_count将进行一轮降权并重新计算。

default_count的设计

由于存在降权的,那么历史的数据次数可能会更低的次数。那么我们将插入的每个元素赋予初始次数,以防止数据在刚插入的时候就被淘汰。此处默认的访问次数为5。如果历史经历了降权,那么将会有可能存在数据比5小的数据,将优先被淘汰。

初始化

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

impl<K, V, S> LfuCache<K, V, S> {/// 提供hash函数pub fn with_hasher(cap: usize, hash_builder: S) -> LfuCache<K, V, S> {let cap = cap.max(1);let map = HashMap::with_capacity_and_hasher(cap, hash_builder);Self {map,times_map: HashMap::new(),visit_count: 0,max_freq: 0,min_freq: u8::MAX,reduce_count: cap.saturating_mul(100),default_count: 4,cap,#[cfg(feature = "ttl")]check_step: DEFAULT_CHECK_STEP,#[cfg(feature = "ttl")]check_next: get_milltimestamp()+DEFAULT_CHECK_STEP * 1000,#[cfg(feature = "ttl")]has_ttl: false,}}
}

此处min_freq > max_freq在循环的时候将不会进行任何循环,表示没有任何元素。

元素插入及删除

插入对象,分已在缓存内和不在缓存内与Lru的类似,此处主要存在可能操作的列表变化问题

fn try_fix_entry(&mut self, entry: *mut LfuEntry<K, V>) {unsafe {if get_freq_by_times((*entry).counter) == get_freq_by_times((*entry).counter + 1) {self.visit_count += 1;(*entry).counter += 1;} else {self.detach(entry);self.attach(entry);}}
}

假如访问次数从10次->变成11次,但是他的映射频次并没有发生变化,此处我们仅仅需要将元素的次数+1即可,不用移动元素的位置。

attach 其中附到节点上:

fn attach(&mut self, entry: *mut LfuEntry<K, V>) {unsafe {self.visit_count += 1;(*entry).counter += 1;let freq = get_freq_by_times((*entry).counter);self.max_freq = self.max_freq.max(freq);self.min_freq = self.min_freq.min(freq);self.times_map.entry(freq).or_default().reserve(1).insert((*entry).key_ref(), ());self.check_reduce();}
}

附到节点时我们将会改变min_freq,max_freq,并将该元素放入到对应的频次里预留足够的空间reserve(1)。并在最后检测是否降权self.check_reduce()

detach 从队列中节点剥离
/// 从队列中节点剥离
fn detach(&mut self, entry: *mut LfuEntry<K, V>) {unsafe {let freq = get_freq_by_times((*entry).counter);self.times_map.entry(freq).and_modify(|v| {v.remove(&(*entry).key_ref());});}
}

此处我们仅仅移除节点key信息,这里使用的是LruCache,移除也是O(1)的时间复杂度。但是此处我们不维护min_freqmax_freq因为不确定是否当前是否维一,此处维护带来的收益较低,故不做维护。

check_reduce 降权
/// 从队列中节点剥离
fn check_reduce(&mut self) {if self.visit_count >= self.reduce_count {let mut max = 0;let mut min = u8::MAX;for (k, v) in self.map.iter() {unsafe {let node = v.as_ptr();let freq = get_freq_by_times((*node).counter);(*node).counter /= 2;let next = get_freq_by_times((*node).counter);max = max.max(next);min = min.min(next);if freq != next {self.times_map.entry(freq).and_modify(|v| {v.remove(k);});self.times_map.entry(next).or_default().reserve(1).insert((*node).key_ref(), ());}}}self.max_freq = max;self.min_freq = min;self.visit_count = 0;}
}

当前降权后将重新初始化min_freqmax_freq,将当前的所有的频次/2,此算法的复杂度为O(n)。

replace_or_create_node 替换节点
fn replace_or_create_node(&mut self, k: K, v: V) -> (Option<(K, V)>, NonNull<LfuEntry<K, V>>) {if self.len() == self.cap {for i in self.min_freq..=self.max_freq {if let Some(val) = self.times_map.get_mut(&i) {if val.is_empty() {continue;}let key = val.pop_unusual().unwrap().0;let old_node = self.map.remove(&key).unwrap();let node_ptr: *mut LfuEntry<K, V> = old_node.as_ptr();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(),)};unsafe {(*node_ptr).counter = self.default_count;}return (Some(replaced), old_node);}}unreachable!()} else {(None, unsafe {NonNull::new_unchecked(Box::into_raw(Box::new(LfuEntry::new_counter(k,v,self.default_count,))))})}
}

当元素数据满时,我们将做淘汰算法,此处我们将从min_reqmax_req做遍历,并将最小的频次的pop掉最后一个元素。此处如果我们不需护min_reqmax_req那么将会最坏的情况为0-255,即256次循环。

其它操作

  • pop移除栈顶上的数据,最近使用的

  • pop_last移除栈尾上的数据,最久未被使用的

  • contains_key判断是否包含key值

  • raw_get直接获取key的值,不会触发双向链表的维护

  • get获取key的值,并维护双向链表

  • get_mut获取key的值,并可以根据需要改变val的值

  • retain 根据函数保留符合条件的元素

  • get_or_insert_default 获取或者插入默认参数

  • get_or_insert_mut 获取或者插入对象,可变数据

  • set_ttl 设置元素的生存时间

  • del_ttl 删除元素的生存时间,表示永不过期

  • get_ttl 获取元素的生存时间

  • set_check_step 设置当前检查lru的间隔

如何使用

在cargo.toml中添加

[dependencies]
algorithm = "0.1"

示例

 
use algorithm::LfuCache;
fn main() {let mut lru = LfuCache::new(3);lru.insert("hello", "algorithm");lru.insert("this", "lru");lru.set_reduce_count(100);assert!(lru.get_visit(&"hello") == Some(5));assert!(lru.get_visit(&"this") == Some(5));for _ in 0..98 {let _ = lru.get("this");}lru.insert("hello", "new");assert!(lru.get_visit(&"this") == Some(51));assert!(lru.get_visit(&"hello") == Some(3));let mut keys = lru.keys();assert!(keys.next()==Some(&"this"));assert!(keys.next()==Some(&"hello"));assert!(keys.next() == None);
}

结语

综上所述,LFU算法通过跟踪数据项的访问频次来决定淘汰对象,适用于数据访问频率差异较大的场景。与LRU相比,LFU更能抵御偶发性的大量访问请求对缓存的冲击。然而,LFU的实现较为复杂,需要综合考虑效率和公平性。在实际应用中,应当根据具体的数据访问模式和系统需求,灵活选择和调整缓存算法,以达到最优的性能表现。

文章转载自:问蒙服务框架

原文链接:https://www.cnblogs.com/wmproxy/p/18270304

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

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

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

相关文章

带安全启动—Ubuntu系统—手动安装Nvidia驱动

教程1&#xff1a;在启用安全启动的 Fedora 中安装英伟达驱动 教程2&#xff1a;UEFI安全启动模式下安装Ubuntu的NVIDIA显卡驱动 1. 搜索合适的驱动 Nvidia驱动官网 选择这个 驱动(.run)链接 2. 安装必要的软件依赖 CUDA底层用C写的&#xff0c;因此导入编译器 sudo apt i…

大模型压缩:基于贝叶斯优化的自适应低秩分解

1.方法 1.1 基于特征的高维空间低秩分解 PCA已经是老朋友了&#xff0c;每次一说主成分都会出现PCA。这篇文章1利用预训练数据的子集作为校准数据集 D c a l { x i } i 1 n \mathcal{D}_{cal}\{x_{i}\}_{i1}^{n} Dcal​{xi​}i1n​&#xff0c;首先用校准数据集的样本协方差…

ts语法---数据类型,interface和type的用法

ts的数据类型 ts的数据类型自上而下的分级有 第一层 any-任意类型和unknow类型&#xff0c; 第二层 原型链的Object类型&#xff0c; 第三层 js类的类型 第四层 标准的typescript类型 第五层 对应的实例数据类型 第六层 never类型&#xff0c;never表示不合理&#xff0c…

SHELL脚本学习——自动备份

1、 tar 命令 tar {operation} [options…] [file]… &#xff1a;压缩文件 operation&#xff1a; -c 创建压缩包 -x 提取文件 -t 列出文件 -f 指定文件名 -z 通过gzip指令处理备份文件 tar命令详细介绍见&#xff1a;https://www.runoob.com/linux/linux-comm-tar.html) 例&…

整除分块的题目

链接 思路&#xff1a; 求1到n中的因数个数和等价于求,设x为因子&#xff0c;就是求x在1到n里出现了几次&#xff0c;求1到n里是x的倍数的数有几个&#xff0c;即n/x。需要用整除分块&#xff0c;n/i的值是分块分部的&#xff0c;右端点是n/&#xff08;n/i&#xff09;。 代…

Application Studio 学习笔记(3)

一、工具栏按钮 1、panel控件添加工具栏按钮 展开panel控件的Advanced属性并点击Action Data&#xff0c;进入Action Data编辑界面 新增Action Data数据&#xff0c;Sequence设定工具按钮的显示顺序 默认工具按钮会显示在弹出工具栏中 勾选Add to Primary ToolBar后&#xff…

deepin基于apt-mirror同步软件源及构建本地内网源

1.安装apt-mirror sudo apt install -y apt-mirror2.配置apt-mirror(/etc/apt/mirror.list) sudo cp /etc/apt/mirror.list /etc/apt/mirror.list.deepin.bak #备份配置文件 sudo gedit /etc/apt/mirror.list修改如下&#xff1a; deb [trustedyes] https://mirrors.bfsu.ed…

Django学习第一天

Django安装&#xff1a; pip install Django -i https://mirrors.aliyun.com/pypi/simple/ 在需要创建文件的文件目录下写这个命令 django-admin startproject mysite 注意&#xff1a;C:\Users\Administrator\AppData\Local\Programs\Python\Python311\Scripts已加入环境变…

JAVA:Word2Vec的使用

1、简介 Word2Vec是 Google 在 2013 年年中开源的一款将词表征为实数值向量的高效工具, 其利用深度学习的思想&#xff0c;可以通过训练&#xff0c;把对文本内容的处理简化为 K 维向量空间中的向量运算&#xff0c;而向量空间上的相似度可以用来表示文本语义上的相似度。 Wo…

【华为OD机试B卷】查找众数及中位数(C++/Java/Python)

题目 题目描述 众数是指一组数据中出现次数量多的那个数,众数可以是多个。中位数是指把一组数据从小到大排列,最中间的那个数,如果这组数据的个数是奇数,那最中间那个就是中位数,如果这组数据的个数为偶数,那就把中间的两个数之和除以2,所得的结果就是中位数。查找整型数…

windows USB设备驱动开发-双角色驱动

在USB的通讯协议中&#xff0c;规定发起连接的一方为主机(Host)&#xff0c;接受连接的一方为设备&#xff0c;这可以用U盘插入电脑举个例子&#xff0c;当U盘插入电脑后&#xff0c;电脑这边主动发起查询和枚举&#xff0c;U盘被动响应查询和数据存取。 USB 双角色驱动程序堆…

使用Qt designer辅助开发pyqt应用,如何将自定义组件添加到designer中

背景&#xff1a; 我想要实现列表项的拖拽功能&#xff0c;于是自定义类 MyListWidget 通过Qt designer生成的代码&#xff0c;使用的是原始类型 QListWidget 我需要手动修改 Ui_xxx.py文件&#xff1a; 1.导入MyListWidget 2.将QListWidget替换为MyListWidget #Ui_xxx.pyf…

Generating Diverse Structure for Image Inpainting With Hierarchical VQ-VAE

Jialun Peng1 Dong Liu1* Songcen Xu2 Houqiang Li1 1 University of Science and Technology of China 2 Noahs Ark Lab, Huawei Technologies Co., Ltd.pjlmail.ustc.edu.cn, {dongeliu, lihq}ustc.edu.cn, xusongcenhuawei.com 原文提供代码链接&#xff1a; GitHub - UST…

3款免费宝藏电脑软件,每一个都非常实用

LICEcap LICEcap是一款简洁易用的动画屏幕录制软件&#xff0c;主要功能是将屏幕录像的内容直接保存为高质量&#xff08;每帧颜色数量可超过256&#xff09;GIF动态图片格式。该软件安装包轻量级&#xff0c;不到500KB&#xff0c;运行后会以窗口的方式显示在桌面上&#xff…

分布式存储和分布式计算两个哪个更适合作为工作深入方向发展?

有朋友问&#xff0c;分布式存储比如hdfs&#xff0c;ceph&#xff0c;minio&#xff0c;tidb&#xff0c;glusterfs&#xff1b;分布式计算比如Hadoop&#xff0c;spark&#xff0c;flink&#xff1b;它们在实际工作中咋样&#xff1f;具体开发工作是啥&#xff1f;哪个更有发…

财务RPA与数字化转型——财务RPA如何促进企业的数字化转型

在数字化时代&#xff0c;企业面临着推动创新、提高效率的巨大挑战。RPA财务机器人作为智慧财务不可或缺的新动能&#xff0c;不仅能够优化财务流程&#xff0c;还能够在整个企业中引领数字化变革。本文金智维将深入探讨财务RPA如何成为企业数字化转型的战略利器&#xff0c;为…

【哈哈大一上学的全忘了,重开!!】STM32从零入门物联网开发

本笔记资料来源 &#xff1a;STM32物联网入门30步&#xff1d;单片机物联网入门教程 WIFI连接阿里云物联网CubeMXHAL库蓝牙ESP8266杜洋主讲_哔哩哔哩_bilibili IOT&#xff1a;Internet of things 学习目标&#xff1a; 1.掌握洋桃IoT开发板的各功能以及驱动与基本应用 2.掌…

Keepalived实践

keepalived集群高可用部署参考 需求描述&#xff1a;某服务A&#xff08;后面都用A来表示该服务&#xff09;需要高可用服务需求。当主服务故障时&#xff0c;需要切换到备服务上。目前为一主一备&#xff0c;后续为一主多备 需求提炼&#xff1a; 部署keepalived服务进行健康…

docker 部署jitsi meet

1. 部署环境&#xff1a; 1.1 vm 虚拟机 安装的 centos 7 1.2 centos7安装docker 和 docker-compose 2.docker命令 官网部署文档地址&#xff1a;&#xff08;文档地址有可能失效&#xff09; Self-Hosting Guide - Docker | Jitsi Meet 2.1Download and extract the late…

1-3.文本数据建模流程范例

文章最前&#xff1a; 我是Octopus&#xff0c;这个名字来源于我的中文名–章鱼&#xff1b;我热爱编程、热爱算法、热爱开源。所有源码在我的个人github &#xff1b;这博客是记录我学习的点点滴滴&#xff0c;如果您对 Python、Java、AI、算法有兴趣&#xff0c;可以关注我的…