首页 > 编程语言 > 详细

算法-00| Cache缓存淘汰算法

时间:2020-09-22 12:08:33      阅读:50      评论:0      收藏:0      [点我收藏+]

Cache缓存

  ①.记忆
  ②.钱包-储物柜
  ③.代码模块

一个经典的链表应用场景,那就是 LRU 缓存淘汰算法。

缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中都有着非常广泛的应用,比如常见的 CPU 缓存、数据库缓存、浏览器缓存等等。

缓存的大小有限,当缓存被用满时,哪些数据应该被清理出去,哪些数据应该被保留?这就需要缓存淘汰策略来决定。

常见的策略有三种:

  • 先进先出策略 FIFO(First In,First Out)
  • 少使 用策略 LFU(Least Frequently Used)
  • 近少使用策略 LRU(Least Recently Used)

打个比方说,你买了很多本技术书,但发现这些书太多,太占书房空间,需要个大扫除,扔掉一些书籍。你会选择扔掉哪些书呢?对应一下,你的选择标准是不是和上面的三种策略神似。

1. CPUSocket

Understanding the Meltdown exploit – in my own simple words

 技术分享图片

 上图为四核CPU,三级缓存(L1 Cache,L2 Cache ,L3 Cache);

  每一个核里边就有L1 D-Cache,L1 l-Cache,L2 Cache,L3 Cache;最常用的数据马上要给CPU的计算模块进行计算处理的就放在L1里边,次之不常用的就放在L1 l-Cache里边,再次之放在L2Cache里边,最后放在L3 Cache里边。外边即内存。他们的速度 L1 D-Cache >  L1 l-Cache  >  L2-Cache  >  L3-Cache 

体积(能存的数据多少)即  L1 D-Cache <  L1 l-Cache  <  L2-Cache  <  L3-Cache

2. LRUCache

两个要素:

  大小

  替换策略(least recent use -- 最近最少使用

实现机制:

  HashTable+DoubleLinkedList

复杂度分析:
  O(1)查询
  O(1)修改、 更新

LRU Cache工作示例:

  更新原则 least recent use

 技术分享图片

替换策略:
  LFU - least frequently used
  LRU - least recently used

    替换算法总揽

如何基于链表实现 LRU 缓存淘汰算法?

  维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问时,从链表头开始顺序遍历链表。

 ①. 如果此数据之前已经被缓存在链表中了,遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。

 ②. 如果此数据没有在缓存链表中,又可以分为两种情况:

  • 如果此时缓存未满,则将此结点直接插入到链表的头部;
  • 如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。

这样我们就用链表实现了一个 LRU 缓存。

 m 缓存访问的时间复杂度是多少。因为不管缓存有没有满,都需要遍历一遍链表,所以这种基于链表的实现思路,缓存访问的时间复杂度为 O(n)。 

可以继续优化这个实现思路,比如引入散列表(Hash table)来记录每个数据的位置,将缓存访问的时间复杂度降到 O(1)。

LRUCache  Python

class LRUCache(object): 
    def __init__(self, capacity): 
        self.dic = collections.OrderedDict() 
        self.remain = capacity 
    def get(self, key): 
        if key not in self.dic: 
            return -1 
        v = self.dic.pop(key) 
        self.dic[key] = v   # key as the newest one 
        return v 
    def put(self, key, value): 
        if key in 
            self.dic: self.dic.pop(key) 
        else: 
            if self.remain > 0: 
                self.remain -= 1 
            else:   # self.dic is full 
                self.dic.popitem(last=False) 
            self.dic[key] = value

 LRUCache Java

    private Map<Integer, Integer> map;

    public LRUCache(int capacity) {
        map = new LinkedCappedHashMap<>(capacity);
    }
    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        return map.get(key);
    }
    public void put(int key, int value) {
        map.put(key, value);
    }

    private static class LinkedCappedHashMap<K, V> extends LinkedHashMap<K, V> {
        int maximumCapacity;

        LinkedCappedHashMap(int maximumCapacity) {
            // initialCapacity代表map的容量, loadFactor代表加载因子, accessOrder默认false,如果要按读取顺序排序需要将其设为true
            super(16, 0.75f, true);//default initial capacity (16) and load factor (0.75) and accessOrder (false)
            this.maximumCapacity = maximumCapacity;
        }
        /* 重写 removeEldestEntry()函数,就能拥有我们自己的缓存策略 */
        protected boolean removeEldestEntry(Map.Entry eldest) {
            return size() > maximumCapacity;
        }
    }

3. 链表和散列表的组合使用 

借助散列表,我们可以把 LRU 缓存淘汰算法的时间复杂度降低为 O(1)

如何通过链表实现 LRU 缓存淘汰算法的。

我们需要维护一个按照访问时间从大到小有序排列的链表结构。因为缓存大小有限,当缓存空间 不够,需要淘汰一个数据的时候,我们就直接将链表头部的结点删除。

当要缓存某个数据的时候,先在链表中查找这个数据。如果没有找到,则直接将数据放到链表的 尾部;如果找到了,我们就把它移动到链表的尾部。因为查找数据需要遍历链表,所以单纯用链 表实现的 LRU 缓

存淘汰算法的时间复杂很高,是 O(n)。

实际上,我总结一下,一个缓存(cache)系统主要包含下面这几个操作:

  • 往缓存中添加一个数据;
  • 从缓存中删除一个数据;
  • 在缓存中查找一个数据。

这三个操作都要涉及“查找”操作,如果单纯地采用链表的话,时间复杂度只能是 O(n)。如果将散列表和链表两种数据结构组合使用,可以将这三个操作的时间复杂度都降低到 O(1)。

具体结构如下:

    技术分享图片

使用双向链表存储数据,链表中的每个结点处理存储数据(data)、前驱指针(prev)、 后继指针(next)之外,还新增了一个特殊的字段 hnext。这个 hnext 有什么作用呢?

因为我们的散列表是通过链表法解决散列冲突的,所以每个结点会在两条链中。一个链是刚刚我 们提到的双向链表,另一个链是散列表中的拉链。前驱和后继指针是为了将结点串在双向链表 中,hnext 指针是

为了将结点串在散列表的拉链中。

 

算法-00| Cache缓存淘汰算法

原文:https://www.cnblogs.com/shengyang17/p/13644143.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!