I was looking for a in-memory cache class, but couldn't find any in the .Net Core framework; luckily I found one already implemented here.
I made some minor modifications to it and tried to make it thread safe. I believe I succeeded; but boy, did I do a sloppy job? Just throwing locks around and whatnot.
I'd like to reduce thread contention and improve its performance. I considered using the ReadWriterLockSlim* class; one instances for the private dict and one for the private linked list.
Can someone help me improve the performance of this class?
For reference, at any given time there may be up to 40 threads hammering down this cache.
I can't provide a real and concise usage example of this class right now; and I have not yet measured the following numbers, but I expect that most 4/5 cache accesses will result in a hit, and 1/5 in a miss followed by an add.
An actual, yet codeless, example of usage of this class is to store the fitness of (immutable) individuals in a genetic algorithm. Since the individuals are immutable, their fitnesses stay the same as long as they live; thus I can compute it once and cache the result.
The most up-to-date version of the code can be found in my own answer; since I received such great feedback and couldn't choose a single answer as the answer, I decided to compile the suggestions, and post the resulting code, in a single new answer. This way I'm able to share the final result with the community, acknowledge the help and still keep the question / answers valid.
I won't accept my answer as "the answer" because I don't want to fragsteal.
namespace Minotaur.Collections {
using System;
using System.Collections.Generic;
/// <remarks>
/// This class is based on the following inplementation https://stackoverflow.com/a/3719378/1642116
/// </remarks>
public sealed class LruCache<K, V> {
private readonly int _capacity;
private readonly Dictionary<K, LinkedListNode<LRUCacheEntry<K, V>>> _cacheMap;
private readonly LinkedList<LRUCacheEntry<K, V>> _lruList = new LinkedList<LRUCacheEntry<K, V>>();
private readonly object _lock = new object();
/// <remarks>
/// Providing the value 0 to <paramref name="capacity"/>
/// effectively disables the cache.
/// </remarks>
public LruCache(int capacity) {
if (capacity < 0)
throw new ArgumentOutOfRangeException(nameof(capacity) + " must be >= 0");
this._capacity = capacity;
this._cacheMap = new Dictionary<K, LinkedListNode<LRUCacheEntry<K, V>>>(capacity: capacity);
}
/// <remarks>
/// This method is thread-safe.
/// </remarks>
public void Add(K key, V value) {
if (key == null)
throw new ArgumentNullException(nameof(key));
lock (_lock) {
if (_capacity == 0)
return;
if (_cacheMap.Count >= _capacity)
RemoveLastRecentlyUsed();
var cacheItem = new LRUCacheEntry<K, V>(key, value);
var node = new LinkedListNode<LRUCacheEntry<K, V>>(cacheItem);
_lruList.AddLast(node);
_cacheMap.Add(key, node);
}
}
/// <remarks>
/// This method is thread-safe.
/// </remarks>
public bool TryGet(K key, out V value) {
if (key == null)
throw new ArgumentNullException(nameof(key));
lock (_lock) {
var isCached = _cacheMap.TryGetValue(key, out var node);
if (!isCached) {
value = default;
return false;
}
// Updating the last recently used thingy
_lruList.Remove(node);
_lruList.AddLast(node);
value = node.Value.Value;
return true;
}
}
private void RemoveLastRecentlyUsed() {
// Remove from lru list
var node = _lruList.First;
_lruList.RemoveFirst();
// Remove from cache
_cacheMap.Remove(node.Value.Key);
}
private sealed class LRUCacheEntry<KeyType, ValueType> {
public readonly KeyType Key;
public readonly ValueType Value;
public LRUCacheEntry(KeyType k, ValueType v) {
Key = k;
Value = v;
}
}
}
}
*then I remembered I don't really know how to use it correctly and would probably end up increasing contention...
TValue GetOrCreate(TKey key, Func<TValue> valueCreator)
function", that's a lot harder than it sounds (if you want to guarantee a single call tovalueCreate
). See this question, and if I may suggest reading point 3 in my answer. Also, you shouldn't change the code in the question: that would be welcome as a new question. \$\endgroup\$