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);