diff --git a/BitFaster.Caching.Benchmarks/Lru/LruCycle2.cs b/BitFaster.Caching.Benchmarks/Lru/LruCycle2.cs new file mode 100644 index 00000000..263f2758 --- /dev/null +++ b/BitFaster.Caching.Benchmarks/Lru/LruCycle2.cs @@ -0,0 +1,133 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using BitFaster.Caching.Lru; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.Caching; +using System.Text; + +namespace BitFaster.Caching.Benchmarks.Lru +{ + [MemoryDiagnoser] + public class LruCycle2 + { + const int capacity = 9; + const int iters = 10; + + private static readonly ConcurrentDictionary dictionary = new ConcurrentDictionary(8, 9, EqualityComparer.Default); + + private static readonly ClassicLru classicLru = new ClassicLru(8, capacity, EqualityComparer.Default); + private static readonly ConcurrentLru concurrentLru = new ConcurrentLru(8, capacity, EqualityComparer.Default); + private static readonly ConcurrentTLru concurrentTLru = new ConcurrentTLru(8, capacity, EqualityComparer.Default, TimeSpan.FromMinutes(1)); + private static readonly FastConcurrentLru fastConcurrentLru = new FastConcurrentLru(8, capacity, EqualityComparer.Default); + private static readonly FastConcurrentTLru fastConcurrentTLru = new FastConcurrentTLru(8, capacity, EqualityComparer.Default, TimeSpan.FromMinutes(1)); + + private static MemoryCache memoryCache = System.Runtime.Caching.MemoryCache.Default; + + [Benchmark(Baseline = true, OperationsPerInvoke = 24)] + public void ConcurrentDictionary() + { + Func func = x => x; + + for (int j = 0; j < iters; j++) + for (int i = 0; i < capacity + 1; i++) + { + dictionary.GetOrAdd(i, func); + + // simulate what the LRU does + if (i == capacity) + { + dictionary.TryRemove(1, out var removed); + } + } + } + + [Benchmark(OperationsPerInvoke = 24)] + public void FastConcurrentLru() + { + Func func = x => x; + + for (int j = 0; j < iters; j++) + for (int i = 0; i < capacity + 1; i++) + { + fastConcurrentLru.GetOrAdd(i, func); + } + } + + [Benchmark(OperationsPerInvoke = 24)] + public void ConcurrentLru() + { + Func func = x => x; + + for (int j = 0; j < iters; j++) + for (int i = 0; i < capacity + 1; i++) + { + concurrentLru.GetOrAdd(i, func); + } + } + + [Benchmark(OperationsPerInvoke = 24)] + public void FastConcurrentTLru() + { + Func func = x => x; + + for (int j = 0; j < iters; j++) + for (int i = 0; i < capacity + 1; i++) + { + fastConcurrentTLru.GetOrAdd(i, func); + } + } + + [Benchmark(OperationsPerInvoke = 24)] + public void ConcurrentTLru() + { + Func func = x => x; + + for (int j = 0; j < iters; j++) + for (int i = 0; i < capacity + 1; i++) + { + concurrentTLru.GetOrAdd(i, func); + } + } + + [Benchmark(OperationsPerInvoke = 24)] + public void ClassicLru() + { + Func func = x => x; + + for (int j = 0; j < iters; j++) + for (int i = 0; i < capacity + 1; i++) + { + classicLru.GetOrAdd(i, func); + } + } + + [Benchmark(OperationsPerInvoke = 24)] + public void MemoryCache() + { + + Func func = x => x; + + for (int j = 0; j < iters; j++) + for (int i = 0; i < capacity + 1; i++) + { + string key = i.ToString(); + var v = memoryCache.Get(key); + + if (v == null) + { + memoryCache.Set(key, "test", new CacheItemPolicy()); + } + + // simulate what the LRU does + if (i == capacity) + { + memoryCache.Remove("1"); + } + } + } + } +} diff --git a/BitFaster.Caching.Benchmarks/Program.cs b/BitFaster.Caching.Benchmarks/Program.cs index aa66d4ed..99e04292 100644 --- a/BitFaster.Caching.Benchmarks/Program.cs +++ b/BitFaster.Caching.Benchmarks/Program.cs @@ -15,7 +15,7 @@ class Program static void Main(string[] args) { var summary = BenchmarkRunner - .Run(ManualConfig.Create(DefaultConfig.Instance) + .Run(ManualConfig.Create(DefaultConfig.Instance) .AddJob(Job.RyuJitX64)); } } diff --git a/BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs b/BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs index b3cb7d5a..2c2eee46 100644 --- a/BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs @@ -16,7 +16,31 @@ public class ClassicLruTests private ClassicLru lru = new ClassicLru(1, capacity, EqualityComparer.Default); ValueFactory valueFactory = new ValueFactory(); - [Fact] + [Fact] + public void WhenConcurrencyIsLessThan1CtorThrows() + { + Action constructor = () => { var x = new ClassicLru(0, 3, EqualityComparer.Default); }; + + constructor.Should().Throw(); + } + + [Fact] + public void WhenCapacityIsLessThan3CtorThrows() + { + Action constructor = () => { var x = new ClassicLru(1, 2, EqualityComparer.Default); }; + + constructor.Should().Throw(); + } + + [Fact] + public void WhenComparerIsNullCtorThrows() + { + Action constructor = () => { var x = new ClassicLru(1, 3, null); }; + + constructor.Should().Throw(); + } + + [Fact] public void WhenItemIsAddedCountIsCorrect() { lru.Count.Should().Be(0); @@ -149,7 +173,37 @@ public void WhenMoreKeysRequestedThanCapacityOldestItemIsEvicted() valueFactory.timesCalled.Should().Be(capacity + 2); } - [Fact] + [Fact] + public void WhenValueExpiresItIsDisposed() + { + var lruOfDisposable = new ClassicLru(1, 6, EqualityComparer.Default); + var disposableValueFactory = new DisposableValueFactory(); + + for (int i = 0; i < 7; i++) + { + lruOfDisposable.GetOrAdd(i, disposableValueFactory.Create); + } + + disposableValueFactory.Items[0].IsDisposed.Should().BeTrue(); + disposableValueFactory.Items[1].IsDisposed.Should().BeFalse(); + } + + [Fact] + public async Task WhenValueExpiresAsyncItIsDisposed() + { + var lruOfDisposable = new ClassicLru(1, 6, EqualityComparer.Default); + var disposableValueFactory = new DisposableValueFactory(); + + for (int i = 0; i < 7; i++) + { + await lruOfDisposable.GetOrAddAsync(i, disposableValueFactory.CreateAsync); + } + + disposableValueFactory.Items[0].IsDisposed.Should().BeTrue(); + disposableValueFactory.Items[1].IsDisposed.Should().BeFalse(); + } + + [Fact] public void WhenKeyDoesNotExistTryGetReturnsFalse() { lru.GetOrAdd(1, valueFactory.Create); @@ -176,7 +230,19 @@ public void WhenKeyExistsTryRemoveRemovesItemAndReturnsTrue() lru.TryGet(1, out var value).Should().BeFalse(); } - [Fact] + [Fact] + public void WhenItemIsRemovedItIsDisposed() + { + var lruOfDisposable = new ClassicLru(1, 6, EqualityComparer.Default); + var disposableValueFactory = new DisposableValueFactory(); + + lruOfDisposable.GetOrAdd(1, disposableValueFactory.Create); + lruOfDisposable.TryRemove(1); + + disposableValueFactory.Items[1].IsDisposed.Should().BeTrue(); + } + + [Fact] public void WhenKeyDoesNotExistTryRemoveReturnsFalse() { lru.GetOrAdd(1, valueFactory.Create); diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs index 4b0f9726..63944049 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs @@ -365,29 +365,5 @@ public void WhenRepeatedlyAddingAndRemovingSameValueLruRemainsInConsistentState( lru.TryRemove(1); } } - - private class DisposableItem : IDisposable - { - public bool IsDisposed { get; private set; } - - public void Dispose() - { - this.IsDisposed = true; - } - } - - private class DisposableValueFactory - { - private Dictionary items = new Dictionary(); - - public Dictionary Items => this.items; - - public DisposableItem Create(int key) - { - var item = new DisposableItem(); - items.Add(key, item); - return item; - } - } } } diff --git a/BitFaster.Caching.UnitTests/Lru/DisposableItem.cs b/BitFaster.Caching.UnitTests/Lru/DisposableItem.cs new file mode 100644 index 00000000..81e9950d --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lru/DisposableItem.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BitFaster.Caching.UnitTests.Lru +{ + public class DisposableItem : IDisposable + { + public bool IsDisposed { get; private set; } + + public void Dispose() + { + this.IsDisposed = true; + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lru/DisposableValueFactory.cs b/BitFaster.Caching.UnitTests/Lru/DisposableValueFactory.cs new file mode 100644 index 00000000..41c95d50 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lru/DisposableValueFactory.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.UnitTests.Lru +{ + public class DisposableValueFactory + { + private Dictionary items = new Dictionary(); + + public Dictionary Items => this.items; + + public DisposableItem Create(int key) + { + var item = new DisposableItem(); + items.Add(key, item); + return item; + } + + public Task CreateAsync(int key) + { + var item = new DisposableItem(); + items.Add(key, item); + return Task.FromResult(item); + } + } +} diff --git a/BitFaster.Caching.UnitTests/ReferenceCountTests.cs b/BitFaster.Caching.UnitTests/ReferenceCountTests.cs index 84208c02..f74d9a4e 100644 --- a/BitFaster.Caching.UnitTests/ReferenceCountTests.cs +++ b/BitFaster.Caching.UnitTests/ReferenceCountTests.cs @@ -52,5 +52,21 @@ public void WhenObjectsAreDifferentHashcodesAreDifferent() a.GetHashCode().Should().NotBe(b.GetHashCode()); } + + [Fact] + public void WhenObjectDisposed() + { + var a = new ReferenceCount(new Disposable()); + var b = a.DecrementCopy(); + + b.Invoking(rc => rc.IncrementCopy()).Should().Throw(); + } + + private class Disposable : IDisposable + { + public void Dispose() + { + } + } } } diff --git a/BitFaster.Caching/Lru/ClassicLru.cs b/BitFaster.Caching/Lru/ClassicLru.cs index bb64d067..cf5e59e2 100644 --- a/BitFaster.Caching/Lru/ClassicLru.cs +++ b/BitFaster.Caching/Lru/ClassicLru.cs @@ -3,69 +3,80 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace BitFaster.Caching.Lru { - /// - /// LRU implementation where Lookup operations are backed by a ConcurrentDictionary and the LRU list is protected - /// by a global lock. All list operations performed within the lock are fast O(1) operations. - /// - /// - /// Due to the lock protecting list operations, this class may suffer lock contention under heavy load. - /// - /// The type of the key - /// The type of the value - public class ClassicLru : ICache - { - private readonly int capacity; - private readonly ConcurrentDictionary> dictionary; - private readonly LinkedList linkedList = new LinkedList(); - - private long requestHitCount; - private long requestTotalCount; - - public ClassicLru(int concurrencyLevel, int capacity, IEqualityComparer comparer) - { - this.capacity = capacity; - this.dictionary = new ConcurrentDictionary>(concurrencyLevel, this.capacity + 1, comparer); - } - - public int Count => this.linkedList.Count; - - public double HitRatio => (double)requestHitCount / (double)requestTotalCount; - - public bool TryGet(K key, out V value) - { - requestTotalCount++; - - LinkedListNode node; - if (dictionary.TryGetValue(key, out node)) - { - LockAndMoveToEnd(node); - requestHitCount++; - value = node.Value.Value; - return true; - } - - value = default(V); - return false; - } - - public V GetOrAdd(K key, Func valueFactory) - { - if (this.TryGet(key, out var value)) - { - return value; - } - - var node = new LinkedListNode(new LruItem(key, valueFactory(key))); - - if (this.dictionary.TryAdd(key, node)) - { - LinkedListNode first = null; - - lock (this.linkedList) + /// + /// LRU implementation where Lookup operations are backed by a ConcurrentDictionary and the LRU list is protected + /// by a global lock. All list operations performed within the lock are fast O(1) operations. + /// + /// + /// Due to the lock protecting list operations, this class may suffer lock contention under heavy load. + /// + /// The type of the key + /// The type of the value + public sealed class ClassicLru : ICache + { + private readonly int capacity; + private readonly ConcurrentDictionary> dictionary; + private readonly LinkedList linkedList = new LinkedList(); + + private long requestHitCount; + private long requestTotalCount; + + public ClassicLru(int concurrencyLevel, int capacity, IEqualityComparer comparer) + { + if (capacity < 3) + { + throw new ArgumentOutOfRangeException("Capacity must be greater than or equal to 3."); + } + + if (comparer == null) + { + throw new ArgumentNullException(nameof(comparer)); + } + + this.capacity = capacity; + this.dictionary = new ConcurrentDictionary>(concurrencyLevel, this.capacity + 1, comparer); + } + + public int Count => this.linkedList.Count; + + public double HitRatio => (double)requestHitCount / (double)requestTotalCount; + + public bool TryGet(K key, out V value) + { + Interlocked.Increment(ref requestTotalCount); + + LinkedListNode node; + if (dictionary.TryGetValue(key, out node)) + { + LockAndMoveToEnd(node); + Interlocked.Increment(ref requestHitCount); + value = node.Value.Value; + return true; + } + + value = default(V); + return false; + } + + public V GetOrAdd(K key, Func valueFactory) + { + if (this.TryGet(key, out var value)) + { + return value; + } + + var node = new LinkedListNode(new LruItem(key, valueFactory(key))); + + if (this.dictionary.TryAdd(key, node)) + { + LinkedListNode first = null; + + lock (this.linkedList) { if (linkedList.Count >= capacity) { @@ -76,122 +87,137 @@ public V GetOrAdd(K key, Func valueFactory) linkedList.AddLast(node); } - // Remove from the dictionary outside the lock. This means that the dictionary at this moment - // contains an item that is not in the linked list. If another thread fetches this item, - // LockAndMoveToEnd will ignore it, since it is detached. This means we potentially 'lose' an - // item just as it was about to move to the back of the LRU list and be preserved. The next request - // for the same key will be a miss. Dictionary and list are eventually consistent. - // However, all operations inside the lock are extremely fast, so contention is minimized. - if (first != null) - { - dictionary.TryRemove(first.Value.Key, out var removed); - } - - return node.Value.Value; - } - - return this.GetOrAdd(key, valueFactory); - } - - public async Task GetOrAddAsync(K key, Func> valueFactory) - { - if (this.TryGet(key, out var value)) - { - return value; - } - - var node = new LinkedListNode(new LruItem(key, await valueFactory(key))); - - if (this.dictionary.TryAdd(key, node)) - { - LinkedListNode first = null; - - lock (this.linkedList) - { - if (linkedList.Count >= capacity) - { - first = linkedList.First; - linkedList.RemoveFirst(); - } - - linkedList.AddLast(node); - } - - // Remove from the dictionary outside the lock. This means that the dictionary at this moment - // contains an item that is not in the linked list. If another thread fetches this item, - // LockAndMoveToEnd will ignore it, since it is detached. This means we potentially 'lose' an - // item just as it was about to move to the back of the LRU list and be preserved. The next request - // for the same key will be a miss. Dictionary and list are eventually consistent. - // However, all operations inside the lock are extremely fast, so contention is minimized. - if (first != null) - { - dictionary.TryRemove(first.Value.Key, out var removed); - } - - return node.Value.Value; - } - - return await this.GetOrAddAsync(key, valueFactory); - } - - public bool TryRemove(K key) + // Remove from the dictionary outside the lock. This means that the dictionary at this moment + // contains an item that is not in the linked list. If another thread fetches this item, + // LockAndMoveToEnd will ignore it, since it is detached. This means we potentially 'lose' an + // item just as it was about to move to the back of the LRU list and be preserved. The next request + // for the same key will be a miss. Dictionary and list are eventually consistent. + // However, all operations inside the lock are extremely fast, so contention is minimized. + if (first != null) + { + dictionary.TryRemove(first.Value.Key, out var removed); + + if (removed.Value.Value is IDisposable d) + { + d.Dispose(); + } + } + + return node.Value.Value; + } + + return this.GetOrAdd(key, valueFactory); + } + + public async Task GetOrAddAsync(K key, Func> valueFactory) { - if (dictionary.TryRemove(key, out var node)) - { - // If the node has already been removed from the list, ignore. - // E.g. thread A reads x from the dictionary. Thread B adds a new item, removes x from - // the List & Dictionary. Now thread A will try to move x to the end of the list. - if (node.List != null) - { - lock (this.linkedList) - { - if (node.List != null) - { - linkedList.Remove(node); - } - } - } - - return true; - } - - return false; + if (this.TryGet(key, out var value)) + { + return value; + } + + var node = new LinkedListNode(new LruItem(key, await valueFactory(key))); + + if (this.dictionary.TryAdd(key, node)) + { + LinkedListNode first = null; + + lock (this.linkedList) + { + if (linkedList.Count >= capacity) + { + first = linkedList.First; + linkedList.RemoveFirst(); + } + + linkedList.AddLast(node); + } + + // Remove from the dictionary outside the lock. This means that the dictionary at this moment + // contains an item that is not in the linked list. If another thread fetches this item, + // LockAndMoveToEnd will ignore it, since it is detached. This means we potentially 'lose' an + // item just as it was about to move to the back of the LRU list and be preserved. The next request + // for the same key will be a miss. Dictionary and list are eventually consistent. + // However, all operations inside the lock are extremely fast, so contention is minimized. + if (first != null) + { + dictionary.TryRemove(first.Value.Key, out var removed); + + if (removed.Value.Value is IDisposable d) + { + d.Dispose(); + } + } + + return node.Value.Value; + } + + return await this.GetOrAddAsync(key, valueFactory); } - // Thead A reads x from the dictionary. Thread B adds a new item. Thread A moves x to the end. Thread B now removes the new first Node (removal is atomic on both data structures). - private void LockAndMoveToEnd(LinkedListNode node) - { - // If the node has already been removed from the list, ignore. - // E.g. thread A reads x from the dictionary. Thread B adds a new item, removes x from - // the List & Dictionary. Now thread A will try to move x to the end of the list. - if (node.List == null) - { - return; - } - - lock (this.linkedList) - { - if (node.List == null) - { - return; - } - - linkedList.Remove(node); - linkedList.AddLast(node); - } - } - - private class LruItem - { - public LruItem(K k, V v) - { - Key = k; - Value = v; - } - - public K Key { get; } - - public V Value { get; } - } - } + public bool TryRemove(K key) + { + if (dictionary.TryRemove(key, out var node)) + { + // If the node has already been removed from the list, ignore. + // E.g. thread A reads x from the dictionary. Thread B adds a new item, removes x from + // the List & Dictionary. Now thread A will try to move x to the end of the list. + if (node.List != null) + { + lock (this.linkedList) + { + if (node.List != null) + { + linkedList.Remove(node); + } + } + } + + if (node.Value.Value is IDisposable d) + { + d.Dispose(); + } + + return true; + } + + return false; + } + + // Thead A reads x from the dictionary. Thread B adds a new item. Thread A moves x to the end. Thread B now removes the new first Node (removal is atomic on both data structures). + private void LockAndMoveToEnd(LinkedListNode node) + { + // If the node has already been removed from the list, ignore. + // E.g. thread A reads x from the dictionary. Thread B adds a new item, removes x from + // the List & Dictionary. Now thread A will try to move x to the end of the list. + if (node.List == null) + { + return; + } + + lock (this.linkedList) + { + if (node.List == null) + { + return; + } + + linkedList.Remove(node); + linkedList.AddLast(node); + } + } + + private class LruItem + { + public LruItem(K k, V v) + { + Key = k; + Value = v; + } + + public K Key { get; } + + public V Value { get; } + } + } } diff --git a/BitFaster.Caching/Lru/TemplateConcurrentLru.cs b/BitFaster.Caching/Lru/TemplateConcurrentLru.cs index e9c771aa..dc631b63 100644 --- a/BitFaster.Caching/Lru/TemplateConcurrentLru.cs +++ b/BitFaster.Caching/Lru/TemplateConcurrentLru.cs @@ -59,11 +59,6 @@ public TemplateConcurrentLru( P itemPolicy, H hitCounter) { - if (capacity < 1) - { - throw new ArgumentOutOfRangeException("Capacity must be greater than or equal to 3."); - } - if (capacity < 3) { throw new ArgumentOutOfRangeException("Capacity must be greater than or equal to 3."); diff --git a/BitFaster.Caching/ReferenceCount.cs b/BitFaster.Caching/ReferenceCount.cs index b1e534d2..4ce8555f 100644 --- a/BitFaster.Caching/ReferenceCount.cs +++ b/BitFaster.Caching/ReferenceCount.cs @@ -54,7 +54,7 @@ public override bool Equals(object obj) public ReferenceCount IncrementCopy() { - if (this.count < 0 && this.value is IDisposable) + if (this.count <= 0 && this.value is IDisposable) { throw new ObjectDisposedException($"{typeof(TValue).Name} is disposed."); } diff --git a/README.md b/README.md index 488edb42..ec031a46 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ LRU implementations are intended as an alternative to the System.Runtime.Caching ## Lru Benchmarks +Benchmarks are based on BenchmarkDotNet, so are single threaded. The ConcurrentLru family of classes can outperform ClassicLru in multithreaded workloads. + ~~~ BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.900 (1909/November2018Update/19H2) Intel Core i7-5600U CPU 2.60GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores @@ -33,7 +35,7 @@ Job=RyuJitX64 Jit=RyuJit Platform=X64 ### Lookup speed -Cache contains 6 items which are fetched repeatedly, no items are evicted. Representative of high hit rate scenario. +Cache contains 6 items which are fetched repeatedly, no items are evicted. Representative of high hit rate scenario, when there are a low number of hot items. - ConcurrentLru family does not move items in the queues, it is just marking as accessed for pure cache hits. - ClassicLru must maintain item order, and is internally splicing the fetched item to the head of the linked list. @@ -68,6 +70,19 @@ This test needs to be improved to provoke queue cycling. | ClassicLru | 363.5 ns | 3.65 ns | 3.23 ns | 2.04 | 0.0763 | 160 B | | MemoryCache | 2,380.9 ns | 33.22 ns | 27.74 ns | 13.37 | 2.3460 | 4912 B | + +### LruCycle2 + +| Method | Mean | Error | StdDev | Ratio | Gen 0 | Allocated | +|--------------------- |-----------:|---------:|---------:|------:|-------:|----------:| +| ConcurrentDictionary | 111.0 ns | 1.60 ns | 1.33 ns | 1.00 | 0.0079 | 17 B | +| FastConcurrentLru | 1,086.2 ns | 21.61 ns | 19.16 ns | 9.77 | 0.1424 | 300 B | +| ConcurrentLru | 1,098.2 ns | 8.15 ns | 7.23 ns | 9.89 | 0.1424 | 300 B | +| FastConcurrentTLru | 2,370.7 ns | 33.77 ns | 28.20 ns | 21.37 | 0.1577 | 333 B | +| ConcurrentTLru | 2,419.7 ns | 46.90 ns | 52.13 ns | 21.82 | 0.1577 | 333 B | +| ClassicLru | 834.3 ns | 10.84 ns | 9.61 ns | 7.52 | 0.2225 | 467 B | +| MemoryCache | 1,572.9 ns | 30.94 ns | 44.37 ns | 14.14 | 0.1424 | 313 B | + ## Meta-programming using structs for JIT dead code removal and inlining TemplateConcurrentLru features injectable policies defined as structs. Since structs are subject to special JIT optimizations, the implementation is much faster than if these policies were defined as classes. Using this technique, 'Fast' versions without hit counting are within 30% of the speed of a ConcurrentDictionary.