Skip to content

Commit 8ac5f56

Browse files
authored
Revise LRU eviction logic (#456)
Simplify eviction logic, remove redundant volatile reads.
1 parent bebba07 commit 8ac5f56

File tree

1 file changed

+35
-66
lines changed

1 file changed

+35
-66
lines changed

BitFaster.Caching/Lru/ConcurrentLruCore.cs

Lines changed: 35 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ public bool TryRemove(KeyValuePair<K, V> item)
312312
if (((ICollection<KeyValuePair<K, I>>)this.dictionary).Remove(kvp))
313313
#endif
314314
{
315-
OnRemove(item.Key, kvp.Value);
315+
OnRemove(item.Key, kvp.Value, ItemRemovedReason.Removed);
316316
return true;
317317
}
318318
}
@@ -333,7 +333,7 @@ public bool TryRemove(K key, out V value)
333333
{
334334
if (this.dictionary.TryRemove(key, out var item))
335335
{
336-
OnRemove(key, item);
336+
OnRemove(key, item, ItemRemovedReason.Removed);
337337
value = item.Value;
338338
return true;
339339
}
@@ -348,15 +348,16 @@ public bool TryRemove(K key)
348348
return TryRemove(key, out _);
349349
}
350350

351-
private void OnRemove(K key, I item)
351+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
352+
private void OnRemove(K key, I item, ItemRemovedReason reason)
352353
{
353354
// Mark as not accessed, it will later be cycled out of the queues because it can never be fetched
354355
// from the dictionary. Note: Hot/Warm/Cold count will reflect the removed item until it is cycled
355356
// from the queue.
356357
item.WasAccessed = false;
357358
item.WasRemoved = true;
358359

359-
this.telemetryPolicy.OnItemRemoved(key, item.Value, ItemRemovedReason.Removed);
360+
this.telemetryPolicy.OnItemRemoved(key, item.Value, reason);
360361

361362
// serialize dispose (common case dispose not thread safe)
362363
lock (item)
@@ -365,7 +366,6 @@ private void OnRemove(K key, I item)
365366
}
366367
}
367368

368-
369369
///<inheritdoc/>
370370
///<remarks>Note: Calling this method does not affect LRU order.</remarks>
371371
public bool TryUpdate(K key, V value)
@@ -546,10 +546,8 @@ private void Cycle(int hotCount)
546546
{
547547
(var dest, var count) = CycleHot(hotCount);
548548

549-
const int maxAttempts = 5;
550-
int attempts = 0;
551-
552-
while (attempts++ < maxAttempts)
549+
int cycles = 0;
550+
while (cycles++ < 3 && dest != ItemDestination.Remove)
553551
{
554552
if (dest == ItemDestination.Warm)
555553
{
@@ -559,41 +557,17 @@ private void Cycle(int hotCount)
559557
{
560558
(dest, count) = CycleCold(count);
561559
}
562-
else
563-
{
564-
// If an item was removed, it is possible that the warm and cold queues are still oversize.
565-
// Attempt to recover. It is possible that multiple threads read the same queue count here,
566-
// so this process has races that could reduce cache size below capacity. This manifests
567-
// in 'off by one' which is considered harmless.
568-
569-
(dest, count) = CycleCold(Volatile.Read(ref counter.cold));
570-
if (dest != ItemDestination.Remove)
571-
{
572-
continue;
573-
}
574-
575-
(dest, count) = CycleWarm(Volatile.Read(ref counter.warm));
576-
if (dest != ItemDestination.Remove)
577-
{
578-
continue;
579-
}
580-
581-
break;
582-
}
583560
}
584-
585-
// If we get here, we have cycled the queues multiple times and still have not removed an item.
586-
// This can happen if the cache is full of items that are not discardable. In this case, we simply
587-
// discard the coldest item to avoid unbounded growth.
561+
562+
// If nothing was removed yet, constrain the size of warm and cold by discarding the coldest item.
588563
if (dest != ItemDestination.Remove)
589564
{
590-
// if an item was last moved into warm, move the last warm item to cold to prevent enlarging warm
591-
if (dest == ItemDestination.Warm)
565+
if (dest == ItemDestination.Warm && count > this.capacity.Warm)
592566
{
593-
LastWarmToCold();
567+
count = LastWarmToCold();
594568
}
595569

596-
RemoveCold(ItemRemovedReason.Evicted);
570+
ConstrainCold(count, ItemRemovedReason.Evicted);
597571
}
598572
}
599573
else
@@ -604,20 +578,6 @@ private void Cycle(int hotCount)
604578
}
605579
}
606580

607-
private void LastWarmToCold()
608-
{
609-
Interlocked.Decrement(ref this.counter.warm);
610-
611-
if (this.warmQueue.TryDequeue(out var item))
612-
{
613-
this.Move(item, ItemDestination.Cold, ItemRemovedReason.Evicted);
614-
}
615-
else
616-
{
617-
Interlocked.Increment(ref this.counter.warm);
618-
}
619-
}
620-
621581
private void CycleDuringWarmup(int hotCount)
622582
{
623583
// do nothing until hot is full
@@ -750,17 +710,29 @@ private void CycleDuringWarmup(int hotCount)
750710
}
751711
}
752712

753-
private void RemoveCold(ItemRemovedReason removedReason)
713+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
714+
private int LastWarmToCold()
754715
{
755-
Interlocked.Decrement(ref this.counter.cold);
716+
Interlocked.Decrement(ref this.counter.warm);
756717

757-
if (this.coldQueue.TryDequeue(out var item))
718+
if (this.warmQueue.TryDequeue(out var item))
758719
{
759-
this.Move(item, ItemDestination.Remove, removedReason);
720+
return this.Move(item, ItemDestination.Cold, ItemRemovedReason.Evicted);
760721
}
761722
else
762723
{
763-
Interlocked.Increment(ref this.counter.cold);
724+
Interlocked.Increment(ref this.counter.warm);
725+
return 0;
726+
}
727+
}
728+
729+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
730+
private void ConstrainCold(int coldCount, ItemRemovedReason removedReason)
731+
{
732+
if (coldCount > this.capacity.Cold && this.coldQueue.TryDequeue(out var item))
733+
{
734+
Interlocked.Decrement(ref this.counter.cold);
735+
this.Move(item, ItemDestination.Remove, removedReason);
764736
}
765737
}
766738

@@ -781,18 +753,14 @@ private int Move(I item, ItemDestination where, ItemRemovedReason removedReason)
781753

782754
var kvp = new KeyValuePair<K, I>(item.Key, item);
783755

784-
// hidden atomic remove
756+
#if NET6_0_OR_GREATER
757+
if (this.dictionary.TryRemove(kvp))
758+
#else
785759
// https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/
786760
if (((ICollection<KeyValuePair<K, I>>)this.dictionary).Remove(kvp))
761+
#endif
787762
{
788-
item.WasRemoved = true;
789-
790-
this.telemetryPolicy.OnItemRemoved(item.Key, item.Value, removedReason);
791-
792-
lock (item)
793-
{
794-
Disposer<V>.Dispose(item.Value);
795-
}
763+
OnRemove(item.Key, item, removedReason);
796764
}
797765
break;
798766
}
@@ -826,6 +794,7 @@ private static CachePolicy CreatePolicy(ConcurrentLruCore<K, V, I, P, T> lru)
826794
// it becomes immutable. However, this object is then somewhere else on the
827795
// heap, which slows down the policies with hit counter logic in benchmarks. Likely
828796
// this approach keeps the structs data members in the same CPU cache line as the LRU.
797+
// backcompat: remove conditional compile
829798
#if NETCOREAPP3_0_OR_GREATER
830799
[DebuggerDisplay("Hit = {Hits}, Miss = {Misses}, Upd = {Updated}, Evict = {Evicted}")]
831800
#else

0 commit comments

Comments
 (0)