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 . Runtime . CompilerServices ;
9+ using System . Runtime . InteropServices ;
810using System . Threading ;
911using System . Threading . Tasks ;
1012using Microsoft . Extensions . Internal ;
@@ -23,7 +25,8 @@ public class MemoryCache : IMemoryCache
2325 internal readonly ILogger _logger ;
2426
2527 private readonly MemoryCacheOptions _options ;
26- private readonly ConcurrentDictionary < object , CacheEntry > _entries ;
28+ private readonly ConcurrentDictionary < string , CacheEntry > _stringKeyEntries ;
29+ private readonly ConcurrentDictionary < object , CacheEntry > _nonStringKeyEntries ;
2730
2831 private long _cacheSize ;
2932 private bool _disposed ;
@@ -56,7 +59,8 @@ public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor, ILoggerFactory
5659 _options = optionsAccessor . Value ;
5760 _logger = loggerFactory . CreateLogger < MemoryCache > ( ) ;
5861
59- _entries = new ConcurrentDictionary < object , CacheEntry > ( ) ;
62+ _stringKeyEntries = new ConcurrentDictionary < string , CacheEntry > ( StringKeyComparer . Instance ) ;
63+ _nonStringKeyEntries = new ConcurrentDictionary < object , CacheEntry > ( ) ;
6064
6165 if ( _options . Clock == null )
6266 {
@@ -74,12 +78,14 @@ public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor, ILoggerFactory
7478 /// <summary>
7579 /// Gets the count of the current entries for diagnostic purposes.
7680 /// </summary>
77- public int Count => _entries . Count ;
81+ public int Count => _stringKeyEntries . Count + _nonStringKeyEntries . Count ;
7882
7983 // internal for testing
8084 internal long Size { get => Interlocked . Read ( ref _cacheSize ) ; }
8185
82- private ICollection < KeyValuePair < object , CacheEntry > > EntriesCollection => _entries ;
86+ private ICollection < KeyValuePair < string , CacheEntry > > StringKeyEntriesCollection => _stringKeyEntries ;
87+
88+ private ICollection < KeyValuePair < object , CacheEntry > > NonStringKeyEntriesCollection => _nonStringKeyEntries ;
8389
8490 /// <inheritdoc />
8591 public ICacheEntry CreateEntry ( object key )
@@ -129,7 +135,16 @@ internal void SetEntry(CacheEntry entry)
129135 // Initialize the last access timestamp at the time the entry is added
130136 entry . LastAccessed = utcNow ;
131137
132- if ( _entries . TryGetValue ( entry . Key , out CacheEntry priorEntry ) )
138+ CacheEntry priorEntry = null ;
139+ string s = entry . Key as string ;
140+ if ( s != null )
141+ {
142+ if ( _stringKeyEntries . TryGetValue ( s , out priorEntry ) )
143+ {
144+ priorEntry . SetExpired ( EvictionReason . Replaced ) ;
145+ }
146+ }
147+ else if ( _nonStringKeyEntries . TryGetValue ( entry . Key , out priorEntry ) )
133148 {
134149 priorEntry . SetExpired ( EvictionReason . Replaced ) ;
135150 }
@@ -143,12 +158,26 @@ internal void SetEntry(CacheEntry entry)
143158 if ( priorEntry == null )
144159 {
145160 // Try to add the new entry if no previous entries exist.
146- entryAdded = _entries . TryAdd ( entry . Key , entry ) ;
161+ if ( s != null )
162+ {
163+ entryAdded = _stringKeyEntries . TryAdd ( s , entry ) ;
164+ }
165+ else
166+ {
167+ entryAdded = _nonStringKeyEntries . TryAdd ( entry . Key , entry ) ;
168+ }
147169 }
148170 else
149171 {
150172 // Try to update with the new entry if a previous entries exist.
151- entryAdded = _entries . TryUpdate ( entry . Key , entry , priorEntry ) ;
173+ if ( s != null )
174+ {
175+ entryAdded = _stringKeyEntries . TryUpdate ( s , entry , priorEntry ) ;
176+ }
177+ else
178+ {
179+ entryAdded = _nonStringKeyEntries . TryUpdate ( entry . Key , entry , priorEntry ) ;
180+ }
152181
153182 if ( entryAdded )
154183 {
@@ -163,7 +192,14 @@ internal void SetEntry(CacheEntry entry)
163192 // The update will fail if the previous entry was removed after retrival.
164193 // Adding the new entry will succeed only if no entry has been added since.
165194 // This guarantees removing an old entry does not prevent adding a new entry.
166- entryAdded = _entries . TryAdd ( entry . Key , entry ) ;
195+ if ( s != null )
196+ {
197+ entryAdded = _stringKeyEntries . TryAdd ( s , entry ) ;
198+ }
199+ else
200+ {
201+ entryAdded = _nonStringKeyEntries . TryAdd ( entry . Key , entry ) ;
202+ }
167203 }
168204 }
169205
@@ -223,7 +259,18 @@ public bool TryGetValue(object key, out object result)
223259
224260 DateTimeOffset utcNow = _options . Clock . UtcNow ;
225261
226- if ( _entries . TryGetValue ( key , out CacheEntry entry ) )
262+ bool found ;
263+ CacheEntry entry ;
264+ if ( key is string s )
265+ {
266+ found = _stringKeyEntries . TryGetValue ( s , out entry ) ;
267+ }
268+ else
269+ {
270+ found = _nonStringKeyEntries . TryGetValue ( key , out entry ) ;
271+ }
272+
273+ if ( found )
227274 {
228275 // Check if expired due to expiration tokens, timers, etc. and if so, remove it.
229276 // Allow a stale Replaced value to be returned due to concurrent calls to SetExpired during SetEntry.
@@ -262,7 +309,18 @@ public void Remove(object key)
262309 ValidateCacheKey ( key ) ;
263310
264311 CheckDisposed ( ) ;
265- if ( _entries . TryRemove ( key , out CacheEntry entry ) )
312+ bool removed ;
313+ CacheEntry entry ;
314+ if ( key is string s )
315+ {
316+ removed = _stringKeyEntries . TryRemove ( s , out entry ) ;
317+ }
318+ else
319+ {
320+ removed = _nonStringKeyEntries . TryRemove ( key , out entry ) ;
321+ }
322+
323+ if ( removed )
266324 {
267325 if ( _options . SizeLimit . HasValue )
268326 {
@@ -278,7 +336,17 @@ public void Remove(object key)
278336
279337 private void RemoveEntry ( CacheEntry entry )
280338 {
281- if ( EntriesCollection . Remove ( new KeyValuePair < object , CacheEntry > ( entry . Key , entry ) ) )
339+ bool removed ;
340+ if ( entry . Key is string s )
341+ {
342+ removed = StringKeyEntriesCollection . Remove ( new KeyValuePair < string , CacheEntry > ( s , entry ) ) ;
343+ }
344+ else
345+ {
346+ removed = NonStringKeyEntriesCollection . Remove ( new KeyValuePair < object , CacheEntry > ( entry . Key , entry ) ) ;
347+ }
348+
349+ if ( removed )
282350 {
283351 if ( _options . SizeLimit . HasValue )
284352 {
@@ -317,10 +385,8 @@ private static void ScanForExpiredItems(MemoryCache cache)
317385 {
318386 DateTimeOffset now = cache . _lastExpirationScan = cache . _options . Clock . UtcNow ;
319387
320- foreach ( KeyValuePair < object , CacheEntry > item in cache . _entries )
388+ foreach ( CacheEntry entry in cache . GetCacheEntries ( ) )
321389 {
322- CacheEntry entry = item . Value ;
323-
324390 if ( entry . CheckExpired ( now ) )
325391 {
326392 cache . RemoveEntry ( entry ) ;
@@ -388,10 +454,26 @@ private static void OvercapacityCompaction(MemoryCache cache)
388454 /// ?. Larger objects - estimated by object graph size, inaccurate.
389455 public void Compact ( double percentage )
390456 {
391- int removalCountTarget = ( int ) ( _entries . Count * percentage ) ;
457+ int removalCountTarget = ( int ) ( Count * percentage ) ;
392458 Compact ( removalCountTarget , _ => 1 ) ;
393459 }
394460
461+ private IEnumerable < CacheEntry > GetCacheEntries ( )
462+ {
463+ // note this mimics the outgoing code in that we don't just access
464+ // .Values, which has additional overheads; this is only used for rare
465+ // calls - compaction, clear, etc - so the additional overhead of a
466+ // generated enumerator is not alarming
467+ foreach ( KeyValuePair < string , CacheEntry > item in _stringKeyEntries )
468+ {
469+ yield return item . Value ;
470+ }
471+ foreach ( KeyValuePair < object , CacheEntry > item in _nonStringKeyEntries )
472+ {
473+ yield return item . Value ;
474+ }
475+ }
476+
395477 private void Compact ( long removalSizeTarget , Func < CacheEntry , long > computeEntrySize )
396478 {
397479 var entriesToRemove = new List < CacheEntry > ( ) ;
@@ -403,9 +485,8 @@ private void Compact(long removalSizeTarget, Func<CacheEntry, long> computeEntry
403485
404486 // Sort items by expired & priority status
405487 DateTimeOffset now = _options . Clock . UtcNow ;
406- foreach ( KeyValuePair < object , CacheEntry > item in _entries )
488+ foreach ( CacheEntry entry in GetCacheEntries ( ) )
407489 {
408- CacheEntry entry = item . Value ;
409490 if ( entry . CheckExpired ( now ) )
410491 {
411492 entriesToRemove . Add ( entry ) ;
@@ -526,5 +607,34 @@ private static void ValidateCacheKey(object key)
526607
527608 static void Throw ( ) => throw new ArgumentNullException ( nameof ( key ) ) ;
528609 }
610+
611+ #if NETCOREAPP
612+ // on .NET Core, the inbuilt comparer has Marvin built in; no need to intercept
613+ private static class StringKeyComparer
614+ {
615+ internal static IEqualityComparer < string > Instance => EqualityComparer < string > . Default ;
616+ }
617+ #else
618+ // otherwise, we need a custom comparer that manually implements Marvin
619+ private sealed class StringKeyComparer : IEqualityComparer < string > , IEqualityComparer
620+ {
621+ private StringKeyComparer ( ) { }
622+
623+ internal static readonly IEqualityComparer < string > Instance = new StringKeyComparer ( ) ;
624+
625+ // special-case string keys and use Marvin hashing
626+ public int GetHashCode ( string ? s ) => s is null ? 0
627+ : Marvin . ComputeHash32 ( MemoryMarshal . AsBytes ( s . AsSpan ( ) ) , Marvin . DefaultSeed ) ;
628+
629+ public bool Equals ( string ? x , string ? y )
630+ => string . Equals ( x , y ) ;
631+
632+ bool IEqualityComparer . Equals ( object x , object y )
633+ => object . Equals ( x , y ) ;
634+
635+ int IEqualityComparer . GetHashCode ( object obj )
636+ => obj is string s ? Marvin . ComputeHash32 ( MemoryMarshal . AsBytes ( s . AsSpan ( ) ) , Marvin . DefaultSeed ) : 0 ;
637+ }
638+ #endif
529639 }
530640}
0 commit comments