38

I would like to implement a simple in-memory LRU cache system and I was thinking about a solution based on an IDictionary implementation which could handle an hashed LRU mechanism. Coming from java, I have experiences with LinkedHashMap, which works fine for what I need: I can't find anywhere a similar solution for .NET.

Has anyone developed it or has anyone had experiences like this?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Antonello
  • 1,326
  • 2
  • 16
  • 23

13 Answers13

62

This a very simple and fast implementation we developed for a web site we own.

We tried to improve the code as much as possible, while keeping it thread safe. I think the code is very simple and clear, but if you need some explanation or a guide related to how to use it, don't hesitate to ask.

namespace LRUCache
{
    public class LRUCache<K,V>
    {
        private int capacity;
        private Dictionary<K, LinkedListNode<LRUCacheItem<K, V>>> cacheMap = new Dictionary<K, LinkedListNode<LRUCacheItem<K, V>>>();
        private LinkedList<LRUCacheItem<K, V>> lruList = new LinkedList<LRUCacheItem<K, V>>();

        public LRUCache(int capacity)
        {
            this.capacity = capacity;
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public V get(K key)
        {
            LinkedListNode<LRUCacheItem<K, V>> node;
            if (cacheMap.TryGetValue(key, out node))
            {
                V value = node.Value.value;
                lruList.Remove(node);
                lruList.AddLast(node);
                return value;
            }
            return default(V);
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public void add(K key, V val)
        {
            if (cacheMap.TryGetValue(key, out var existingNode))
            {
                lruList.Remove(existingNode);
            }
            else if (cacheMap.Count >= capacity)
            {
                RemoveFirst();
            }

            LRUCacheItem<K, V> cacheItem = new LRUCacheItem<K, V>(key, val);
            LinkedListNode<LRUCacheItem<K, V>> node = new LinkedListNode<LRUCacheItem<K, V>>(cacheItem);
            lruList.AddLast(node);
            // cacheMap.Add(key, node); - here's bug if try to add already existing value
            cacheMap[key] = node;
        }

        private void RemoveFirst()
        {
            // Remove from LRUPriority
            LinkedListNode<LRUCacheItem<K,V>> node = lruList.First;
            lruList.RemoveFirst();

            // Remove from cache
            cacheMap.Remove(node.Value.key);
        }
    }

    class LRUCacheItem<K,V>
    {
        public LRUCacheItem(K k, V v)
        {
            key = k;
            value = v;
        }
        public K key;
        public V value;
    }
}
GSerjo
  • 4,725
  • 1
  • 36
  • 55
Martin
  • 1
  • 2
  • 3
  • 1
    It looks like the code formatting didn't come out correctly. Please confirm my edit fixed it to your liking. – Redwood Sep 15 '10 at 15:53
  • 6
    Small bug in this. `add` may throw an exception after adding the node to the `lruList` if `key` already exists in `cacheMap`. To fix, either invert the order of the method calls so `cacheMap.Add` call is first or add code to check if the key already exists and deal with that differently (i.e. handle as a change and just adjust the `lruList`). – Kevin Brock Sep 12 '11 at 17:36
  • Won't the get and add methods conflict with each other potentially causing the internal structures to become corrupted since the sync is on the method? http://stackoverflow.com/a/6140261/910348 – ThisGuy Feb 25 '14 at 05:02
  • 6
    Please correct me if I'm wrong, but can't we eliminate LRUCacheItem class entirely and just keep a LinkedList and Dictionary? – mclaassen Feb 27 '14 at 20:00
  • 1
    This code works great, however I refactored it to remove the used of the [MethodImpl] attribute and instead lock on cacheMap in both add() and get(). I also added a get() overload that takes a Func so that it can do a "get or add" operation with one call rather than checking for null from get() and then calling add() (a sequence which can never be made thread-safe). And while I was in there, I took @mclaassen's suggestion and got rid of the (unnecessary) LRUCacheItem class. – Mr. T Jul 13 '15 at 18:52
  • 10
    @mclaassen LRUCacheItem is needed here because you would want to use LinkedList.Remove(LinkedListNode) which is O(1) instead of LinkedList.Remove(Value) which is O(n), this makes a very big performance difference. So with Dictionary) you can get LinkedListNode by key and then remove it from the LinkedList in O(1) – Godsent Nov 04 '15 at 11:25
  • 4
    Just noticed that there is no MethodImplOptions.Synchronized on .NET Core but it's possible to implement the same functionality with a lock statement. – Malte Goetz Jul 05 '17 at 14:34
  • 5
    There is a bug in add() where if the key already exists lruList is still appended with a new node, leaving the old node in the list. The fix is to check if a node already exists (cacheMap.TryGetValue()) and remove it if it does. – Austin Hanson Jun 26 '18 at 16:37
  • Which is faster? The above LRU, or the in memory cache of ASP.NET C#? – Pangamma Feb 25 '19 at 22:49
  • 1
    @AustinHanson the bug can be resolved transforming the method in AddOrReplace() and make an if (cacheMap.ContainsKey(key)) { cacheMap[key].Value.value = val; return; } – cpsaez May 07 '19 at 22:17
  • @cpsaez Nearly. The AddOrReplace would have to remove the key-value (that already exists in the cache) from the lruList and place the new key-value onto the back on the last of the lruList, otherwise its not really last-recently-used. – andrew pate Jun 10 '19 at 14:23
  • With regard to my previous comment. I guess it depends if you consider adding as 'using' a key-value pair. Maybe in some cases you don't. – andrew pate Jun 10 '19 at 14:43
10

There is nothing in the base class libraries that does this.

On the free side, maybe something like C5's HashedLinkedList would work.

If you're willing to pay, maybe check out this C# toolkit. It contains an implementation.

Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • 1
    @Antonello, the linked c5 doesn't seem to order by access (as LinkedHashMap is capable of doing). Am I missing something, or did you remove and reinsert items when they were accessed? – zod Feb 28 '13 at 17:16
6

I've recently released a class called LurchTable to address the need for a C# variant of the LinkedHashMap. A brief discussion of the LurchTable can be found here.

Basic features:

  • Linked Concurrent Dictionary by Insertion, Modification, or Access
  • Dictionary/ConcurrentDictionary interface support
  • Peek/TryDequeue/Dequeue access to 'oldest' entry
  • Allows hard-limit on items enforced at insertion
  • Exposes events for add, update, and remove

Source Code: http://csharptest.net/browse/src/Library/Collections/LurchTable.cs

GitHub: https://github.com/csharptest/CSharpTest.Net.Collections

HTML Help: http://help.csharptest.net/

PM> Install-Package CSharpTest.Net.Collections

Chris Marisic
  • 32,487
  • 24
  • 164
  • 258
csharptest.net
  • 62,602
  • 11
  • 71
  • 89
6

The LRUCache answer with sample code above (by Martin) uses MethodImplOptions.Synchronized, which is equivalent to putting lock(this) around each method call. Whilst correct, this global lock will significantly reduce throughput under concurrent load.

To solve this I implemented a thread safe pseudo LRU designed for concurrent workloads. Performance is very close to ConcurrentDictionary, ~10x faster than MemoryCache and hit rate is better than a conventional LRU. Full analysis provided in the GitHub link below.

Usage looks like this:

int capacity = 500;
var lru = new ConcurrentLru<int, SomeItem>(capacity);

var value = lru.GetOrAdd(1, (k) => new SomeItem(k));

GitHub: https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Alex Peck
  • 4,603
  • 1
  • 33
  • 37
4

This takes Martin's code with Mr T's suggestions and makes it Stylecop friendly. Oh, it also allows for disposal of values as they cycle out of the cache.

namespace LruCache
{
    using System;
    using System.Collections.Generic;

    /// <summary>
    /// A least-recently-used cache stored like a dictionary.
    /// </summary>
    /// <typeparam name="TKey">
    /// The type of the key to the cached item
    /// </typeparam>
    /// <typeparam name="TValue">
    /// The type of the cached item.
    /// </typeparam>
    /// <remarks>
    /// Derived from https://stackoverflow.com/a/3719378/240845
    /// </remarks>
    public class LruCache<TKey, TValue>
    {
        private readonly Dictionary<TKey, LinkedListNode<LruCacheItem>> cacheMap =
            new Dictionary<TKey, LinkedListNode<LruCacheItem>>();

        private readonly LinkedList<LruCacheItem> lruList =
            new LinkedList<LruCacheItem>();

        private readonly Action<TValue> dispose;

        /// <summary>
        /// Initializes a new instance of the <see cref="LruCache{TKey, TValue}"/>
        /// class.
        /// </summary>
        /// <param name="capacity">
        /// Maximum number of elements to cache.
        /// </param>
        /// <param name="dispose">
        /// When elements cycle out of the cache, disposes them. May be null.
        /// </param>
        public LruCache(int capacity, Action<TValue> dispose = null)
        {
            this.Capacity = capacity;
            this.dispose = dispose;
        }

        /// <summary>
        /// Gets the capacity of the cache.
        /// </summary>
        public int Capacity { get; }

        /// <summary>Gets the value associated with the specified key.</summary>
        /// <param name="key">
        /// The key of the value to get.
        /// </param>
        /// <param name="value">
        /// When this method returns, contains the value associated with the specified
        /// key, if the key is found; otherwise, the default value for the type of the 
        /// <paramref name="value" /> parameter. This parameter is passed
        /// uninitialized.
        /// </param>
        /// <returns>
        /// true if the <see cref="T:System.Collections.Generic.Dictionary`2" /> 
        /// contains an element with the specified key; otherwise, false.
        /// </returns>
        public bool TryGetValue(TKey key, out TValue value)
        {
            lock (this.cacheMap)
            {
                LinkedListNode<LruCacheItem> node;
                if (this.cacheMap.TryGetValue(key, out node))
                {
                    value = node.Value.Value;
                    this.lruList.Remove(node);
                    this.lruList.AddLast(node);
                    return true;
                }

                value = default(TValue);
                return false;
            }
        }

        /// <summary>
        /// Looks for a value for the matching <paramref name="key"/>. If not found, 
        /// calls <paramref name="valueGenerator"/> to retrieve the value and add it to
        /// the cache.
        /// </summary>
        /// <param name="key">
        /// The key of the value to look up.
        /// </param>
        /// <param name="valueGenerator">
        /// Generates a value if one isn't found.
        /// </param>
        /// <returns>
        /// The requested value.
        /// </returns>
        public TValue Get(TKey key, Func<TValue> valueGenerator)
        {
            lock (this.cacheMap)
            {
                LinkedListNode<LruCacheItem> node;
                TValue value;
                if (this.cacheMap.TryGetValue(key, out node))
                {
                    value = node.Value.Value;
                    this.lruList.Remove(node);
                    this.lruList.AddLast(node);
                }
                else
                {
                    value = valueGenerator();
                    if (this.cacheMap.Count >= this.Capacity)
                    {
                        this.RemoveFirst();
                    }

                    LruCacheItem cacheItem = new LruCacheItem(key, value);
                    node = new LinkedListNode<LruCacheItem>(cacheItem);
                    this.lruList.AddLast(node);
                    this.cacheMap.Add(key, node);
                }

                return value;
            }
        }

        /// <summary>
        /// Adds the specified key and value to the dictionary.
        /// </summary>
        /// <param name="key">
        /// The key of the element to add.
        /// </param>
        /// <param name="value">
        /// The value of the element to add. The value can be null for reference types.
        /// </param>
        public void Add(TKey key, TValue value)
        {
            lock (this.cacheMap)
            {
                if (this.cacheMap.Count >= this.Capacity)
                {
                    this.RemoveFirst();
                }

                LruCacheItem cacheItem = new LruCacheItem(key, value);
                LinkedListNode<LruCacheItem> node = 
                    new LinkedListNode<LruCacheItem>(cacheItem);
                this.lruList.AddLast(node);
                this.cacheMap.Add(key, node);
            }
        }

        private void RemoveFirst()
        {
            // Remove from LRUPriority
            LinkedListNode<LruCacheItem> node = this.lruList.First;
            this.lruList.RemoveFirst();

            // Remove from cache
            this.cacheMap.Remove(node.Value.Key);

            // dispose
            this.dispose?.Invoke(node.Value.Value);
        }

        private class LruCacheItem
        {
            public LruCacheItem(TKey k, TValue v)
            {
                this.Key = k;
                this.Value = v;
            }

            public TKey Key { get; }

            public TValue Value { get; }
        }
    }
}
Community
  • 1
  • 1
mheyman
  • 4,211
  • 37
  • 34
  • There's some code repetition between `TryGetValue`, `Add` and `Get`. Can you not implement `Get` by calling the other two? – Dejan Jul 19 '17 at 02:02
  • If you implement Get() in terms of first TryGetValue(), and, if that fails, do an Add(), you create a latent bug for when another thread sneaks in an Add() between your two calls. Having said that, you can certainly extract common code into private methods to be called once the lock is properly held. – mheyman Jul 24 '17 at 11:15
4

Found you answer while googling, also found this:

http://code.google.com/p/csharp-lru-cache/

csharp-lru-cache: LRU cache collection class library

This is a collection class that functions as a least-recently-used cache. It implements ICollection<T>, but also exposes three other members:

  • Capacity, the maximum number of items the cache can contain. Once the collection is at capacity, adding a new item to the cache will cause the least recently used item to be discarded. If the Capacity is set to 0 at construction, the cache will not automatically discard items.
  • Oldest, the oldest (i.e. least recently used) item in the collection.
  • DiscardingOldestItem, an event raised when the cache is about to discard its oldest item. This is an extremely simple implementation. While its Add and Remove methods are thread-safe, it shouldn't be used in heavy multithreading environments because the entire collection is locked during those methods.
Joel Purra
  • 24,294
  • 8
  • 60
  • 60
mcintyre321
  • 12,996
  • 8
  • 66
  • 103
3

The Caching Application Block of EntLib has an LRU scavenging option out of the box and can be in memory. It might be a bit heavyweight for what you want tho.

JP Alioto
  • 44,864
  • 6
  • 88
  • 112
2

I like Lawrence's implementation. Hashtable + LinkedList is a good solution.

Regarding threading, I would not lock this with[MethodImpl(MethodImplOptions.Synchronized)], but rather use ReaderWriterLockSlim or spin lock (since contention usually fast) instead.

In the Get function I would check if it's already the 1st item first, rather than always removing and adding. This gives you the possibility to keep that within a reader lock that is not blocking other readers.

janw
  • 8,758
  • 11
  • 40
  • 62
Jerry Ju
  • 21
  • 1
2

I don't believe so. I've certainly seen hand-rolled ones implemented several times in various unrelated projects (which more or less confirms this. If there was one, surely at least one of the projects would have used it).

It's pretty simple to implement, and usually gets done by creating a class which contains both a Dictionary and a List.

The keys go in the list (in-order) and the items go in the dictionary.
When you Add a new item to the collection, the function checks the length of the list, pulls out the last Key (if it's too long) and then evicts the key and value from the dictionary to match. Not much more to it really

Orion Edwards
  • 121,657
  • 64
  • 239
  • 328
  • this is an easy and working solution, although there's a loss of performances by double accessing two collections (first the List to retrieve the key and then the Dictionary to retrieve the value), but it's working... – Antonello Apr 16 '09 at 17:20
1

I just accidently found now LruCache.cs in aws-sdk-net: https://github.com/aws/aws-sdk-net/blob/master/sdk/src/Core/Amazon.Runtime/Internal/Util/LruCache.cs

Stanislav
  • 4,389
  • 2
  • 33
  • 35
1

Here is a modern implementation of a LRUCache<TKey, TValue> collection, for .NET 6 and later. The main feature is the method GetOrAdd. This method either returns an existing value, or invokes the valueFactory and returns a new value. Each time a new value is added, the boundedCapacity policy is enforced by evicting the least recently used value from the collection. The valueFactory is invoked lazily, so that multiple concurrent GetOrAdd calls for the same key receive the same value.

public class LRUCache<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>>
{
    private readonly Dictionary<TKey, LinkedListNode<Entry>> _dictionary;
    private readonly LinkedList<Entry> _linkedList;
    private readonly int _boundedCapacity;

    private readonly record struct Entry(TKey Key, Lazy<TValue> Lazy);

    public LRUCache(int boundedCapacity, IEqualityComparer<TKey> comparer = default)
    {
        if (boundedCapacity < 0)
            throw new ArgumentOutOfRangeException(nameof(boundedCapacity));
        _dictionary = new(boundedCapacity + 1, comparer);
        _linkedList = new();
        _boundedCapacity = boundedCapacity;
    }

    private object SyncRoot => _dictionary;
    public int Count { get { lock (SyncRoot) return _dictionary.Count; } }

    public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
    {
        ArgumentNullException.ThrowIfNull(valueFactory);
        Lazy<TValue> lazy;
        lock (SyncRoot)
        {
            ref LinkedListNode<Entry> refNode = ref CollectionsMarshal
                .GetValueRefOrAddDefault(_dictionary, key, out bool exists);
            if (exists)
            {
                lazy = refNode.Value.Lazy;
                if (!ReferenceEquals(refNode, _linkedList.Last))
                {
                    _linkedList.Remove(refNode);
                    _linkedList.AddLast(refNode);
                }
            }
            else
            {
                lazy = new(() => valueFactory(key));
                refNode = new(new Entry(key, lazy));
                _linkedList.AddLast(refNode);
                if (_dictionary.Count > _boundedCapacity)
                {
                    bool removed = _dictionary.Remove(_linkedList.First.Value.Key);
                    Debug.Assert(removed);
                    _linkedList.RemoveFirst();
                }
            }
            Debug.Assert(_dictionary.Count == _linkedList.Count);
        }
        return lazy.Value;
    }

    public void Clear()
    {
        lock (SyncRoot)
        {
            _dictionary.Clear();
            _linkedList.Clear();
        }
    }

    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        lock (SyncRoot)
        {
            return _linkedList
                .ToArray()
                .Select((Entry e) => KeyValuePair.Create(e.Key, e.Lazy.Value))
                .AsEnumerable()
                .GetEnumerator();
        }
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

Usage example:

LRUCache<string, object> cache = new(30);
object value = cache.GetOrAdd("SomeKey", key => GetObject(key));

The advanced API CollectionsMarshal.GetValueRefOrAddDefault is used so that the key is hashed only once per GetOrAdd call.

In case the valueFactory fails, the behavior of the Lazy<T> class is to cache permanently the exception. This behavior might not be suitable for a caching system, so you may want to substitute the Lazy<T> with the simple LazyWithRetry<T> implementation that I have posted here.

In case you would like to use an asynchronous valueFactory, there are AsyncLazy<T> implementations in this question.

The LRUCache<TKey, TValue> class is thread-safe.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
0

If it's an asp.net app you can use the cache class[1] but you'll be competing for space with other cached stuff, which may be what you want or may not be.

[1] http://msdn.microsoft.com/en-us/library/system.web.caching.cache.aspx

Tony Lee
  • 5,622
  • 1
  • 28
  • 45
0

There is OrderedDictionary

using System.Collections.Specialized;

You can remove a element by key and (re)insert it on the end of the order. When you need memory remove the first element in the order.

This shows how, but its a trifle slower:

https://leetcode.com/problems/lru-cache/solutions/1065496/c-two-implementationsordered-dictionary-linkedlist-and-their-comparison-with-explanation/

andrew pate
  • 3,833
  • 36
  • 28