22// The .NET Foundation licenses this file to you under the MIT license.
33
44using System ;
5+ using System . Collections ;
56using System . Collections . Concurrent ;
67using System . Collections . Generic ;
78using System . Diagnostics . CodeAnalysis ;
89using System . Runtime . CompilerServices ;
10+ using System . Runtime . InteropServices ;
911using System . Threading ;
1012using System . Threading . Tasks ;
1113using Microsoft . Extensions . Logging ;
@@ -126,7 +128,7 @@ internal void SetEntry(CacheEntry entry)
126128 entry . LastAccessed = utcNow ;
127129
128130 CoherentState coherentState = _coherentState ; // Clear() can update the reference in the meantime
129- if ( coherentState . _entries . TryGetValue ( entry . Key , out CacheEntry ? priorEntry ) )
131+ if ( coherentState . TryGetValue ( entry . Key , out CacheEntry ? priorEntry ) )
130132 {
131133 priorEntry . SetExpired ( EvictionReason . Replaced ) ;
132134 }
@@ -145,12 +147,12 @@ internal void SetEntry(CacheEntry entry)
145147 if ( priorEntry == null )
146148 {
147149 // Try to add the new entry if no previous entries exist.
148- entryAdded = coherentState . _entries . TryAdd ( entry . Key , entry ) ;
150+ entryAdded = coherentState . TryAdd ( entry . Key , entry ) ;
149151 }
150152 else
151153 {
152154 // Try to update with the new entry if a previous entries exist.
153- entryAdded = coherentState . _entries . TryUpdate ( entry . Key , entry , priorEntry ) ;
155+ entryAdded = coherentState . TryUpdate ( entry . Key , entry , priorEntry ) ;
154156
155157 if ( entryAdded )
156158 {
@@ -165,7 +167,7 @@ internal void SetEntry(CacheEntry entry)
165167 // The update will fail if the previous entry was removed after retrieval.
166168 // Adding the new entry will succeed only if no entry has been added since.
167169 // This guarantees removing an old entry does not prevent adding a new entry.
168- entryAdded = coherentState . _entries . TryAdd ( entry . Key , entry ) ;
170+ entryAdded = coherentState . TryAdd ( entry . Key , entry ) ;
169171 }
170172 }
171173
@@ -210,7 +212,7 @@ public bool TryGetValue(object key, out object? result)
210212 DateTime utcNow = UtcNow ;
211213
212214 CoherentState coherentState = _coherentState ; // Clear() can update the reference in the meantime
213- if ( coherentState . _entries . TryGetValue ( key , out CacheEntry ? tmp ) )
215+ if ( coherentState . TryGetValue ( key , out CacheEntry ? tmp ) )
214216 {
215217 CacheEntry entry = tmp ;
216218 // Check if expired due to expiration tokens, timers, etc. and if so, remove it.
@@ -269,7 +271,8 @@ public void Remove(object key)
269271 CheckDisposed ( ) ;
270272
271273 CoherentState coherentState = _coherentState ; // Clear() can update the reference in the meantime
272- if ( coherentState . _entries . TryRemove ( key , out CacheEntry ? entry ) )
274+
275+ if ( coherentState . TryRemove ( key , out CacheEntry ? entry ) )
273276 {
274277 if ( _options . HasSizeLimit )
275278 {
@@ -291,10 +294,10 @@ public void Clear()
291294 CheckDisposed ( ) ;
292295
293296 CoherentState oldState = Interlocked . Exchange ( ref _coherentState , new CoherentState ( ) ) ;
294- foreach ( KeyValuePair < object , CacheEntry > entry in oldState . _entries )
297+ foreach ( CacheEntry entry in oldState . GetAllValues ( ) )
295298 {
296- entry . Value . SetExpired ( EvictionReason . Removed ) ;
297- entry . Value . InvokeEvictionCallbacks ( ) ;
299+ entry . SetExpired ( EvictionReason . Removed ) ;
300+ entry . InvokeEvictionCallbacks ( ) ;
298301 }
299302 }
300303
@@ -415,10 +418,9 @@ private void ScanForExpiredItems()
415418 DateTime utcNow = _lastExpirationScan = UtcNow ;
416419
417420 CoherentState coherentState = _coherentState ; // Clear() can update the reference in the meantime
418- foreach ( KeyValuePair < object , CacheEntry > item in coherentState . _entries )
419- {
420- CacheEntry entry = item . Value ;
421421
422+ foreach ( CacheEntry entry in coherentState . GetAllValues ( ) )
423+ {
422424 if ( entry . CheckExpired ( utcNow ) )
423425 {
424426 coherentState . RemoveEntry ( entry , _options ) ;
@@ -516,9 +518,8 @@ private void Compact(long removalSizeTarget, Func<CacheEntry, long> computeEntry
516518
517519 // Sort items by expired & priority status
518520 DateTime utcNow = UtcNow ;
519- foreach ( KeyValuePair < object , CacheEntry > item in coherentState . _entries )
521+ foreach ( CacheEntry entry in coherentState . GetAllValues ( ) )
520522 {
521- CacheEntry entry = item . Value ;
522523 if ( entry . CheckExpired ( utcNow ) )
523524 {
524525 entriesToRemove . Add ( entry ) ;
@@ -645,18 +646,59 @@ private static void ValidateCacheKey(object key)
645646 /// </summary>
646647 private sealed class CoherentState
647648 {
648- internal ConcurrentDictionary < object , CacheEntry > _entries = new ConcurrentDictionary < object , CacheEntry > ( ) ;
649+ private readonly ConcurrentDictionary < string , CacheEntry > _stringEntries = new ConcurrentDictionary < string , CacheEntry > ( StringKeyComparer . Instance ) ;
650+ private readonly ConcurrentDictionary < object , CacheEntry > _nonStringEntries = new ConcurrentDictionary < object , CacheEntry > ( ) ;
649651 internal long _cacheSize ;
650652
651- private ICollection < KeyValuePair < object , CacheEntry > > EntriesCollection => _entries ;
653+ internal bool TryGetValue ( object key , [ NotNullWhen ( true ) ] out CacheEntry ? entry )
654+ => key is string s ? _stringEntries . TryGetValue ( s , out entry ) : _nonStringEntries . TryGetValue ( key , out entry ) ;
655+
656+ internal bool TryRemove ( object key , [ NotNullWhen ( true ) ] out CacheEntry ? entry )
657+ => key is string s ? _stringEntries . TryRemove ( s , out entry ) : _nonStringEntries . TryRemove ( key , out entry ) ;
658+
659+ internal bool TryAdd ( object key , CacheEntry entry )
660+ => key is string s ? _stringEntries . TryAdd ( s , entry ) : _nonStringEntries . TryAdd ( key , entry ) ;
661+
662+ internal bool TryUpdate ( object key , CacheEntry entry , CacheEntry comparison )
663+ => key is string s ? _stringEntries . TryUpdate ( s , entry , comparison ) : _nonStringEntries . TryUpdate ( key , entry , comparison ) ;
664+
665+ public IEnumerable < CacheEntry > GetAllValues ( )
666+ {
667+ // note this mimics the outgoing code in that we don't just access
668+ // .Values, which has additional overheads; this is only used for rare
669+ // calls - compaction, clear, etc - so the additional overhead of a
670+ // generated enumerator is not alarming
671+ foreach ( KeyValuePair < string , CacheEntry > entry in _stringEntries )
672+ {
673+ yield return entry . Value ;
674+ }
675+ foreach ( KeyValuePair < object , CacheEntry > entry in _nonStringEntries )
676+ {
677+ yield return entry . Value ;
678+ }
679+ }
680+
681+ private ICollection < KeyValuePair < string , CacheEntry > > StringEntriesCollection => _stringEntries ;
682+ private ICollection < KeyValuePair < object , CacheEntry > > NonStringEntriesCollection => _nonStringEntries ;
652683
653- internal int Count => _entries . Count ;
684+ internal int Count => _stringEntries . Count + _nonStringEntries . Count ;
654685
655686 internal long Size => Volatile . Read ( ref _cacheSize ) ;
656687
657688 internal void RemoveEntry ( CacheEntry entry , MemoryCacheOptions options )
658689 {
659- if ( EntriesCollection . Remove ( new KeyValuePair < object , CacheEntry > ( entry . Key , entry ) ) )
690+ if ( entry . Key is string s )
691+ {
692+ if ( StringEntriesCollection . Remove ( new KeyValuePair < string , CacheEntry > ( s , entry ) ) )
693+ {
694+ if ( options . SizeLimit . HasValue )
695+ {
696+ Interlocked . Add ( ref _cacheSize , - entry . Size ) ;
697+ }
698+ entry . InvokeEvictionCallbacks ( ) ;
699+ }
700+ }
701+ else if ( NonStringEntriesCollection . Remove ( new KeyValuePair < object , CacheEntry > ( entry . Key , entry ) ) )
660702 {
661703 if ( options . SizeLimit . HasValue )
662704 {
@@ -665,6 +707,35 @@ internal void RemoveEntry(CacheEntry entry, MemoryCacheOptions options)
665707 entry . InvokeEvictionCallbacks ( ) ;
666708 }
667709 }
710+
711+ #if NETCOREAPP
712+ // on .NET Core, the inbuilt comparer has Marvin built in; no need to intercept
713+ private static class StringKeyComparer
714+ {
715+ internal static IEqualityComparer < string > Instance => EqualityComparer < string > . Default ;
716+ }
717+ #else
718+ // otherwise, we need a custom comparer that manually implements Marvin
719+ private sealed class StringKeyComparer : IEqualityComparer < string > , IEqualityComparer
720+ {
721+ private StringKeyComparer ( ) { }
722+
723+ internal static readonly IEqualityComparer < string > Instance = new StringKeyComparer ( ) ;
724+
725+ // special-case string keys and use Marvin hashing
726+ public int GetHashCode ( string ? s ) => s is null ? 0
727+ : Marvin . ComputeHash32 ( MemoryMarshal . AsBytes ( s . AsSpan ( ) ) , Marvin . DefaultSeed ) ;
728+
729+ public bool Equals ( string ? x , string ? y )
730+ => string . Equals ( x , y ) ;
731+
732+ bool IEqualityComparer . Equals ( object x , object y )
733+ => object . Equals ( x , y ) ;
734+
735+ int IEqualityComparer . GetHashCode ( object obj )
736+ => obj is string s ? GetHashCode ( s ) : 0 ;
737+ }
738+ #endif
668739 }
669740 }
670741}
0 commit comments