如何实现最不常用(LFU)缓存?

Posted

技术标签:

【中文标题】如何实现最不常用(LFU)缓存?【英文标题】:How to implement a Least Frequently Used (LFU) cache? 【发布时间】:2014-01-14 15:45:06 【问题描述】:

最不常用 (LFU) 是一种用于管理计算机内存的缓存算法。此方法的标准特征包括系统跟踪内存中引用块的次数。当缓存已满并需要更多空间时,系统将清除参考频率最低的项目。

实现最近使用的对象缓存的最佳方法是什么,比如在 Java 中?

我已经使用 LinkedHashMap 实现了一个(通过维护对象被访问的次数)但是我很好奇是否有任何新的并发集合会是更好的候选者。

考虑这种情况:假设缓存已满,我们需要为另一个缓存腾出空间。假设在缓存中记录了两个对象,它们仅被访问一次。如果我们知道另一个(不在缓存中的)对象被多次访问,那么应该删除哪一个?

谢谢!

【问题讨论】:

使用现有的实现,例如来自 Guava 的实现。 为每个缓存条目保留一个计数器,并在每次成功引用时增加相应的计数器。如果计数器溢出,将所有计数器除以 2(或 4 或其他)以“重新调整”它们。计数最少的条目是最不常用的。 【参考方案1】:

您可能会从 ActiveMQ 的 LFU 实现中受益:LFUCache

它们提供了一些很好的功能。

【讨论】:

【参考方案2】:

我认为,LFU 数据结构必须结合优先级队列(用于保持对 lfu 项的快速访问)和哈希映射(用于通过其键提供对任何项的快速访问);我建议为缓存中存储的每个对象使用以下节点定义:

class Node<T> 
   // access key
   private int key;
   // counter of accesses
   private int numAccesses;
   // current position in pq
   private int currentPos;
   // item itself
   private T item;
   //getters, setters, constructors go here

你需要key 来引用一个项目。 您需要 numAccesses 作为优先队列的键。 您需要currentPos 才能通过按键快速找到项目的pq 位置。 现在您组织哈希映射 (key(Integer) -> node(Node&lt;T&gt;)) 以使用访问次数作为优先级快速访问项目和基于堆的最小优先级队列。现在您可以非常快速地执行所有操作(访问、添加新项目、更新访问次数、删除 lfu)。您需要仔细编写每个操作,以使所有节点保持一致(它们的访问次数、它们在 pq 中的位置以及在 hash map 中的存在)。所有操作都将以恒定的平均时间复杂度工作,这是您对缓存的期望。

【讨论】:

【参考方案3】:

    据我说,实现最近使用的对象缓存的最佳方法是为每个对象包含一个新变量作为“latestTS”。 TS 代表时间戳

    // 一个静态方法,返回自 1970 年 1 月 1 日以来的当前日期和时间(以毫秒为单位) long latestTS = System.currentTimeMillis();

    ConcurrentLinkedHashMap 尚未在并发 Java 集合中实现。 (参考:Java Concurrent Collection API)。但是,您可以尝试使用ConcurrentHashMap 和DoublyLinkedList

    关于要考虑的情况:在这种情况下,正如我所说,您可以声明latestTS 变量,根据latestTS 变量的值,您可以删除条目并添加新对象。 (不要忘记更新添加的新对象的频率和最新的TS)

正如您所提到的,您可以使用LinkedHashMap,因为它提供了 O(1) 中的元素访问权限,并且您还可以获得订单遍历。 请找到以下 LFU 缓存代码: (PS:下面的代码是标题中问题的答案,即“如何实现LFU缓存”)

import java.util.LinkedHashMap;
import java.util.Map;

public class LFUCache 

    class CacheEntry
    
        private String data;
        private int frequency;

        // default constructor
        private CacheEntry()
        

        public String getData() 
            return data;
        
        public void setData(String data) 
            this.data = data;
        

        public int getFrequency() 
            return frequency;
        
        public void setFrequency(int frequency) 
            this.frequency = frequency;
               

    

    private static int initialCapacity = 10;

    private static LinkedHashMap<Integer, CacheEntry> cacheMap = new LinkedHashMap<Integer, CacheEntry>();
    /* LinkedHashMap is used because it has features of both HashMap and LinkedList. 
     * Thus, we can get an entry in O(1) and also, we can iterate over it easily.
     * */

    public LFUCache(int initialCapacity)
    
        this.initialCapacity = initialCapacity;
    

    public void addCacheEntry(int key, String data)
    
        if(!isFull())
        
            CacheEntry temp = new CacheEntry();
            temp.setData(data);
            temp.setFrequency(0);

            cacheMap.put(key, temp);
        
        else
        
            int entryKeyToBeRemoved = getLFUKey();
            cacheMap.remove(entryKeyToBeRemoved);

            CacheEntry temp = new CacheEntry();
            temp.setData(data);
            temp.setFrequency(0);

            cacheMap.put(key, temp);
        
    

    public int getLFUKey()
    
        int key = 0;
        int minFreq = Integer.MAX_VALUE;

        for(Map.Entry<Integer, CacheEntry> entry : cacheMap.entrySet())
        
            if(minFreq > entry.getValue().frequency)
            
                key = entry.getKey();
                minFreq = entry.getValue().frequency;
                       
        

        return key;
    

    public String getCacheEntry(int key)
    
        if(cacheMap.containsKey(key))  // cache hit
        
            CacheEntry temp = cacheMap.get(key);
            temp.frequency++;
            cacheMap.put(key, temp);
            return temp.data;
        
        return null; // cache miss
    

    public static boolean isFull()
    
        if(cacheMap.size() == initialCapacity)
            return true;

        return false;
    

【讨论】:

如果在 map 中添加的 key 是同一个 key 但它的 value 不同,那么我们可能需要增加 key 的频率并将其 value 设置为不同。 使用此算法,驱逐将在 O(n) 时间内运行,但如果使用堆,则应该可以运行 O(log(n))。 一旦缓存已满,每个新添加的内容都会对地图进行线性搜索,以寻找要删除的最佳条目。可怕。在我的实现中,我允许地图增长到其目标大小的两倍,然后进行重建以减少一半的条目:重建的成本更高,但它的频率要低得多。【参考方案4】:

这是 LFU 的 o(1) 实现 - http://dhruvbird.com/lfu.pdf

【讨论】:

虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会失效。 除非您是 Google,否则指向 PDF 的链接没有帮助。如果你自己读过论文,请总结一下。 我阅读了链接和作者建议使用 1) HashMap 2) LinkedList 用于存储/连接排序频率的项目 3) LinkedList 用于存储相同频率的元素【参考方案5】:

我试图在 LFU 缓存实现下实现这个。参考了这个—— LFU 纸。我的实现运行良好。

如果有人想提供任何进一步的建议以再次改进它,请告诉我。

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;

public class LFUCacheImplementation 

    private Map<Integer, Node> cache = new HashMap<>();
    private Map<Integer, Integer> counts = new HashMap<>();
    private TreeMap<Integer, DoublyLinkedList> frequencies = new TreeMap<>();
    private final int CAPACITY;

    public LFUCache(int capacity) 
        this.CAPACITY = capacity;
    

    public int get(int key) 
        if (!cache.containsKey(key)) 
            return -1;
        

        Node node = cache.get(key);

        int frequency = counts.get(key);
        frequencies.get(frequency).remove(new Node(node.key(), node.value()));
        removeFreq(frequency);
        frequencies.computeIfAbsent(frequency + 1, k -> new DoublyLinkedList()).add(new Node(node.key(), node.value()));

        counts.put(key, frequency + 1);
        return cache.get(key).value();
    

    public void set(int key, int value) 
        if (!cache.containsKey(key)) 

            Node node = new Node(key, value);

            if (cache.size() == CAPACITY) 

                int l_count = frequencies.firstKey();
                Node deleteThisNode = frequencies.get(l_count).head();
                frequencies.get(l_count).remove(deleteThisNode);

                int deleteThisKey = deleteThisNode.key();
                removeFreq(l_count);
                cache.remove(deleteThisKey);
                counts.remove(deleteThisKey);
            

            cache.put(key, node);
            counts.put(key, 1);
            frequencies.computeIfAbsent(1, k -> new DoublyLinkedList()).add(node);
        
    

    private void removeFreq(int frequency) 
        if (frequencies.get(frequency).size() == 0) 
            frequencies.remove(frequency);
        
    

    public Map<Integer, Node> getCache() 
        return cache;
    

    public Map<Integer, Integer> getCounts() 
        return counts;
    

    public TreeMap<Integer, DoublyLinkedList> getFrequencies() 
        return frequencies;
    


class Node 
    private int key;
    private int value;
    private Node next;
    private Node prev;

    public Node(int key, int value) 
        this.key = key;
        this.value = value;
    

    public Node getNext() 
        return next;
    

    public void setNext(Node next) 
        this.next = next;
    

    public Node getPrev() 
        return prev;
    

    public void setPrev(Node prev) 
        this.prev = prev;
    

    public int key() 
        return key;
    

    public int value() 
        return value;
    

    @Override
    public boolean equals(Object o) 
        if (this == o) return true;
        if (!(o instanceof Node)) return false;
        Node node = (Node) o;
        return key == node.key &&
                value == node.value;
    

    @Override
    public int hashCode() 
        return Objects.hash(key, value);
    

    @Override
    public String toString() 
        return "Node" +
                "key=" + key +
                ", value=" + value +
                '';
    


class DoublyLinkedList 
    private int size;
    private Node head;
    private Node tail;

    public void add(Node node) 
        if (null == head) 
            head = node;
         else 
            tail.setNext(node);
            node.setPrev(tail);
        
        tail = node;
        size++;
    

    public void remove(Node node) 
        if(null == head || null == node) 
            return;
        
        if(this.size() == 1 && head.equals(node)) 
            head = null;
            tail = null;
         else if (head.equals(node)) 
            head = node.getNext();
            head.setPrev(null);
         else if (tail.equals(node)) 
            Node prevToTail = tail.getPrev();
            prevToTail.setNext(null);
            tail = prevToTail;
         else 
            Node current = head.getNext();

            while(!current.equals(tail)) 
                if(current.equals(node)) 
                    Node prevToCurrent = current.getPrev();
                    Node nextToCurrent = current.getNext();
                    prevToCurrent.setNext(nextToCurrent);
                    nextToCurrent.setPrev(prevToCurrent);
                    break;
                
               current = current.getNext();
            
        
        size--;
    

    public Node head() 
        return head;
    

    public int size() 
        return size;
    

使用上述缓存实现的客户端代码-

import java.util.Map;

public class Client 

    public static void main(String[] args) 
        Client client = new Client();
        LFUCache cache = new LFUCache(4);
        cache.set(11, function(11));
        cache.set(12, function(12));
        cache.set(13, function(13));
        cache.set(14, function(14));
        cache.set(15, function(15));
        client.print(cache.getFrequencies());

        cache.get(13);
        cache.get(13);
        cache.get(13);
        cache.get(14);
        cache.get(14);
        cache.get(14);
        cache.get(14);
        client.print(cache.getCache());
        client.print(cache.getCounts());
        client.print(cache.getFrequencies());
    

    public void print(Map<Integer, ? extends Object> map) 

        for(Map.Entry<Integer, ? extends Object> entry : map.entrySet()) 
            if(entry.getValue() instanceof Node) 
                System.out.println("Cache Key => "+entry.getKey()+", Cache Value => "+((Node) entry.getValue()).toString());
             else if (entry.getValue() instanceof DoublyLinkedList) 
                System.out.println("Frequency Key => "+entry.getKey()+" Frequency Values => [");
                Node head = ((DoublyLinkedList) entry.getValue()).head();
                while(null != head) 
                    System.out.println(head.toString());
                    head = head.getNext();
                
                System.out.println(" ]");
             else 
                System.out.println("Count Key => "+entry.getKey()+", Count Value => "+entry.getValue());
            
        
    

    public static int function(int key) 
        int prime = 31;
        return key*prime;
    

【讨论】:

【参考方案6】:

优先级队列怎么样?您可以使用表示频率的键将元素排序在那里。只需在访问后更新队列中的对象位置即可。您可以不时更新以优化性能(但会降低精度)。

【讨论】:

如何计算键值? 我会使用一个全局计数器C,计算所有使用情况。然后对于每个对象o,我会记住C 创建时的值start(o) 以及该元素的使用次数uses(o)。最后,这个对象的频率由uses(o) / (C - start(o)) 给出。当然,我们需要弄清楚细节,但我希望这能解释这个想法。 队列只允许访问其头部,因此不能用于缓存【参考方案7】:

我见过的许多实现都具有运行时复杂性O(log(n))。这意味着,当缓存大小为n 时,将元素插入/删除缓存所需的时间是对数的。这样的实现通常使用min heap 来维护元素的使用频率。堆的根包含频率最低的元素,可以在O(1)时间访问。但是为了维护堆属性,我们必须移动一个元素,每次在堆内部使用它(并且频率增加),将它放置到适当的位置,或者当我们必须将新元素插入缓存时(等等将其放入堆中)。 但是运行时复杂度可以降低到O(1),当我们维护hashmap (Java) 或unordered_map (C++) 并以元素为键。此外,我们需要两种列表,frequency listelements listselements lists 包含具有相同频率的元素,frequency list 包含element lists

  frequency list
  1   3   6   7
  a   k   y   x
  c   l       z
  m   n

在示例中,我们看到 frequency list 有 4 个元素 (4 elements lists)。元素列表1 包含元素(a,c,m),元素列表3 包含元素(k, l, n) 等。 现在,当我们使用说元素y 时,我们必须增加它的频率并将其放在下一个列表中。因为频率为 6 的元素列表变为空,我们将其删除。结果是:

  frequency list
  1   3   7
  a   k   y
  c   l   x
  m   n   z

我们将元素 y 放在 elements list 7 的开头。当我们稍后必须从列表中删除元素时,我们将从末尾开始(首先是 z,然后是 x,然后是 y )。 现在,当我们使用元素 n 时,我们必须增加它的频率并将其放入新列表中,频率为 4:

  frequency list
  1   3   4  7
  a   k   n  y
  c   l      x
  m          z

我希望这个想法很清楚。我现在提供 LFU 缓存的 C++ 实现,稍后将添加 Java 实现。 该类只有 2 个公共方法,void set(key k, value v)bool get(key k, value &amp;v)。在 get 方法中,当找到元素时,将根据引用设置要检索的值,在这种情况下,该方法返回 true。当没有找到该元素时,该方法返回 false。

#include<unordered_map>
#include<list>

using namespace std;

typedef unsigned uint;

template<typename K, typename V = K>
struct Entry

    K key;
    V value;
;


template<typename K, typename V = K>
class LFUCache


typedef  typename list<typename Entry<K, V>> ElementList;
typedef typename list <pair <uint, ElementList>> FrequencyList;

private:
    unordered_map <K, pair<typename FrequencyList::iterator, typename ElementList::iterator>> cacheMap;
    FrequencyList elements;
    uint maxSize;
    uint curSize;

    void incrementFrequency(pair<typename FrequencyList::iterator, typename ElementList::iterator> p) 
        if (p.first == prev(elements.end())) 
            //frequency list contains single list with some frequency, create new list with incremented frequency (p.first->first + 1)
            elements.push_back( p.first->first + 1,  p.second->key, p.second->value  );
            // erase and insert the key with new iterator pair
            cacheMap[p.second->key] =  prev(elements.end()), prev(elements.end())->second.begin() ;
        
        else 
            // there exist element(s) with higher frequency
            auto pos = next(p.first);
            if (p.first->first + 1 == pos->first)
                // same frequency in the next list, add the element in the begin
                pos->second.push_front( p.second->key, p.second->value );
            else
                // insert new list before next list
                pos = elements.insert(pos,  p.first->first + 1 , p.second->key, p.second->value );
            // update cachMap iterators
            cacheMap[p.second->key] =  pos, pos->second.begin() ;
        
        // if element list with old frequency contained this singe element, erase the list from frequency list
        if (p.first->second.size() == 1)
            elements.erase(p.first);
        else
            // erase only the element with updated frequency from the old list
            p.first->second.erase(p.second);
    

    void eraseOldElement() 
        if (elements.size() > 0) 
            auto key = prev(elements.begin()->second.end())->key;
            if (elements.begin()->second.size() < 2)
                elements.erase(elements.begin());
            else
                elements.begin()->second.erase(prev(elements.begin()->second.end()));
            cacheMap.erase(key);
            curSize--;
        
    

public:
    LFUCache(uint size) 
        if (size > 0)
            maxSize = size;
        else
            maxSize = 10;
        curSize = 0;
    
    void set(K key, V value) 
        auto entry = cacheMap.find(key);
        if (entry == cacheMap.end()) 
            if (curSize == maxSize)
                eraseOldElement();
            if (elements.begin() == elements.end()) 
                elements.push_front( 1,  key, value  );
            
            else if (elements.begin()->first == 1) 
                elements.begin()->second.push_front( key,value );
            
            else 
                elements.push_front( 1,  key, value  );
            
            cacheMap.insert( key, elements.begin(), elements.begin()->second.begin() );
            curSize++;
        
        else 
            entry->second.second->value = value;
            incrementFrequency(entry->second);
        
    

    bool get(K key, V &value) 
        auto entry = cacheMap.find(key);
        if (entry == cacheMap.end())
            return false;
        value = entry->second.second->value;
        incrementFrequency(entry->second);
        return true;
    
;

以下是使用示例:

    int main()
    
        LFUCache<int>cache(3); // cache of size 3
        cache.set(1, 1);
        cache.set(2, 2);
        cache.set(3, 3);
        cache.set(2, 4); 

        rc = cache.get(1, r);

        assert(rc);
        assert(r == 1);
        // evict old element, in this case 3
        cache.set(4, 5);
        rc = cache.get(3, r);
        assert(!rc);
        rc = cache.get(4, r);
        assert(rc);
        assert(r == 5);

        LFUCache<int, string>cache2(2);
        cache2.set(1, "one");
        cache2.set(2, "two");
        string val;
        rc = cache2.get(1, val);
       if (rc)
          assert(val == "one");
       else
          assert(false);

       cache2.set(3, "three"); // evict 2
       rc = cache2.get(2, val);
       assert(rc == false);
       rc = cache2.get(3, val);
       assert(rc);
       assert(val == "three");


【讨论】:

我已经考虑过了...为什么没有一个二维双向链表,其中第一维的每个元素是频率,第二维是缓存的哈希映射指向的元素有指向根的指针吗?执行get时,只需拉出一个双向链表,跟随指针指向根,向上移动一个并添加到下一个频率列表(或根据需要扩展数组)?【参考方案8】:

这里是基于here的Go/Golang中LFU缓存的简单实现。

import "container/list" 

type LFU struct 
    cache      map[int]*list.Element
    freqQueue  map[int]*list.List
    cap        int
    maxFreq    int
    lowestFreq int


type entry struct 
    key, val int
    freq     int


func NewLFU(capacity int) *LFU 
    return &LFU
        cache:      make(map[int]*list.Element),
        freqQueue:  make(map[int]*list.List),
        cap:        capacity,
        maxFreq:    capacity - 1,
        lowestFreq: 0,
    


// O(1)
func (c *LFU) Get(key int) int 
    if e, ok := c.cache[key]; ok 
        val := e.Value.(*entry).val
        c.updateEntry(e, val)
        return val
    

    return -1


// O(1)
func (c *LFU) Put(key int, value int) 
    if e, ok := c.cache[key]; ok 
        c.updateEntry(e, value)

     else 
        if len(c.cache) == c.cap 
            c.evict()
        

        if c.freqQueue[0] == nil 
            c.freqQueue[0] = list.New()
        
        e := c.freqQueue[0].PushFront(&entrykey, value, 0)
        c.cache[key] = e
        c.lowestFreq = 0
    


func (c *LFU) updateEntry(e *list.Element, val int) 
    key := e.Value.(*entry).key
    curFreq := e.Value.(*entry).freq

    c.freqQueue[curFreq].Remove(e)
    delete(c.cache, key)

    nextFreq := curFreq + 1
    if nextFreq > c.maxFreq 
        nextFreq = c.maxFreq
    

    if c.lowestFreq == curFreq && c.freqQueue[curFreq].Len() == 0 
        c.lowestFreq = nextFreq
    

    if c.freqQueue[nextFreq] == nil 
        c.freqQueue[nextFreq] = list.New()
    
    newE := c.freqQueue[nextFreq].PushFront(&entrykey, val, nextFreq)
    c.cache[key] = newE


func (c *LFU) evict() 
    back := c.freqQueue[c.lowestFreq].Back()
    delete(c.cache, back.Value.(*entry).key)
    c.freqQueue[c.lowestFreq].Remove(back)

【讨论】:

正如目前所写,您的答案尚不清楚。请edit 添加其他详细信息,以帮助其他人了解这如何解决所提出的问题。你可以找到更多关于如何写好答案的信息in the help center。

以上是关于如何实现最不常用(LFU)缓存?的主要内容,如果未能解决你的问题,请参考以下文章

[LeetCode] LFU Cache 最近最不常用页面置换缓存器

[LeetCode] 460. LFU Cache 最近最不常用页面置换缓存器

LFU(最近最不常用)实现(python)

LFU缓存实现

LFU -- Javascript实现版本

LFU -- Javascript实现版本