@@ -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