如何实现最低频繁使用(LFU)缓存?

时间:2014-01-14 15:45:07

标签: java algorithm caching

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

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

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

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

谢谢!

7 个答案:

答案 0 :(得分:8)

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

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

答案 1 :(得分:4)

我认为,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位置。 现在,您可以组织哈希映射(键(Integer) - &gt;节点(Node<T>))以使用访问次数作为优先级快速访问项目和最小的基于堆的优先级队列。现在,您可以非常快速地执行所有操作(访问,添加新项目,更新加入次数,删除lfu)。您需要仔细编写每个操作,以便它保持所有节点的一致性(它们的访问次数,它们在pq中的位置以及哈希映射中是否存在)。所有操作都将以恒定的平均时间复杂度工作,这是您对缓存的期望。

答案 2 :(得分:0)

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

答案 3 :(得分:0)

  1. 据我所知,实现最近使用的对象缓存的最佳方法是为每个对象包含一个新变量'latestTS'。 TS代表时间戳。

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

  2. ConcurrentLinkedHashMap尚未在Concurrent Java Collections中实现。 (参考:Java Concurrent Collection API)。但是,您可以尝试使用ConcurrentHashMapDoublyLinkedList

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

  4. 正如您所提到的,您可以使用LinkedHashMap,因为它在O(1)中提供了元素访问权限,并且您还可以获得订单遍历。 请找下LFU Cache的以下代码: (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;
        }
    }
    

答案 4 :(得分:0)

我见过的许多实现都有运行时复杂性O(log(n))。这意味着,当缓存大小为n时,将元素插入/移出chache所需的时间是对数的。这样的实现通常使用min heap来维护元素的使用频率。堆的根包含频率最低的元素,可以在O(1)时间访问。但是要维护heap属性,我们必须每次在堆内部使用元素(并增加频率),将其放置在适当的位置时,或者当我们必须将新元素插入高速缓存时,都必须移动元素(等等)放入堆中)。 但是,当我们使用元素作为键维护O(1)(Java)或hashmap(C ++)时,运行时复杂度可以降低到unordered_map。另外,我们需要两种列表,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,然后是{{1} },然后x)。 现在,当我们使用元素y时,我们必须增加其频率并将其放入频率为4的新列表中:

n

我希望这个想法很明确。现在,我提供LFU缓存的C ++实现,并将在以后添加Java实现。 该类只有2个公共方法, frequency list 1 3 4 7 a k n y c l x m z void set(key k, value v)。在get方法中,找到元素时将为每个引用设置要检索的值,在这种情况下,该方法返回true。当找不到该元素时,该方法返回false。

bool get(key k, value &v)

以下是用法示例:

#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;
    }
};

答案 5 :(得分:0)

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

答案 6 :(得分:0)

我已经尝试在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;
    }
}