web-dev-qa-db-fra.com

Comment implémenter un cache LFU (Least Frequently Used)?

LFU (Least Frequently Used) est un type d'algorithme de cache utilisé pour gérer la mémoire d'un ordinateur. Les caractéristiques standard de cette méthode impliquent que le système garde la trace du nombre de fois qu'un bloc est référencé en mémoire. Lorsque le cache est plein et nécessite plus d'espace, le système purge l'élément avec la fréquence de référence la plus basse.

Quel serait le meilleur moyen d'implémenter le cache d'objets le plus récemment utilisé, par exemple en Java?

J'ai déjà mis en place une application avec LinkedHashMap (en conservant le nombre d'accès aux objets), mais je suis curieux de savoir si l'une des nouvelles collections simultanées serait de meilleurs candidats.

Prenons le cas suivant: supposons que le cache soit saturé et que nous devions laisser de la place pour un autre. Disons que deux objets sont notés dans le cache et qu’on y accède une seule fois. Lequel supprimer si nous en venons à savoir qu'un autre objet (qui n'est pas dans le cache) est utilisé plusieurs fois?

Merci!

14
Snehal Masne
  1. Selon moi, le meilleur moyen d'implémenter le cache d'objets le plus récemment utilisé serait d'inclure une nouvelle variable comme "latestTS" pour chaque objet. TS signifie horodatage.

    // Une méthode statique qui renvoie la date et l'heure actuelles en millisecondes depuis le 1er janvier 1970 Long latestTS = System.currentTimeMillis (); 

  2. ConcurrentLinkedHashMap n'est pas encore implémenté dans les collections Java simultanées. (Ref: API Java Concurrent Collection ). Cependant, vous pouvez essayer d'utiliser ConcurrentHashMap et DoublyLinkedList

  3. À propos du cas à considérer: dans ce cas, comme je l’ai dit, vous pouvez déclarer la variable latestTS, en fonction de la valeur de la variable latestTS, vous pouvez supprimer une entrée et ajouter le nouvel objet. (N'oubliez pas de mettre à jour la fréquence et les dernières informations du nouvel objet ajouté)

Comme vous l'avez mentionné, vous pouvez utiliser LinkedHashMap car cela donne un accès à l'élément dans O(1) et vous obtenez également la traversée de la commande . Veuillez trouver le code ci-dessous pour le cache LFU : (PS: le code ci-dessous est la réponse à la question dans le titre, à savoir "Comment implémenter le cache 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;
    }
}
2
Rakmo

Vous pourriez bénéficier de la mise en œuvre ActiveMQ de LFU: LFUCache

Ils ont fourni de bonnes fonctionnalités.

8
MozenRath

Je pense que la structure de données de la LFU doit combiner la file d'attente prioritaire (pour maintenir un accès rapide à l'élément lfu) et la table de hachage (pour fournir un accès rapide à n'importe quel élément à l'aide de sa clé); Je suggérerais la définition de noeud suivante pour chaque objet stocké dans le cache: 

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
}

Vous avez besoin de key pour faire référence à un élément . Vous avez besoin de numAccesses comme clé pour la file d'attente prioritaire . Vous devez currentPos pour pouvoir trouver rapidement une position pq d'article par clé . (clé (Integer) -> noeud (Node<T>)) pour accéder rapidement aux éléments et à la file d’attente de priorité minimale basée sur le tas en utilisant le nombre d’accès en tant que priorité. Maintenant, vous pouvez effectuer très rapidement toutes les opérations (accès, ajout d’un nouvel élément, mise à jour du nombre d’accès, suppression de lfu). Vous devez écrire chaque opération avec soin, de manière à ce que tous les nœuds soient cohérents (leur nombre d'accès, leur position dans pq et leur existence dans la table de hachage). Toutes les opérations fonctionneront avec une complexité temporelle moyenne constante, ce que vous attendez du cache.

3
heorhi

Que diriez-vous d'une file d'attente prioritaire? Vous pouvez conserver les éléments triés ici avec des clés représentant la fréquence. Il suffit de mettre à jour la position de l'objet dans la file d'attente après l'avoir visitée. Vous pouvez mettre à jour uniquement de temps en temps pour optimiser les performances (mais en réduisant la précision).

0
Łukasz Kidziński

De nombreuses implémentations que j'ai vues ont une complexité d'exécution O(log(n)). Cela signifie que, lorsque la taille du cache est n, le temps nécessaire pour insérer/supprimer un élément dans/de chache est logarithmique. De telles implémentations utilisent généralement un min heap pour maintenir les fréquences d'utilisation des éléments. La racine du segment contient l'élément dont la fréquence est la plus basse et est accessible en O(1) time. Mais pour conserver la propriété heap, nous devons déplacer un élément, chaque fois qu’il est utilisé (et que la fréquence est incrémentée) à l’intérieur du tas, afin de le positionner correctement ou lorsqu’il faut insérer un nouvel élément dans le cache (et ainsi de suite). mettez-le dans le tas) . Mais la complexité de l'exécution peut être réduite à O(1), lorsque nous maintenons une variable hashmap (Java) ou unordered_map (C++) avec l'élément en tant que clé. De plus, nous avons besoin de deux sortes de listes, frequency list et elements lists. Le elements lists contient des éléments ayant la même fréquence et le frequency list contient le element lists.

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

Ici, dans l'exemple, nous voyons le frequency list qui a 4 éléments (4 elements lists). La liste d'éléments 1 contient des éléments (a,c,m), la liste d'éléments 3 contient des éléments (k, l, n) etc . Maintenant, lorsque nous utilisons say élément y, nous devons incrémenter sa fréquence et la placer dans la liste suivante. Comme la liste des éléments de fréquence 6 devient vide, nous la supprimons. Le résultat est:

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

Nous plaçons l'élément y au début du elements list 7. Quand nous devrons supprimer des éléments de la liste plus tard, nous commencerons à la fin (d'abord z, puis x et ensuite y) . Maintenant, lorsque nous utilisons élément n, nous devons incrémenter sa fréquence et la mettre dans la nouvelle liste, avec les fréquences 4:

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

J'espère que l'idée est claire. Je fournis maintenant mon implémentation C++ du cache LFU et ajouterai plus tard une implémentation Java. La classe n'a que 2 méthodes publiques, void set(key k, value v) Et bool get(key k, value &v). Dans la méthode get, la valeur à extraire sera définie par référence lorsque l'élément est trouvé. Dans ce cas, la méthode renvoie true. Lorsque l'élément n'est pas trouvé, la méthode retourne 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;
    }
};

Voici des exemples d'utilisation:

    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");

}
0