@@ -64,7 +64,7 @@ public void EvictExpired()
64
64
65
65
foreach ( var p in _dict )
66
66
{
67
- if ( currTime > p . Value . TickCountWhenToKill ) //instead of calling "p.Value.IsExpired" we're essentially doing the same thing manually
67
+ if ( p . Value . IsExpired ( currTime ) ) //call IsExpired with "currTime" to avoid calling Environment.TickCount64 multiple times
68
68
_dict . TryRemove ( p ) ;
69
69
}
70
70
}
@@ -181,14 +181,23 @@ public bool TryAdd(TKey key, TValue value, TimeSpan ttl)
181
181
/// <param name="ttl">TTL of the item</param>
182
182
public TValue GetOrAdd ( TKey key , Func < TKey , TValue > valueFactory , TimeSpan ttl )
183
183
{
184
- if ( TryGet ( key , out var value ) )
185
- return value ;
186
-
187
- return _dict . GetOrAdd (
184
+ bool wasAdded = false ; //flag to indicate "add vs get". TODO: wrap in ref type some day to avoid captures/closures
185
+ var ttlValue = _dict . GetOrAdd (
188
186
key ,
189
- ( k , arg ) => new TtlValue ( arg . valueFactory ( k ) , arg . ttl ) ,
190
- ( ttl , valueFactory )
191
- ) . Value ;
187
+ ( k ) =>
188
+ {
189
+ wasAdded = true ;
190
+ return new TtlValue ( valueFactory ( k ) , ttl ) ;
191
+ } ) ;
192
+
193
+ //if the item is expired, update value and TTL
194
+ //since TtlValue is a reference type we can update its properties in-place, instead of removing and re-adding to the dictionary (extra lookups)
195
+ if ( ! wasAdded ) //performance hack: skip expiration check if a brand item was just added
196
+ {
197
+ ttlValue . ModifyIfExpired ( ( ) => valueFactory ( key ) , ttl ) ;
198
+ }
199
+
200
+ return ttlValue . Value ;
192
201
}
193
202
194
203
/// <summary>
@@ -200,14 +209,23 @@ public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory, TimeSpan ttl)
200
209
/// <param name="factoryArgument">Argument value to pass into valueFactory</param>
201
210
public TValue GetOrAdd < TArg > ( TKey key , Func < TKey , TArg , TValue > valueFactory , TimeSpan ttl , TArg factoryArgument )
202
211
{
203
- if ( TryGet ( key , out var value ) )
204
- return value ;
205
-
206
- return _dict . GetOrAdd (
212
+ bool wasAdded = false ; //flag to indicate "add vs get"
213
+ var ttlValue = _dict . GetOrAdd (
207
214
key ,
208
- ( k , arg ) => new TtlValue ( arg . valueFactory ( k , arg . factoryArgument ) , arg . ttl ) ,
209
- ( ttl , valueFactory , factoryArgument )
210
- ) . Value ;
215
+ ( k ) =>
216
+ {
217
+ wasAdded = true ;
218
+ return new TtlValue ( valueFactory ( k , factoryArgument ) , ttl ) ;
219
+ } ) ;
220
+
221
+ //if the item is expired, update value and TTL
222
+ //since TtlValue is a reference type we can update its properties in-place, instead of removing and re-adding to the dictionary (extra lookups)
223
+ if ( ! wasAdded ) //performance hack: skip expiration check if a brand item was just added
224
+ {
225
+ ttlValue . ModifyIfExpired ( ( ) => valueFactory ( key , factoryArgument ) , ttl ) ;
226
+ }
227
+
228
+ return ttlValue . Value ;
211
229
}
212
230
213
231
/// <summary>
@@ -218,10 +236,22 @@ public TValue GetOrAdd<TArg>(TKey key, Func<TKey, TArg, TValue> valueFactory, Ti
218
236
/// <param name="ttl">TTL of the item</param>
219
237
public TValue GetOrAdd ( TKey key , TValue value , TimeSpan ttl )
220
238
{
221
- if ( TryGet ( key , out var existingValue ) )
222
- return existingValue ;
239
+ bool wasAdded = false ; //flag to indicate "add vs get"
240
+ var ttlValue = _dict . GetOrAdd ( key ,
241
+ ( k ) =>
242
+ {
243
+ wasAdded = true ;
244
+ return new TtlValue ( value , ttl ) ;
245
+ } ) ;
246
+
247
+ //if the item is expired, update value and TTL
248
+ //since TtlValue is a reference type we can update its properties in-place, instead of removing and re-adding to the dictionary (extra lookups)
249
+ if ( ! wasAdded ) //performance hack: skip expiration check if a brand item was just added
250
+ {
251
+ ttlValue . ModifyIfExpired ( ( ) => value , ttl ) ;
252
+ }
223
253
224
- return _dict . GetOrAdd ( key , new TtlValue ( value , ttl ) ) . Value ;
254
+ return ttlValue . Value ;
225
255
}
226
256
227
257
/// <summary>
@@ -245,11 +275,13 @@ public bool TryRemove(TKey key, out TValue value)
245
275
return res ;
246
276
}
247
277
278
+ /// <inheritdoc/>
248
279
public IEnumerator < KeyValuePair < TKey , TValue > > GetEnumerator ( )
249
280
{
281
+ var currTime = Environment . TickCount64 ; //save to a var to prevent multiple calls to Environment.TickCount64
250
282
foreach ( var kvp in _dict )
251
283
{
252
- if ( ! kvp . Value . IsExpired ( ) )
284
+ if ( ! kvp . Value . IsExpired ( currTime ) )
253
285
yield return new KeyValuePair < TKey , TValue > ( kvp . Key , kvp . Value . Value ) ;
254
286
}
255
287
}
@@ -261,18 +293,31 @@ IEnumerator IEnumerable.GetEnumerator()
261
293
262
294
private class TtlValue
263
295
{
264
- public readonly TValue Value ;
265
- public readonly long TickCountWhenToKill ;
296
+ public TValue Value { get ; private set ; }
297
+ private long TickCountWhenToKill ;
266
298
267
299
public TtlValue ( TValue value , TimeSpan ttl )
268
300
{
269
301
Value = value ;
270
302
TickCountWhenToKill = Environment . TickCount64 + ( long ) ttl . TotalMilliseconds ;
271
303
}
272
304
273
- public bool IsExpired ( )
305
+ public bool IsExpired ( ) => IsExpired ( Environment . TickCount64 ) ;
306
+
307
+ //use an overload instead of optional param to avoid extra IF's
308
+ public bool IsExpired ( long currTime ) => currTime > TickCountWhenToKill ;
309
+
310
+ /// <summary>
311
+ /// Updates the value and TTL only if the item is expired
312
+ /// </summary>
313
+ public void ModifyIfExpired ( Func < TValue > newValueFactory , TimeSpan newTtl )
274
314
{
275
- return Environment . TickCount64 > TickCountWhenToKill ;
315
+ var ticks = Environment . TickCount64 ; //save to a var to prevent multiple calls to Environment.TickCount64
316
+ if ( IsExpired ( ticks ) ) //if expired - update the value and TTL
317
+ {
318
+ TickCountWhenToKill = ticks + ( long ) newTtl . TotalMilliseconds ; //update the expiration time first for better concurrency
319
+ Value = newValueFactory ( ) ;
320
+ }
276
321
}
277
322
}
278
323
0 commit comments