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 . Runtime . CompilerServices ;
9
+ using System . Runtime . InteropServices ;
8
10
using System . Threading ;
9
11
using System . Threading . Tasks ;
10
12
using Microsoft . Extensions . Internal ;
@@ -23,7 +25,8 @@ public class MemoryCache : IMemoryCache
23
25
internal readonly ILogger _logger ;
24
26
25
27
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 ;
27
30
28
31
private long _cacheSize ;
29
32
private bool _disposed ;
@@ -56,7 +59,8 @@ public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor, ILoggerFactory
56
59
_options = optionsAccessor . Value ;
57
60
_logger = loggerFactory . CreateLogger < MemoryCache > ( ) ;
58
61
59
- _entries = new ConcurrentDictionary < object , CacheEntry > ( ) ;
62
+ _stringKeyEntries = new ConcurrentDictionary < string , CacheEntry > ( StringKeyComparer . Instance ) ;
63
+ _nonStringKeyEntries = new ConcurrentDictionary < object , CacheEntry > ( ) ;
60
64
61
65
if ( _options . Clock == null )
62
66
{
@@ -74,12 +78,14 @@ public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor, ILoggerFactory
74
78
/// <summary>
75
79
/// Gets the count of the current entries for diagnostic purposes.
76
80
/// </summary>
77
- public int Count => _entries . Count ;
81
+ public int Count => _stringKeyEntries . Count + _nonStringKeyEntries . Count ;
78
82
79
83
// internal for testing
80
84
internal long Size { get => Interlocked . Read ( ref _cacheSize ) ; }
81
85
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 ;
83
89
84
90
/// <inheritdoc />
85
91
public ICacheEntry CreateEntry ( object key )
@@ -129,7 +135,16 @@ internal void SetEntry(CacheEntry entry)
129
135
// Initialize the last access timestamp at the time the entry is added
130
136
entry . LastAccessed = utcNow ;
131
137
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 ) )
133
148
{
134
149
priorEntry . SetExpired ( EvictionReason . Replaced ) ;
135
150
}
@@ -143,12 +158,26 @@ internal void SetEntry(CacheEntry entry)
143
158
if ( priorEntry == null )
144
159
{
145
160
// 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
+ }
147
169
}
148
170
else
149
171
{
150
172
// 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
+ }
152
181
153
182
if ( entryAdded )
154
183
{
@@ -163,7 +192,14 @@ internal void SetEntry(CacheEntry entry)
163
192
// The update will fail if the previous entry was removed after retrival.
164
193
// Adding the new entry will succeed only if no entry has been added since.
165
194
// 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
+ }
167
203
}
168
204
}
169
205
@@ -223,7 +259,18 @@ public bool TryGetValue(object key, out object result)
223
259
224
260
DateTimeOffset utcNow = _options . Clock . UtcNow ;
225
261
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 )
227
274
{
228
275
// Check if expired due to expiration tokens, timers, etc. and if so, remove it.
229
276
// 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)
262
309
ValidateCacheKey ( key ) ;
263
310
264
311
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 )
266
324
{
267
325
if ( _options . SizeLimit . HasValue )
268
326
{
@@ -278,7 +336,17 @@ public void Remove(object key)
278
336
279
337
private void RemoveEntry ( CacheEntry entry )
280
338
{
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 )
282
350
{
283
351
if ( _options . SizeLimit . HasValue )
284
352
{
@@ -317,10 +385,8 @@ private static void ScanForExpiredItems(MemoryCache cache)
317
385
{
318
386
DateTimeOffset now = cache . _lastExpirationScan = cache . _options . Clock . UtcNow ;
319
387
320
- foreach ( KeyValuePair < object , CacheEntry > item in cache . _entries )
388
+ foreach ( CacheEntry entry in cache . GetCacheEntries ( ) )
321
389
{
322
- CacheEntry entry = item . Value ;
323
-
324
390
if ( entry . CheckExpired ( now ) )
325
391
{
326
392
cache . RemoveEntry ( entry ) ;
@@ -388,10 +454,26 @@ private static void OvercapacityCompaction(MemoryCache cache)
388
454
/// ?. Larger objects - estimated by object graph size, inaccurate.
389
455
public void Compact ( double percentage )
390
456
{
391
- int removalCountTarget = ( int ) ( _entries . Count * percentage ) ;
457
+ int removalCountTarget = ( int ) ( Count * percentage ) ;
392
458
Compact ( removalCountTarget , _ => 1 ) ;
393
459
}
394
460
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
+
395
477
private void Compact ( long removalSizeTarget , Func < CacheEntry , long > computeEntrySize )
396
478
{
397
479
var entriesToRemove = new List < CacheEntry > ( ) ;
@@ -403,9 +485,8 @@ private void Compact(long removalSizeTarget, Func<CacheEntry, long> computeEntry
403
485
404
486
// Sort items by expired & priority status
405
487
DateTimeOffset now = _options . Clock . UtcNow ;
406
- foreach ( KeyValuePair < object , CacheEntry > item in _entries )
488
+ foreach ( CacheEntry entry in GetCacheEntries ( ) )
407
489
{
408
- CacheEntry entry = item . Value ;
409
490
if ( entry . CheckExpired ( now ) )
410
491
{
411
492
entriesToRemove . Add ( entry ) ;
@@ -526,5 +607,34 @@ private static void ValidateCacheKey(object key)
526
607
527
608
static void Throw ( ) => throw new ArgumentNullException ( nameof ( key ) ) ;
528
609
}
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
529
639
}
530
640
}
0 commit comments