diff --git a/BitFaster.Caching.Benchmarks/Lru/LruAsyncGet.cs b/BitFaster.Caching.Benchmarks/Lru/LruAsyncGet.cs
new file mode 100644
index 00000000..02742ee1
--- /dev/null
+++ b/BitFaster.Caching.Benchmarks/Lru/LruAsyncGet.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using BitFaster.Caching.Lru;
+
+namespace BitFaster.Caching.Benchmarks.Lru
+{
+ ///
+ /// Verify 0 allocs for GetOrAddAsync cache hits.
+ ///
+ [SimpleJob(RuntimeMoniker.Net48)]
+ [SimpleJob(RuntimeMoniker.Net60)]
+ [DisassemblyDiagnoser(printSource: true, maxDepth: 5)]
+ [MemoryDiagnoser]
+ public class LruAsyncGet
+ {
+ // if the cache value is a value type, value task has no effect - so use string to repro.
+ private static readonly IAsyncCache concurrentLru = new ConcurrentLruBuilder().AsAsyncCache().Build();
+ private static readonly IAsyncCache atomicConcurrentLru = new ConcurrentLruBuilder().AsAsyncCache().WithAtomicCreate().Build();
+
+ private static Task returnTask = Task.FromResult("1");
+
+ [Benchmark()]
+ public async ValueTask GetOrAddAsync()
+ {
+ Func> func = x => returnTask;
+
+ return await concurrentLru.GetOrAddAsync(1, func);
+ }
+
+ [Benchmark()]
+ public async ValueTask AtomicGetOrAddAsync()
+ {
+ Func> func = x => returnTask;
+
+ return await atomicConcurrentLru.GetOrAddAsync(1, func);
+ }
+ }
+}
diff --git a/BitFaster.Caching.UnitTests/Synchronized/AsyncAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AsyncAtomicFactoryTests.cs
index 18f8e3ef..e2885f34 100644
--- a/BitFaster.Caching.UnitTests/Synchronized/AsyncAtomicFactoryTests.cs
+++ b/BitFaster.Caching.UnitTests/Synchronized/AsyncAtomicFactoryTests.cs
@@ -70,7 +70,7 @@ public async Task WhenCallersRunConcurrentlyResultIsFromWinner()
var result = 0;
var winnerCount = 0;
- Task first = atomicFactory.GetValueAsync(1, async k =>
+ var first = atomicFactory.GetValueAsync(1, async k =>
{
enter.SetResult(true);
await resume.Task;
@@ -80,7 +80,7 @@ public async Task WhenCallersRunConcurrentlyResultIsFromWinner()
return 1;
});
- Task second = atomicFactory.GetValueAsync(1, async k =>
+ var second = atomicFactory.GetValueAsync(1, async k =>
{
enter.SetResult(true);
await resume.Task;
diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryCacheTests.cs
index 55fa417e..722882d1 100644
--- a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryCacheTests.cs
+++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryCacheTests.cs
@@ -106,14 +106,6 @@ public void WhenKeyDoesNotExistGetOrAddAddsValue()
value.Should().Be(1);
}
- [Fact]
- public async Task GetOrAddAsyncThrows()
- {
- Func getOrAdd = async () => { await this.cache.GetOrAddAsync(1, k => Task.FromResult(k)); };
-
- await getOrAdd.Should().ThrowAsync();
- }
-
[Fact]
public void WhenCacheContainsValuesTrim1RemovesColdestValue()
{
diff --git a/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs
index bd04a979..f7b3833d 100644
--- a/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs
+++ b/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs
@@ -98,7 +98,7 @@ public async Task WhenCallersRunConcurrentlyResultIsFromWinner()
var winningNumber = 0;
var winnerCount = 0;
- Task<(bool r, Lifetime l)> first = atomicFactory.TryCreateLifetimeAsync(1, async k =>
+ ValueTask<(bool r, Lifetime l)> first = atomicFactory.TryCreateLifetimeAsync(1, async k =>
{
enter.SetResult(true);
await resume.Task;
@@ -108,7 +108,7 @@ public async Task WhenCallersRunConcurrentlyResultIsFromWinner()
return new Scoped(new IntHolder() { actualNumber = 1 });
});
- Task<(bool r, Lifetime l)> second = atomicFactory.TryCreateLifetimeAsync(1, async k =>
+ ValueTask<(bool r, Lifetime l)> second = atomicFactory.TryCreateLifetimeAsync(1, async k =>
{
enter.SetResult(true);
await resume.Task;
@@ -142,7 +142,7 @@ public async Task WhenDisposedWhileInitResultIsDisposed()
var atomicFactory = new ScopedAsyncAtomicFactory();
var holder = new IntHolder() { actualNumber = 1 };
- Task<(bool r, Lifetime l)> first = atomicFactory.TryCreateLifetimeAsync(1, async k =>
+ ValueTask<(bool r, Lifetime l)> first = atomicFactory.TryCreateLifetimeAsync(1, async k =>
{
enter.SetResult(true);
await resume.Task;
@@ -171,7 +171,7 @@ public async Task WhenDisposedWhileThrowingNextInitIsDisposed()
var atomicFactory = new ScopedAsyncAtomicFactory();
var holder = new IntHolder() { actualNumber = 1 };
- Task<(bool r, Lifetime l)> first = atomicFactory.TryCreateLifetimeAsync(1, async k =>
+ ValueTask<(bool r, Lifetime l)> first = atomicFactory.TryCreateLifetimeAsync(1, async k =>
{
enter.SetResult(true);
await resume.Task;
diff --git a/BitFaster.Caching/BitFaster.Caching.csproj b/BitFaster.Caching/BitFaster.Caching.csproj
index b217ca49..2a4fab3b 100644
--- a/BitFaster.Caching/BitFaster.Caching.csproj
+++ b/BitFaster.Caching/BitFaster.Caching.csproj
@@ -39,4 +39,8 @@
+
+
+
+
diff --git a/BitFaster.Caching/IAsyncCache.cs b/BitFaster.Caching/IAsyncCache.cs
index 7e547cb1..7f837876 100644
--- a/BitFaster.Caching/IAsyncCache.cs
+++ b/BitFaster.Caching/IAsyncCache.cs
@@ -48,7 +48,7 @@ public interface IAsyncCache
/// The key of the element to add.
/// The factory function used to asynchronously generate a value for the key.
/// A task that represents the asynchronous GetOrAdd operation.
- Task GetOrAddAsync(K key, Func> valueFactory);
+ ValueTask GetOrAddAsync(K key, Func> valueFactory);
///
/// Attempts to remove the value that has the specified key.
diff --git a/BitFaster.Caching/IScopedAsyncCache.cs b/BitFaster.Caching/IScopedAsyncCache.cs
index 4906d441..32ba7150 100644
--- a/BitFaster.Caching/IScopedAsyncCache.cs
+++ b/BitFaster.Caching/IScopedAsyncCache.cs
@@ -53,7 +53,7 @@ public interface IScopedAsyncCache where V : IDisposable
/// The key of the element to add.
/// The factory function used to asynchronously generate a scoped value for the key.
/// A task that represents the asynchronous ScopedGetOrAdd operation.
- Task> ScopedGetOrAddAsync(K key, Func>> valueFactory);
+ ValueTask> ScopedGetOrAddAsync(K key, Func>> valueFactory);
///
/// Attempts to remove the value that has the specified key.
diff --git a/BitFaster.Caching/Lru/ClassicLru.cs b/BitFaster.Caching/Lru/ClassicLru.cs
index b787d37b..0bfc8f62 100644
--- a/BitFaster.Caching/Lru/ClassicLru.cs
+++ b/BitFaster.Caching/Lru/ClassicLru.cs
@@ -149,7 +149,7 @@ public V GetOrAdd(K key, Func valueFactory)
}
///
- public async Task GetOrAddAsync(K key, Func> valueFactory)
+ public async ValueTask GetOrAddAsync(K key, Func> valueFactory)
{
if (this.TryGet(key, out var value))
{
diff --git a/BitFaster.Caching/Lru/TemplateConcurrentLru.cs b/BitFaster.Caching/Lru/TemplateConcurrentLru.cs
index c332fa97..4759222b 100644
--- a/BitFaster.Caching/Lru/TemplateConcurrentLru.cs
+++ b/BitFaster.Caching/Lru/TemplateConcurrentLru.cs
@@ -184,7 +184,7 @@ public V GetOrAdd(K key, Func valueFactory)
}
///
- public async Task GetOrAddAsync(K key, Func> valueFactory)
+ public async ValueTask GetOrAddAsync(K key, Func> valueFactory)
{
while (true)
{
diff --git a/BitFaster.Caching/ScopedAsyncCache.cs b/BitFaster.Caching/ScopedAsyncCache.cs
index 97195fa1..f15dbe53 100644
--- a/BitFaster.Caching/ScopedAsyncCache.cs
+++ b/BitFaster.Caching/ScopedAsyncCache.cs
@@ -53,7 +53,7 @@ public void Clear()
}
///
- public async Task> ScopedGetOrAddAsync(K key, Func>> valueFactory)
+ public async ValueTask> ScopedGetOrAddAsync(K key, Func>> valueFactory)
{
int c = 0;
var spinwait = new SpinWait();
diff --git a/BitFaster.Caching/Synchronized/AsyncAtomicFactory.cs b/BitFaster.Caching/Synchronized/AsyncAtomicFactory.cs
index c9f5f289..0c33122f 100644
--- a/BitFaster.Caching/Synchronized/AsyncAtomicFactory.cs
+++ b/BitFaster.Caching/Synchronized/AsyncAtomicFactory.cs
@@ -26,7 +26,7 @@ public AsyncAtomicFactory(V value)
this.value = value;
}
- public async Task GetValueAsync(K key, Func> valueFactory)
+ public async ValueTask GetValueAsync(K key, Func> valueFactory)
{
if (initializer == null)
{
@@ -51,7 +51,7 @@ public V ValueIfCreated
}
}
- private async Task CreateValueAsync(K key, Func> valueFactory)
+ private async ValueTask CreateValueAsync(K key, Func> valueFactory)
{
var init = initializer;
@@ -70,7 +70,7 @@ private class Initializer
private bool isInitialized;
private Task valueTask;
- public async Task CreateValueAsync(K key, Func> valueFactory)
+ public async ValueTask CreateValueAsync(K key, Func> valueFactory)
{
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs
index 2ebd7589..467d7f6b 100644
--- a/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs
+++ b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs
@@ -40,7 +40,7 @@ public void Clear()
cache.Clear();
}
- public Task GetOrAddAsync(K key, Func> valueFactory)
+ public ValueTask GetOrAddAsync(K key, Func> valueFactory)
{
var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomicFactory());
return synchronized.GetValueAsync(key, valueFactory);
diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs
index 867f6e08..e84a74c3 100644
--- a/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs
+++ b/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs
@@ -46,11 +46,6 @@ public V GetOrAdd(K key, Func valueFactory)
return atomicFactory.GetValue(key, valueFactory);
}
- public Task GetOrAddAsync(K key, Func> valueFactory)
- {
- throw new NotImplementedException();
- }
-
public void Trim(int itemCount)
{
this.cache.Trim(itemCount);
diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs
index b07de119..9ce1ecc8 100644
--- a/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs
+++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs
@@ -41,7 +41,7 @@ public void Clear()
this.cache.Clear();
}
- public async Task> ScopedGetOrAddAsync(K key, Func>> valueFactory)
+ public async ValueTask> ScopedGetOrAddAsync(K key, Func>> valueFactory)
{
int c = 0;
var spinwait = new SpinWait();
diff --git a/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs b/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs
index c14d89d5..2355b30f 100644
--- a/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs
+++ b/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs
@@ -46,7 +46,7 @@ public bool TryCreateLifetime(out Lifetime lifetime)
return scope.TryCreateLifetime(out lifetime);
}
- public async Task<(bool success, Lifetime lifetime)> TryCreateLifetimeAsync(K key, Func>> valueFactory)
+ public async ValueTask<(bool success, Lifetime lifetime)> TryCreateLifetimeAsync(K key, Func>> valueFactory)
{
// if disposed, return
if (scope?.IsDisposed ?? false)
@@ -64,7 +64,7 @@ public bool TryCreateLifetime(out Lifetime lifetime)
return (res, lifetime);
}
- private async Task InitializeScopeAsync(K key, Func>> valueFactory)
+ private async ValueTask InitializeScopeAsync(K key, Func>> valueFactory)
{
var init = initializer;
@@ -97,7 +97,7 @@ private class Initializer
private bool isDisposeRequested;
private Task> task;
- public async Task> CreateScopeAsync(K key, Func>> valueFactory)
+ public async ValueTask> CreateScopeAsync(K key, Func>> valueFactory)
{
var tcs = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously);