zoukankan      html  css  js  c++  java
  • LRU Cache

    一、什么是Cache

    1 概念

    Cache,即高速缓存,是介于CPU和内存之间的高速小容量存储器。在金字塔式存储体系中它位于自顶向下的第二层,仅次于CPU寄存器。其容量远小于内存,但速度却可以接近CPU的频率。

    当CPU发出内存访问请求时,会先查看 Cache 内是否有请求数据。

    • 如果存在(命中),则直接返回该数据;
    • 如果不存在(失效),再去访问内存 —— 先把内存中的相应数据载入缓存,再将其返回处理器。

    提供“高速缓存”的目的是让数据访问的速度适应CPU的处理速度,通过减少访问内存的次数来提高数据存取的速度。

    2 原理

    Cache 技术所依赖的原理是”程序执行与数据访问的局部性原理“,这种局部性表现在两个方面:

    1. 时间局部性:如果程序中的某条指令一旦执行,不久以后该指令可能再次执行,如果某数据被访问过,不久以后该数据可能再次被访问。
    2. 空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令或数据通常是顺序存放的。

    时间局部性是通过将近来使用的指令和数据保存到Cache中实现。空间局部性通常是使用较大的高速缓存,并将 预取机制 集成到高速缓存控制逻辑中来实现。

    3 替换策略

    Cache的容量是有限的,当Cache的空间都被占满后,如果再次发生缓存失效,就必须选择一个缓存块来替换掉。常用的替换策略有以下几种:

    1. 随机算法(Rand):随机法是随机地确定替换的存储块。设置一个随机数产生器,依据所产生的随机数,确定替换块。这种方法简单、易于实现,但命中率比较低。

    2. 先进先出算法(FIFO, First In First Out):先进先出法是选择那个最先调入的那个块进行替换。当最先调入并被多次命中的块,很可能被优先替换,因而不符合局部性规律。这种方法的命中率比随机法好些,但还不满足要求。

    3. 最久未使用算法(LRU, Least Recently Used):LRU法是依据各块使用的情况, 总是选择那个最长时间未被使用的块替换。这种方法比较好地反映了程序局部性规律。

    4. 最不经常使用算法(LFU, Least Frequently Used):将最近一段时期内,访问次数最少的块替换出Cache。

    4 概念的扩充

    如今高速缓存的概念已被扩充,不仅在CPU和主内存之间有Cache,而且在内存和硬盘之间也有Cache(磁盘缓存),乃至在硬盘与网络之间也有某种意义上的Cache──称为Internet临时文件夹或网络内容缓存等。凡是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构,均可称之为Cache。

    二、LRU Cache的实现

    Google的一道面试题:

    Design an LRU cache with all the operations to be done in O(1) .

    1 思路分析

    对一个Cache的操作无非三种:插入(insert)、替换(replace)、查找(lookup)。

    为了能够快速删除最久没有访问的数据项和插入最新的数据项,我们使用 双向链表 连接Cache中的数据项,并且保证链表维持数据项从最近访问到最旧访问的顺序。

    • 插入:当Cache未满时,新的数据项只需插到双链表头部即可。时间复杂度为O(1).

    • 替换:当Cache已满时,将新的数据项插到双链表头部,并删除双链表的尾结点即可。时间复杂度为O(1).

    • 查找:每次数据项被查询到时,都将此数据项移动到链表头部。

    经过分析,我们知道使用双向链表可以保证插入和替换的时间复杂度是O(1),但查询的时间复杂度是O(n),因为需要对双链表进行遍历。为了让查找效率也达到O(1),很自然的会想到使用 hash table 。

    2 代码实现

    从上述分析可知,我们需要使用两种数据结构:

    1. 双向链表(Doubly Linked List):考虑到缓存中的数据需要经常的插入和删除,所以可以使用双链表来实现,例如C++中的list
    2. 哈希表(Hash Table):考虑到list中每次访问一个数据需要先在list中查找是否命中缓存以及将缓存结点移到链表的表头,但是如果只是按序访问list可能要遍历整个链表,所以需要一个数据结构来辅助加速查找的过程,可以选择树形结构的红黑树(map)

    用list来存放缓存结点cacheNode,用map来存放缓存结点和该缓存结点在list中的位置(用迭代器表示)。

    复制代码
    #include<unordered_map>
    #include<list>
    #include<iostream>
    using namespace std;
    
    //缓存结点
    struct CacheNode
    {
        int key;
        int value;
        CacheNode(int k,int v):key(k),value(v) {}
    };
    
    //对缓存结点进行操作的类
    class LRUCache
    {
    public:
        LRUCache(int capacity)
        {
            size=capacity;
        }
    
        int get(int key)
        {
            auto iter=cacheMap.find(key);  //在辅助结构map中能够进行快速查找
            if(iter!=cacheMap.end())
            {
                cacheList.splice(cacheList.begin(),cacheList,iter->second); //将找到的结点插入到list的表头,其余结点后移一位
                cacheMap[key]=cacheList.begin(); //更新map中指向访问结点的位置(迭代器)
                return cacheMap[key]->value;
            }
            return -1;
        }
    
        void set(int key, int value)
        {
            auto iter=cacheMap.find(key);
            if(iter!=cacheMap.end())
            {
                cacheMap[key]->value=value;
                cacheList.splice(cacheList.begin(),cacheList,cacheMap[key]);
                cacheMap[key]=cacheList.begin();
            }
            else
            {
                if(size==(int)cacheList.size())
                {
                    //记得要先删除map中的元素,然后再删除list中的地址,不然map中的地址无效,有可能指向后来插入的元素
                    cacheMap.erase(cacheList.back().key);
                    cacheList.pop_back();
                }
                cacheList.push_front(CacheNode(key,value));
                cacheMap[key]=cacheList.begin();
            }
        }
    private:
        int size;
        unordered_map<int,list<CacheNode>::iterator> cacheMap;
        list<CacheNode> cacheList;
    };
    
    int main(){
        LRUCache lru_cache(1);
        lru_cache.set(2,1);
        cout<<lru_cache.get(2)<<endl;
        lru_cache.set(3,2);
        cout<<lru_cache.get(2)<<endl;
        cout<<lru_cache.get(3)<<endl;
    }
    复制代码

    2 然后会问一下关于hashtable相关的,可能会问道hashtable在高并发情况下访问的线程安全问题(百度),或者当hash表需要扩容时怎样扩容,已经hash表中元素需要重新计算hash值,重新映射(美团)。

    hashtable中并发访问最简单的同步方法就是给hash表加锁,但是这样当一个线程访问hashtable的时候,其他线程访问hashtable可能会进入阻塞或者轮询,这样访问hashtable的效率非常低下。

    可以使用锁分段技术来缓解这种情况:

    hashtable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问hashtable的线程必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

    而对于hash表的扩容情况,可以使用一致性hash试试。不知道对不对。

  • 相关阅读:
    xls与csv文件的区别
    青音,经典爱情语录
    win7用户账户自动登录方法汇总
    How to using Procedure found Lead Blocker
    FTS(3) BSD 库函数手册 遍历文件夹(二)
    FTS(3) BSD 库函数手册 遍历文件夹(一)
    DisplayMetrics类 获取手机显示屏的基本信息 包括尺寸、密度、字体缩放等信息
    About App Distribution 关于应用发布
    FTS(3) 遍历文件夹实例
    OpenCV 2.1.0 with Visual Studio 2008
  • 原文地址:https://www.cnblogs.com/alantu2018/p/8459992.html
Copyright © 2011-2022 走看看