diff --git a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs index 86e0e075..00ab7247 100644 --- a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs @@ -307,32 +307,33 @@ private async Task RunIntegrityCheckAsync(ConcurrentLfu lfu, int it RunIntegrityCheck(lfu, this.output); } - private static void RunIntegrityCheck(ConcurrentLfu cache, ITestOutputHelper output) { - new ConcurrentLfuIntegrityChecker(cache).Validate(output); + new ConcurrentLfuIntegrityChecker, AccessOrderPolicy>(cache.Core).Validate(output); } } - public class ConcurrentLfuIntegrityChecker + internal class ConcurrentLfuIntegrityChecker + where N : LfuNode + where P : struct, INodePolicy { - private readonly ConcurrentLfu cache; + private readonly ConcurrentLfuCore cache; private readonly LfuNodeList windowLru; private readonly LfuNodeList probationLru; private readonly LfuNodeList protectedLru; - private readonly StripedMpscBuffer> readBuffer; - private readonly MpscBoundedBuffer> writeBuffer; + private readonly StripedMpscBuffer readBuffer; + private readonly MpscBoundedBuffer writeBuffer; - private static FieldInfo windowLruField = typeof(ConcurrentLfu).GetField("windowLru", BindingFlags.NonPublic | BindingFlags.Instance); - private static FieldInfo probationLruField = typeof(ConcurrentLfu).GetField("probationLru", BindingFlags.NonPublic | BindingFlags.Instance); - private static FieldInfo protectedLruField = typeof(ConcurrentLfu).GetField("protectedLru", BindingFlags.NonPublic | BindingFlags.Instance); + private static FieldInfo windowLruField = typeof(ConcurrentLfuCore).GetField("windowLru", BindingFlags.NonPublic | BindingFlags.Instance); + private static FieldInfo probationLruField = typeof(ConcurrentLfuCore).GetField("probationLru", BindingFlags.NonPublic | BindingFlags.Instance); + private static FieldInfo protectedLruField = typeof(ConcurrentLfuCore).GetField("protectedLru", BindingFlags.NonPublic | BindingFlags.Instance); - private static FieldInfo readBufferField = typeof(ConcurrentLfu).GetField("readBuffer", BindingFlags.NonPublic | BindingFlags.Instance); - private static FieldInfo writeBufferField = typeof(ConcurrentLfu).GetField("writeBuffer", BindingFlags.NonPublic | BindingFlags.Instance); + private static FieldInfo readBufferField = typeof(ConcurrentLfuCore).GetField("readBuffer", BindingFlags.NonPublic | BindingFlags.Instance); + private static FieldInfo writeBufferField = typeof(ConcurrentLfuCore).GetField("writeBuffer", BindingFlags.NonPublic | BindingFlags.Instance); - public ConcurrentLfuIntegrityChecker(ConcurrentLfu cache) + public ConcurrentLfuIntegrityChecker(ConcurrentLfuCore cache) { this.cache = cache; @@ -341,8 +342,8 @@ public ConcurrentLfuIntegrityChecker(ConcurrentLfu cache) this.probationLru = (LfuNodeList)probationLruField.GetValue(cache); this.protectedLru = (LfuNodeList)protectedLruField.GetValue(cache); - this.readBuffer = (StripedMpscBuffer>)readBufferField.GetValue(cache); - this.writeBuffer = (MpscBoundedBuffer>)writeBufferField.GetValue(cache); + this.readBuffer = (StripedMpscBuffer)readBufferField.GetValue(cache); + this.writeBuffer = (MpscBoundedBuffer)writeBufferField.GetValue(cache); } public void Validate(ITestOutputHelper output) diff --git a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs index 791c534a..b806d867 100644 --- a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs @@ -26,6 +26,30 @@ public ConcurrentLfuTests(ITestOutputHelper output) this.output = output; } + [Fact] + public void WhenCapacityIsLessThan3CtorThrows() + { + Action constructor = () => { var x = new ConcurrentLfu(2); }; + + constructor.Should().Throw(); + } + + [Fact] + public void WhenCapacityIsValidCacheIsCreated() + { + var x = new ConcurrentLfu(3); + + x.Capacity.Should().Be(3); + } + + [Fact] + public void WhenConcurrencyIsLessThan1CtorThrows() + { + Action constructor = () => { var x = new ConcurrentLfu(0, 20, new ForegroundScheduler(), EqualityComparer.Default); }; + + constructor.Should().Throw(); + } + [Fact] public void DefaultSchedulerIsThreadPool() { @@ -548,7 +572,11 @@ public void WhenItemsAddedGenericEnumerateContainsKvps() cache.GetOrAdd(1, k => k + 1); cache.GetOrAdd(2, k => k + 1); - cache.Should().BeEquivalentTo(new[] { new KeyValuePair(1, 2), new KeyValuePair(2, 3) }); + var enumerator = cache.GetEnumerator(); + enumerator.MoveNext().Should().BeTrue(); + enumerator.Current.Should().Be(new KeyValuePair(1, 2)); + enumerator.MoveNext().Should().BeTrue(); + enumerator.Current.Should().Be(new KeyValuePair(2, 3)); } [Fact] diff --git a/BitFaster.Caching/BitFaster.Caching.csproj b/BitFaster.Caching/BitFaster.Caching.csproj index 248e6559..60aa9ccc 100644 --- a/BitFaster.Caching/BitFaster.Caching.csproj +++ b/BitFaster.Caching/BitFaster.Caching.csproj @@ -2,7 +2,7 @@ netstandard2.0;netcoreapp3.1;net6.0 - 9.0 + 10.0 Alex Peck BitFaster.Caching diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index 6f19e3fb..0faf7351 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -40,21 +40,254 @@ namespace BitFaster.Caching.Lfu /// /// Based on the Caffeine library by ben.manes@gmail.com (Ben Manes). /// https://github.com/ben-manes/caffeine - [DebuggerTypeProxy(typeof(ConcurrentLfu<,>.LfuDebugView))] + [DebuggerTypeProxy(typeof(ConcurrentLfu<,>.LfuDebugView<>))] [DebuggerDisplay("Count = {Count}/{Capacity}")] public sealed class ConcurrentLfu : ICache, IAsyncCache, IBoundedPolicy { - private const int MaxWriteBufferRetries = 64; + // Note: for performance reasons this is a mutable struct, it cannot be readonly. + private ConcurrentLfuCore, AccessOrderPolicy> core; /// /// The default buffer size. /// public const int DefaultBufferSize = 128; - private readonly ConcurrentDictionary> dictionary; + /// + /// Initializes a new instance of the ConcurrentLfu class with the specified capacity. + /// + /// The capacity. + public ConcurrentLfu(int capacity) + { + this.core = new(Defaults.ConcurrencyLevel, capacity, new ThreadPoolScheduler(), EqualityComparer.Default, () => this.DrainBuffers()); + } + + /// + /// Initializes a new instance of the ConcurrentLfu class with the specified concurrencyLevel, capacity, scheduler, equality comparer and buffer size. + /// + /// The concurrency level. + /// The capacity. + /// The scheduler. + /// The equality comparer. + public ConcurrentLfu(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer comparer) + { + this.core = new (concurrencyLevel, capacity, scheduler, comparer, () => this.DrainBuffers()); + } + + internal ConcurrentLfuCore, AccessOrderPolicy> Core => core; + + // structs cannot declare self referencing lambda functions, therefore pass this in from the ctor + private void DrainBuffers() + { + this.core.DrainBuffers(); + } + + /// + public int Count => core.Count; + + /// + public Optional Metrics => core.Metrics; + + /// + public Optional> Events => core.Events; + + /// + public CachePolicy Policy => core.Policy; + + /// + public ICollection Keys => core.Keys; + + /// + public int Capacity => core.Capacity; + + /// + public IScheduler Scheduler => core.Scheduler; + + /// + /// Synchronously perform all pending policy maintenance. Drain the read and write buffers then + /// use the eviction policy to preserve bounded size and remove expired items. + /// + /// + /// Note: maintenance is automatically performed asynchronously immediately following a read or write. + /// It is not necessary to call this method, is provided purely to enable tests to reach a consistent state. + /// + public void DoMaintenance() + { + core.DoMaintenance(); + } + + /// + public void AddOrUpdate(K key, V value) + { + core.AddOrUpdate(key, value); + } + + /// + public void Clear() + { + core.Clear(); + } + + /// + public V GetOrAdd(K key, Func valueFactory) + { + return core.GetOrAdd(key, valueFactory); + } - private readonly StripedMpscBuffer> readBuffer; - private readonly MpscBoundedBuffer> writeBuffer; + /// + public V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) + { + return core.GetOrAdd(key, valueFactory, factoryArgument); + } + + /// + public ValueTask GetOrAddAsync(K key, Func> valueFactory) + { + return core.GetOrAddAsync(key, valueFactory); + } + + /// + public ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) + { + return core.GetOrAddAsync(key, valueFactory, factoryArgument); + } + + /// + public void Trim(int itemCount) + { + core.Trim(itemCount); + } + + /// + public bool TryGet(K key, out V value) + { + return core.TryGet(key, out value); + } + + /// + public bool TryRemove(K key) + { + return core.TryRemove(key); + } + + /// + /// Attempts to remove the specified key value pair. + /// + /// The item to remove. + /// true if the item was removed successfully; otherwise, false. + public bool TryRemove(KeyValuePair item) + { + return core.TryRemove(item); + } + + /// + /// Attempts to remove and return the value that has the specified key. + /// + /// The key of the element to remove. + /// When this method returns, contains the object removed, or the default value of the value type if key does not exist. + /// true if the object was removed successfully; otherwise, false. + public bool TryRemove(K key, out V value) + { + return core.TryRemove(key, out value); + } + + /// + public bool TryUpdate(K key, V value) + { + return core.TryUpdate(key, value); + } + + /// + public IEnumerator> GetEnumerator() + { + return core.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return core.GetEnumerator(); + } + +#if DEBUG + /// + /// Format the LFU as a string by converting all the keys to strings. + /// + /// The LFU formatted as a string. + public string FormatLfuString() + { + return core.FormatLfuString(); + } +#endif + + [ExcludeFromCodeCoverage] + internal class LfuDebugView + where N : LfuNode + { + private readonly ConcurrentLfu lfu; + + public LfuDebugView(ConcurrentLfu lfu) + { + this.lfu = lfu; + } + + public string Maintenance => lfu.core.drainStatus.Format(); + + public ICacheMetrics Metrics => lfu.Metrics.Value; + + public StripedMpscBuffer ReadBuffer => this.lfu.core.readBuffer as StripedMpscBuffer; + + public MpscBoundedBuffer WriteBuffer => this.lfu.core.writeBuffer as MpscBoundedBuffer; + + public KeyValuePair[] Items + { + get + { + var items = new KeyValuePair[lfu.Count]; + + int index = 0; + foreach (var kvp in lfu) + { + items[index++] = kvp; + } + return items; + } + } + } + } + + /// + /// An approximate LFU based on the W-TinyLfu eviction policy. W-TinyLfu tracks items using a window LRU list, and + /// a main space LRU divided into protected and probation segments. Reads and writes to the cache are stored in buffers + /// and later applied to the policy LRU lists in batches under a lock. Each read and write is tracked using a compact + /// popularity sketch to probalistically estimate item frequency. Items proceed through the LRU lists as follows: + /// + /// New items are added to the window LRU. When acessed window items move to the window MRU position. + /// When the window is full, candidate items are moved to the probation segment in LRU order. + /// When the main space is full, the access frequency of each window candidate is compared + /// to probation victims in LRU order. The item with the lowest frequency is evicted until the cache size is within bounds. + /// When a probation item is accessed, it is moved to the protected segment. If the protected segment is full, + /// the LRU protected item is demoted to probation. + /// When a protected item is accessed, it is moved to the protected MRU position. + /// + /// The size of the admission window and main space are adapted over time to iteratively improve hit rate using a + /// hill climbing algorithm. A larger window favors workloads with high recency bias, whereas a larger main space + /// favors workloads with frequency bias. + /// + /// Based on the Caffeine library by ben.manes@gmail.com (Ben Manes). + /// https://github.com/ben-manes/caffeine + + internal struct ConcurrentLfuCore : IBoundedPolicy + where N : LfuNode + where P : struct, INodePolicy + { + private const int MaxWriteBufferRetries = 64; + + private const int DefaultBufferSize = 128; + + private readonly ConcurrentDictionary dictionary; + + internal readonly StripedMpscBuffer readBuffer; + internal readonly MpscBoundedBuffer writeBuffer; private readonly CacheMetrics metrics = new(); @@ -66,42 +299,29 @@ public sealed class ConcurrentLfu : ICache, IAsyncCache, IBoun private readonly LfuCapacityPartition capacity; - private readonly DrainStatus drainStatus = new(); + internal readonly DrainStatus drainStatus = new(); private readonly object maintenanceLock = new(); private readonly IScheduler scheduler; private readonly Action drainBuffers; - private readonly LfuNode[] drainBuffer; + private readonly N[] drainBuffer; - /// - /// Initializes a new instance of the ConcurrentLfu class with the specified capacity. - /// - /// The capacity. - public ConcurrentLfu(int capacity) - : this(Defaults.ConcurrencyLevel, capacity, new ThreadPoolScheduler(), EqualityComparer.Default) - { - } - - /// - /// Initializes a new instance of the ConcurrentLfu class with the specified concurrencyLevel, capacity, scheduler, equality comparer and buffer size. - /// - /// The concurrency level. - /// The capacity. - /// The scheduler. - /// The equality comparer. - public ConcurrentLfu(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer comparer) + public ConcurrentLfuCore(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer comparer, Action drainBuffers) { + if (capacity < 3) + Throw.ArgOutOfRange(nameof(capacity)); + int dictionaryCapacity = ConcurrentDictionarySize.Estimate(capacity); - this.dictionary = new ConcurrentDictionary>(concurrencyLevel, dictionaryCapacity, comparer); + this.dictionary = new (concurrencyLevel, dictionaryCapacity, comparer); // cap concurrency at proc count * 2 int readStripes = Math.Min(BitOps.CeilingPowerOfTwo(concurrencyLevel), BitOps.CeilingPowerOfTwo(Environment.ProcessorCount * 2)); - this.readBuffer = new StripedMpscBuffer>(readStripes, DefaultBufferSize); + this.readBuffer = new (readStripes, DefaultBufferSize); // Cap the write buffer to the cache size, or 128. Whichever is smaller. int writeBufferSize = Math.Min(BitOps.CeilingPowerOfTwo(capacity), 128); - this.writeBuffer = new MpscBoundedBuffer>(writeBufferSize); + this.writeBuffer = new (writeBufferSize); this.cmSketch = new CmSketch(capacity, comparer); this.windowLru = new LfuNodeList(); @@ -111,36 +331,27 @@ public ConcurrentLfu(int concurrencyLevel, int capacity, IScheduler scheduler, I this.capacity = new LfuCapacityPartition(capacity); this.scheduler = scheduler; - this.drainBuffers = () => this.DrainBuffers(); - this.drainBuffer = new LfuNode[this.readBuffer.Capacity]; + this.drainBuffer = new N[this.readBuffer.Capacity]; + + this.drainBuffers = drainBuffers; } // No lock count: https://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/ - /// public int Count => this.dictionary.Skip(0).Count(); - /// public int Capacity => this.capacity.Capacity; - /// public Optional Metrics => new(this.metrics); - /// public Optional> Events => Optional>.None(); - /// public CachePolicy Policy => new(new Optional(this), Optional.None()); - /// public ICollection Keys => this.dictionary.Keys; - /// - /// Gets the scheduler. - /// public IScheduler Scheduler => scheduler; - /// public void AddOrUpdate(K key, V value) { while (true) @@ -150,7 +361,7 @@ public void AddOrUpdate(K key, V value) return; } - var node = new LfuNode(key, value); + var node = default(P).Create(key, value); if (this.dictionary.TryAdd(key, node)) { AfterWrite(node); @@ -159,7 +370,6 @@ public void AddOrUpdate(K key, V value) } } - /// public void Clear() { this.Trim(this.Count); @@ -172,10 +382,6 @@ public void Clear() } } - /// - /// Trim the specified number of items from the cache. - /// - /// The number of items to remove. public void Trim(int itemCount) { itemCount = Math.Min(itemCount, this.Count); @@ -205,7 +411,7 @@ public void Trim(int itemCount) private bool TryAdd(K key, V value) { - var node = new LfuNode(key, value); + var node = default(P).Create(key, value); if (this.dictionary.TryAdd(key, node)) { @@ -217,7 +423,6 @@ private bool TryAdd(K key, V value) return false; } - /// public V GetOrAdd(K key, Func valueFactory) { while (true) @@ -235,16 +440,6 @@ public V GetOrAdd(K key, Func valueFactory) } } - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The type of an argument to pass into valueFactory. - /// The key of the element to add. - /// The factory function used to generate a value for the key. - /// An argument value to pass into valueFactory. - /// The value for the key. This will be either the existing value for the key if the key is already - /// in the cache, or the new value if the key was not in the cache. public V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) { while (true) @@ -262,7 +457,6 @@ public V GetOrAdd(K key, Func valueFactory, TArg factoryArgume } } - /// public async ValueTask GetOrAddAsync(K key, Func> valueFactory) { while (true) @@ -280,15 +474,6 @@ public async ValueTask GetOrAddAsync(K key, Func> valueFactory) } } - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The type of an argument to pass into valueFactory. - /// The key of the element to add. - /// The factory function used to asynchronously generate a value for the key. - /// An argument value to pass into valueFactory. - /// A task that represents the asynchronous GetOrAdd operation. public async ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) { while (true) @@ -306,7 +491,6 @@ public async ValueTask GetOrAddAsync(K key, Func> valu } } - /// public bool TryGet(K key, out V value) { if (this.dictionary.TryGetValue(key, out var node)) @@ -327,24 +511,19 @@ public bool TryGet(K key, out V value) return false; } - /// - /// Attempts to remove the specified key value pair. - /// - /// The item to remove. - /// true if the item was removed successfully; otherwise, false. public bool TryRemove(KeyValuePair item) { if (this.dictionary.TryGetValue(item.Key, out var node)) { if (EqualityComparer.Default.Equals(node.Value, item.Value)) { - var kvp = new KeyValuePair>(item.Key, node); + var kvp = new KeyValuePair(item.Key, node); #if NET6_0_OR_GREATER if (this.dictionary.TryRemove(kvp)) #else // https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/ - if (((ICollection>>)this.dictionary).Remove(kvp)) + if (((ICollection>)this.dictionary).Remove(kvp)) #endif { node.WasRemoved = true; @@ -357,12 +536,6 @@ public bool TryRemove(KeyValuePair item) return false; } - /// - /// Attempts to remove and return the value that has the specified key. - /// - /// The key of the element to remove. - /// When this method returns, contains the object removed, or the default value of the value type if key does not exist. - /// true if the object was removed successfully; otherwise, false. public bool TryRemove(K key, out V value) { if (this.dictionary.TryRemove(key, out var node)) @@ -377,13 +550,11 @@ public bool TryRemove(K key, out V value) return false; } - /// public bool TryRemove(K key) { return this.TryRemove(key, out var _); } - /// public bool TryUpdate(K key, V value) { if (this.dictionary.TryGetValue(key, out var node)) @@ -400,27 +571,11 @@ public bool TryUpdate(K key, V value) return false; } - /// - /// Synchronously perform all pending policy maintenance. Drain the read and write buffers then - /// use the eviction policy to preserve bounded size and remove expired items. - /// - /// - /// Note: maintenance is automatically performed asynchronously immediately following a read or write. - /// It is not necessary to call this method, is provided purely to enable tests to reach a consistent state. - /// public void DoMaintenance() { DrainBuffers(); } - /// Returns an enumerator that iterates through the cache. - /// An enumerator for the cache. - /// - /// The enumerator returned from the cache is safe to use concurrently with - /// reads and writes, however it does not represent a moment-in-time snapshot. - /// The contents exposed through the enumerator may contain modifications - /// made after was called. - /// public IEnumerator> GetEnumerator() { foreach (var kvp in this.dictionary) @@ -445,7 +600,7 @@ private static void TakeCandidatesInLruOrder(LfuNodeList lru, List node) + private void AfterWrite(N node) { for (int i = 0; i < MaxWriteBufferRetries; i++) { @@ -507,11 +662,6 @@ private void ScheduleAfterWrite() } } - IEnumerator IEnumerable.GetEnumerator() - { - return ((ConcurrentLfu)this).GetEnumerator(); - } - private void TryScheduleDrain() { if (this.drainStatus.VolatileRead() >= DrainStatus.ProcessingToIdle) @@ -546,7 +696,7 @@ private void TryScheduleDrain() } } - private void DrainBuffers() + internal void DrainBuffers() { bool done = false; @@ -570,7 +720,7 @@ private void DrainBuffers() } } - private bool Maintenance(LfuNode droppedWrite = null) + private bool Maintenance(N droppedWrite = null) { this.drainStatus.VolatileWrite(DrainStatus.ProcessingToIdle); @@ -859,7 +1009,7 @@ private void ReFitProtected() } [DebuggerDisplay("{Format(),nq}")] - private class DrainStatus + internal class DrainStatus { public const int Idle = 0; public const int Required = 1; @@ -876,8 +1026,8 @@ public bool ShouldDrain(bool delayable) { Idle => !delayable, Required => true, - ProcessingToIdle or ProcessingToRequired => false, - _ => false,// not reachable + // ProcessingToIdle or ProcessingToRequired => false, undefined not reachable + _ => false, }; } @@ -971,40 +1121,6 @@ public string FormatLfuString() return sb.ToString(); } #endif - - [ExcludeFromCodeCoverage] - internal class LfuDebugView - { - private readonly ConcurrentLfu lfu; - - public LfuDebugView(ConcurrentLfu lfu) - { - this.lfu = lfu; - } - - public string Maintenance => lfu.drainStatus.Format(); - - public ICacheMetrics Metrics => lfu.metrics; - - public StripedMpscBuffer> ReadBuffer => this.lfu.readBuffer; - - public MpscBoundedBuffer> WriteBuffer => this.lfu.writeBuffer; - - public KeyValuePair[] Items - { - get - { - var items = new KeyValuePair[lfu.Count]; - - int index = 0; - foreach (var kvp in lfu) - { - items[index++] = kvp; - } - return items; - } - } - } } // Explicit layout cannot be a generic class member diff --git a/BitFaster.Caching/Lfu/LfuNode.cs b/BitFaster.Caching/Lfu/LfuNode.cs index 40028176..f56db8e2 100644 --- a/BitFaster.Caching/Lfu/LfuNode.cs +++ b/BitFaster.Caching/Lfu/LfuNode.cs @@ -1,6 +1,6 @@ namespace BitFaster.Caching.Lfu { - internal sealed class LfuNode + internal class LfuNode { internal LfuNodeList list; internal LfuNode next; @@ -57,4 +57,11 @@ internal enum Position Probation, Protected, } + + internal sealed class AccessOrderNode : LfuNode + { + public AccessOrderNode(K k, V v) : base(k, v) + { + } + } } diff --git a/BitFaster.Caching/Lfu/NodePolicy.cs b/BitFaster.Caching/Lfu/NodePolicy.cs new file mode 100644 index 00000000..5e3a21f8 --- /dev/null +++ b/BitFaster.Caching/Lfu/NodePolicy.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; + +namespace BitFaster.Caching.Lfu +{ + internal interface INodePolicy + where N : LfuNode + { + N Create(K key, V value); + } + + internal struct AccessOrderPolicy : INodePolicy> + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public AccessOrderNode Create(K key, V value) + { + return new AccessOrderNode(key, value); + } + } +}