2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
4
using System ;
5
+ using System . Collections ;
5
6
using System . Collections . Concurrent ;
6
7
using System . Collections . Generic ;
7
8
using System . Diagnostics . CodeAnalysis ;
8
9
using System . Runtime . CompilerServices ;
10
+ using System . Runtime . InteropServices ;
9
11
using System . Threading ;
10
12
using System . Threading . Tasks ;
11
13
using Microsoft . Extensions . Logging ;
@@ -126,7 +128,7 @@ internal void SetEntry(CacheEntry entry)
126
128
entry . LastAccessed = utcNow ;
127
129
128
130
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 ) )
130
132
{
131
133
priorEntry . SetExpired ( EvictionReason . Replaced ) ;
132
134
}
@@ -145,12 +147,12 @@ internal void SetEntry(CacheEntry entry)
145
147
if ( priorEntry == null )
146
148
{
147
149
// 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 ) ;
149
151
}
150
152
else
151
153
{
152
154
// 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 ) ;
154
156
155
157
if ( entryAdded )
156
158
{
@@ -165,7 +167,7 @@ internal void SetEntry(CacheEntry entry)
165
167
// The update will fail if the previous entry was removed after retrieval.
166
168
// Adding the new entry will succeed only if no entry has been added since.
167
169
// 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 ) ;
169
171
}
170
172
}
171
173
@@ -210,7 +212,7 @@ public bool TryGetValue(object key, out object? result)
210
212
DateTime utcNow = UtcNow ;
211
213
212
214
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 ) )
214
216
{
215
217
CacheEntry entry = tmp ;
216
218
// Check if expired due to expiration tokens, timers, etc. and if so, remove it.
@@ -269,7 +271,8 @@ public void Remove(object key)
269
271
CheckDisposed ( ) ;
270
272
271
273
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 ) )
273
276
{
274
277
if ( _options . HasSizeLimit )
275
278
{
@@ -291,10 +294,10 @@ public void Clear()
291
294
CheckDisposed ( ) ;
292
295
293
296
CoherentState oldState = Interlocked . Exchange ( ref _coherentState , new CoherentState ( ) ) ;
294
- foreach ( KeyValuePair < object , CacheEntry > entry in oldState . _entries )
297
+ foreach ( CacheEntry entry in oldState . GetAllValues ( ) )
295
298
{
296
- entry . Value . SetExpired ( EvictionReason . Removed ) ;
297
- entry . Value . InvokeEvictionCallbacks ( ) ;
299
+ entry . SetExpired ( EvictionReason . Removed ) ;
300
+ entry . InvokeEvictionCallbacks ( ) ;
298
301
}
299
302
}
300
303
@@ -415,10 +418,9 @@ private void ScanForExpiredItems()
415
418
DateTime utcNow = _lastExpirationScan = UtcNow ;
416
419
417
420
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 ;
421
421
422
+ foreach ( CacheEntry entry in coherentState . GetAllValues ( ) )
423
+ {
422
424
if ( entry . CheckExpired ( utcNow ) )
423
425
{
424
426
coherentState . RemoveEntry ( entry , _options ) ;
@@ -516,9 +518,8 @@ private void Compact(long removalSizeTarget, Func<CacheEntry, long> computeEntry
516
518
517
519
// Sort items by expired & priority status
518
520
DateTime utcNow = UtcNow ;
519
- foreach ( KeyValuePair < object , CacheEntry > item in coherentState . _entries )
521
+ foreach ( CacheEntry entry in coherentState . GetAllValues ( ) )
520
522
{
521
- CacheEntry entry = item . Value ;
522
523
if ( entry . CheckExpired ( utcNow ) )
523
524
{
524
525
entriesToRemove . Add ( entry ) ;
@@ -645,18 +646,59 @@ private static void ValidateCacheKey(object key)
645
646
/// </summary>
646
647
private sealed class CoherentState
647
648
{
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 > ( ) ;
649
651
internal long _cacheSize ;
650
652
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 ;
652
683
653
- internal int Count => _entries . Count ;
684
+ internal int Count => _stringEntries . Count + _nonStringEntries . Count ;
654
685
655
686
internal long Size => Volatile . Read ( ref _cacheSize ) ;
656
687
657
688
internal void RemoveEntry ( CacheEntry entry , MemoryCacheOptions options )
658
689
{
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 ) ) )
660
702
{
661
703
if ( options . SizeLimit . HasValue )
662
704
{
@@ -665,6 +707,35 @@ internal void RemoveEntry(CacheEntry entry, MemoryCacheOptions options)
665
707
entry . InvokeEvictionCallbacks ( ) ;
666
708
}
667
709
}
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
668
739
}
669
740
}
670
741
}
0 commit comments