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

27

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

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

我已经使用LinkedHashMap实现了一个缓存(通过维护对象的访问次数),但我想知道是否有任何新的并发集合可以更好地胜任这个任务。

考虑这种情况:假设缓存已满且我们需要为另一个对象腾出空间。 假设缓存中有两个对象只被访问了一次。如果我们得知其他(不在缓存中)对象被访问了多次,那么应该移除哪个对象呢?

谢谢!


1
使用现有的实现,例如来自Guava的实现。 - Boris the Spider
1
为每个缓存条目保留一个计数器,并在每次成功引用时递增相应的计数器。如果计数器溢出,则将所有计数器除以2(或4或其他值)以“重新调整”它们。计数最低的条目是最不经常使用的。 - Hot Licks
8个回答

13
你可能会从ActiveMQ的LFU实现中受益: LFUCache。他们提供了一些很不错的功能。

6

我认为,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 快速查找具有相应键的项目在优先队列中的位置。 现在,你可以通过哈希映射 (key(Integer) -> node(Node<T>)) 和基于最小堆的优先队列(以访问次数作为优先级)来快速访问项目和添加新项目、更新访问次数以及移除最近不常用的项目。你需要仔细编写每个操作,以确保它们保持节点的一致性(其访问次数、在优先队列中的位置以及在哈希映射中的存在)。所有操作将具有恒定的平均时间复杂度,这正是你从高速缓存中期望的。

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

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

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

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

如您所提到的,您可以使用LinkedHashMap,因为它的元素访问时间复杂度为O(1),并且您还可以获得顺序遍历。请查看以下代码实现LFU缓存:(注:以下代码是标题中“如何实现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;
    }
}

1
如果在添加到映射中时,键相同但值不同,我们可能只需要增加键的频率并将其值设置为不同。 - Raj Hassani
使用此算法,驱逐操作将在O(n)时间内运行,但如果使用堆,则可能达到O(log(n))的时间复杂度。 - Cameron Hudson
一旦缓存已满,每个新添加的条目都会在映射中进行线性搜索,以查找要删除的最佳条目。这太可怕了。在我的实现中,我允许映射增长到其目标大小的两倍,然后进行重建,删除一半的条目:重建的成本更高,但其频率要低得多。 - Michael Kay

3

2
虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供链接以供参考。仅有链接的答案如果链接页面发生更改,则可能变得无效。 - Til
除非你是谷歌,否则PDF链接并不是很有用。如果你已经阅读了这篇论文,请总结一下它的内容。 - Abhijit Sarkar
我阅读了链接,作者建议使用以下数据结构: 1)HashMap 2)LinkedList 用于存储/连接按频率排序的项目 3)LinkedList 用于存储相同频率的元素。 - ondway

1
我尝试实现了下面的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;
    }
}

0

这里是一个基于这里的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(&entry{key, 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(&entry{key, 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)
}

你的回答目前不够清晰。请[编辑]以添加更多细节,帮助他人理解如何解答该问题。您可以在帮助中心了解有关编写良好答案的更多信息。 - Community

0

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


你是如何计算关键值的? - Hot Licks
我会使用一个全局计数器C,来计算所有的使用次数。然后对于每个对象o,我会记住它创建时的start(o)值和该元素的使用次数uses(o)。最后,这个对象的频率由uses(o) / (C - start(o))给出。当然,需要解决一些细节问题,但我希望这能解释清楚这个想法。 - Łukasz Kidziński
队列只允许访问其头部,因此不能用于缓存。 - MozenRath

0
我看到的许多实现都具有运行时复杂度为O(log(n))。这意味着,当缓存大小为n时,将元素插入/从缓存中删除所需的时间是对数级别的。这些实现通常使用最小堆来维护元素的使用频率。堆的根包含具有最低频率的元素,并且可以在O(1)的时间内访问。但是,为了维护堆属性,我们必须每次在堆内使用元素(并增加频率)时将其移动到正确的位置,或者在将新元素插入缓存时(因此将其放入堆中)。 但是,当我们使用元素作为键维护哈希表(Java)或unordered_map(C++)时,运行时复杂度可以降至O(1)。此外,我们需要两种类型的列表:频率列表元素列表元素列表包含具有相同频率的元素,而频率列表包含元素列表
  frequency list
  1   3   6   7
  a   k   y   x
  c   l       z
  m   n

在这个例子中,我们看到有一个包含4个元素列表(4个元素列表)的频率列表。元素列表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 &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操作时,只需从一个双向链表中取出,跟随指针到达根,向上移动一个并添加到下一个频率列表(或扩展数组,如果必要的话)即可。 - Sam Keays

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接