Skip to content

Commit 8465588

Browse files
authored
factory delegate has arg to avoid closure allocs (#339)
* arg * basic tests * docs * simpler * same for LFU ---------
1 parent 4320996 commit 8465588

File tree

5 files changed

+212
-24
lines changed

5 files changed

+212
-24
lines changed

BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ public void WhenKeyIsRequestedItIsCreatedAndCached()
4343
result1.Should().Be(result2);
4444
}
4545

46+
[Fact]
47+
public void WhenKeyIsRequestedWithArgItIsCreatedAndCached()
48+
{
49+
var result1 = cache.GetOrAdd(1, valueFactory.Create, 9);
50+
var result2 = cache.GetOrAdd(1, valueFactory.Create, 17);
51+
52+
valueFactory.timesCalled.Should().Be(1);
53+
result1.Should().Be(result2);
54+
}
55+
4656
[Fact]
4757
public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync()
4858
{
@@ -53,6 +63,16 @@ public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync()
5363
result1.Should().Be(result2);
5464
}
5565

66+
[Fact]
67+
public async Task WhenKeyIsRequestedWithArgItIsCreatedAndCachedAsync()
68+
{
69+
var result1 = await cache.GetOrAddAsync(1, valueFactory.CreateAsync, 9).ConfigureAwait(false);
70+
var result2 = await cache.GetOrAddAsync(1, valueFactory.CreateAsync, 17).ConfigureAwait(false);
71+
72+
valueFactory.timesCalled.Should().Be(1);
73+
result1.Should().Be(result2);
74+
}
75+
5676
[Fact]
5777
public void WhenItemsAddedExceedsCapacityItemsAreDiscarded()
5878
{
@@ -852,11 +872,23 @@ public int Create(int key)
852872
return key;
853873
}
854874

875+
public int Create(int key, int arg)
876+
{
877+
timesCalled++;
878+
return key + arg;
879+
}
880+
855881
public Task<int> CreateAsync(int key)
856882
{
857883
timesCalled++;
858884
return Task.FromResult(key);
859885
}
886+
887+
public Task<int> CreateAsync(int key, int arg)
888+
{
889+
timesCalled++;
890+
return Task.FromResult(key + arg);
891+
}
860892
}
861893
}
862894
}

BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,17 @@ public void WhenKeyIsRequestedItIsCreatedAndCached()
246246
}
247247

248248
[Fact]
249-
public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync()
249+
public void WhenKeyIsRequestedWithArgItIsCreatedAndCached()
250+
{
251+
var result1 = lru.GetOrAdd(1, valueFactory.Create, "x");
252+
var result2 = lru.GetOrAdd(1, valueFactory.Create, "y");
253+
254+
valueFactory.timesCalled.Should().Be(1);
255+
result1.Should().Be(result2);
256+
}
257+
258+
[Fact]
259+
public async Task WhenKeyIsRequestedItIsCreatedAndCachedAsync()
250260
{
251261
var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false);
252262
var result2 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false);
@@ -255,6 +265,16 @@ public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync()
255265
result1.Should().Be(result2);
256266
}
257267

268+
[Fact]
269+
public async Task WhenKeyIsRequestedWithArgItIsCreatedAndCachedAsync()
270+
{
271+
var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync, "x").ConfigureAwait(false);
272+
var result2 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync, "y").ConfigureAwait(false);
273+
274+
valueFactory.timesCalled.Should().Be(1);
275+
result1.Should().Be(result2);
276+
}
277+
258278
[Fact]
259279
public void WhenDifferentKeysAreRequestedValueIsCreatedForEach()
260280
{

BitFaster.Caching.UnitTests/Lru/ValueFactory.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,22 @@ public string Create(int key)
1515
return key.ToString();
1616
}
1717

18+
public string Create<TArg>(int key, TArg arg)
19+
{
20+
timesCalled++;
21+
return $"{key}{arg}";
22+
}
23+
1824
public Task<string> CreateAsync(int key)
1925
{
2026
timesCalled++;
2127
return Task.FromResult(key.ToString());
2228
}
29+
30+
public Task<string> CreateAsync<TArg>(int key, TArg arg)
31+
{
32+
timesCalled++;
33+
return Task.FromResult($"{key}{arg}");
34+
}
2335
}
2436
}

BitFaster.Caching/Lfu/ConcurrentLfu.cs

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,20 @@ public void Trim(int itemCount)
194194
}
195195
}
196196

197+
private bool TryAdd(K key, V value)
198+
{
199+
var node = new LfuNode<K, V>(key, value);
200+
201+
if (this.dictionary.TryAdd(key, node))
202+
{
203+
AfterWrite(node);
204+
return true;
205+
}
206+
207+
Disposer<V>.Dispose(node.Value);
208+
return false;
209+
}
210+
197211
///<inheritdoc/>
198212
public V GetOrAdd(K key, Func<K, V> valueFactory)
199213
{
@@ -204,14 +218,38 @@ public V GetOrAdd(K key, Func<K, V> valueFactory)
204218
return value;
205219
}
206220

207-
var node = new LfuNode<K, V>(key, valueFactory(key));
208-
if (this.dictionary.TryAdd(key, node))
221+
value = valueFactory(key);
222+
if (this.TryAdd(key, value))
209223
{
210-
AfterWrite(node);
211-
return node.Value;
224+
return value;
225+
}
226+
}
227+
}
228+
229+
/// <summary>
230+
/// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the
231+
/// existing value if the key already exists.
232+
/// </summary>
233+
/// <typeparam name="TArg">The type of an argument to pass into valueFactory.</typeparam>
234+
/// <param name="key">The key of the element to add.</param>
235+
/// <param name="valueFactory">The factory function used to generate a value for the key.</param>
236+
/// <param name="factoryArgument">An argument value to pass into valueFactory.</param>
237+
/// <returns>The value for the key. This will be either the existing value for the key if the key is already
238+
/// in the cache, or the new value if the key was not in the cache.</returns>
239+
public V GetOrAdd<TArg>(K key, Func<K, TArg, V> valueFactory, TArg factoryArgument)
240+
{
241+
while (true)
242+
{
243+
if (this.TryGet(key, out V value))
244+
{
245+
return value;
212246
}
213247

214-
Disposer<V>.Dispose(node.Value);
248+
value = valueFactory(key, factoryArgument);
249+
if (this.TryAdd(key, value))
250+
{
251+
return value;
252+
}
215253
}
216254
}
217255

@@ -225,14 +263,37 @@ public async ValueTask<V> GetOrAddAsync(K key, Func<K, Task<V>> valueFactory)
225263
return value;
226264
}
227265

228-
var node = new LfuNode<K, V>(key, await valueFactory(key).ConfigureAwait(false));
229-
if (this.dictionary.TryAdd(key, node))
266+
value = await valueFactory(key).ConfigureAwait(false);
267+
if (this.TryAdd(key, value))
230268
{
231-
AfterWrite(node);
232-
return node.Value;
269+
return value;
233270
}
271+
}
272+
}
234273

235-
Disposer<V>.Dispose(node.Value);
274+
/// <summary>
275+
/// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the
276+
/// existing value if the key already exists.
277+
/// </summary>
278+
/// <typeparam name="TArg">The type of an argument to pass into valueFactory.</typeparam>
279+
/// <param name="key">The key of the element to add.</param>
280+
/// <param name="valueFactory">The factory function used to asynchronously generate a value for the key.</param>
281+
/// <param name="factoryArgument">An argument value to pass into valueFactory.</param>
282+
/// <returns>A task that represents the asynchronous GetOrAdd operation.</returns>
283+
public async ValueTask<V> GetOrAddAsync<TArg>(K key, Func<K, TArg, Task<V>> valueFactory, TArg factoryArgument)
284+
{
285+
while (true)
286+
{
287+
if (this.TryGet(key, out V value))
288+
{
289+
return value;
290+
}
291+
292+
value = await valueFactory(key, factoryArgument).ConfigureAwait(false);
293+
if (this.TryAdd(key, value))
294+
{
295+
return value;
296+
}
236297
}
237298
}
238299

BitFaster.Caching/Lru/ConcurrentLruCore.cs

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,21 @@ private bool GetOrDiscard(I item, out V value)
184184
return true;
185185
}
186186

187+
private bool TryAdd(K key, V value)
188+
{
189+
var newItem = this.itemPolicy.CreateItem(key, value);
190+
191+
if (this.dictionary.TryAdd(key, newItem))
192+
{
193+
this.hotQueue.Enqueue(newItem);
194+
Cycle(Interlocked.Increment(ref counter.hot));
195+
return true;
196+
}
197+
198+
Disposer<V>.Dispose(newItem.Value);
199+
return false;
200+
}
201+
187202
///<inheritdoc/>
188203
public V GetOrAdd(K key, Func<K, V> valueFactory)
189204
{
@@ -195,17 +210,41 @@ public V GetOrAdd(K key, Func<K, V> valueFactory)
195210
}
196211

197212
// The value factory may be called concurrently for the same key, but the first write to the dictionary wins.
198-
// This is identical logic in ConcurrentDictionary.GetOrAdd method.
199-
var newItem = this.itemPolicy.CreateItem(key, valueFactory(key));
213+
value = valueFactory(key);
200214

201-
if (this.dictionary.TryAdd(key, newItem))
215+
if (TryAdd(key, value))
202216
{
203-
this.hotQueue.Enqueue(newItem);
204-
Cycle(Interlocked.Increment(ref counter.hot));
205-
return newItem.Value;
217+
return value;
206218
}
219+
}
220+
}
207221

208-
Disposer<V>.Dispose(newItem.Value);
222+
/// <summary>
223+
/// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the
224+
/// existing value if the key already exists.
225+
/// </summary>
226+
/// <typeparam name="TArg">The type of an argument to pass into valueFactory.</typeparam>
227+
/// <param name="key">The key of the element to add.</param>
228+
/// <param name="valueFactory">The factory function used to generate a value for the key.</param>
229+
/// <param name="factoryArgument">An argument value to pass into valueFactory.</param>
230+
/// <returns>The value for the key. This will be either the existing value for the key if the key is already
231+
/// in the cache, or the new value if the key was not in the cache.</returns>
232+
public V GetOrAdd<TArg>(K key, Func<K, TArg, V> valueFactory, TArg factoryArgument)
233+
{
234+
while (true)
235+
{
236+
if (this.TryGet(key, out var value))
237+
{
238+
return value;
239+
}
240+
241+
// The value factory may be called concurrently for the same key, but the first write to the dictionary wins.
242+
value = valueFactory(key, factoryArgument);
243+
244+
if (TryAdd(key, value))
245+
{
246+
return value;
247+
}
209248
}
210249
}
211250

@@ -221,16 +260,40 @@ public async ValueTask<V> GetOrAddAsync(K key, Func<K, Task<V>> valueFactory)
221260

222261
// The value factory may be called concurrently for the same key, but the first write to the dictionary wins.
223262
// This is identical logic in ConcurrentDictionary.GetOrAdd method.
224-
var newItem = this.itemPolicy.CreateItem(key, await valueFactory(key).ConfigureAwait(false));
263+
value = await valueFactory(key).ConfigureAwait(false);
225264

226-
if (this.dictionary.TryAdd(key, newItem))
265+
if (TryAdd(key, value))
227266
{
228-
this.hotQueue.Enqueue(newItem);
229-
Cycle(Interlocked.Increment(ref counter.hot));
230-
return newItem.Value;
267+
return value;
231268
}
269+
}
270+
}
232271

233-
Disposer<V>.Dispose(newItem.Value);
272+
/// <summary>
273+
/// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the
274+
/// existing value if the key already exists.
275+
/// </summary>
276+
/// <typeparam name="TArg">The type of an argument to pass into valueFactory.</typeparam>
277+
/// <param name="key">The key of the element to add.</param>
278+
/// <param name="valueFactory">The factory function used to asynchronously generate a value for the key.</param>
279+
/// <param name="factoryArgument">An argument value to pass into valueFactory.</param>
280+
/// <returns>A task that represents the asynchronous GetOrAdd operation.</returns>
281+
public async ValueTask<V> GetOrAddAsync<TArg>(K key, Func<K, TArg, Task<V>> valueFactory, TArg factoryArgument)
282+
{
283+
while (true)
284+
{
285+
if (this.TryGet(key, out var value))
286+
{
287+
return value;
288+
}
289+
290+
// The value factory may be called concurrently for the same key, but the first write to the dictionary wins.
291+
value = await valueFactory(key, factoryArgument).ConfigureAwait(false);
292+
293+
if (TryAdd(key, value))
294+
{
295+
return value;
296+
}
234297
}
235298
}
236299

0 commit comments

Comments
 (0)