Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions BitFaster.Caching.Benchmarks/LockBench.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@

using System.Threading;
using Benchly;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace BitFaster.Caching.Benchmarks
{
[SimpleJob(RuntimeMoniker.Net90)]
[MemoryDiagnoser(displayGenColumns: false)]
[HideColumns("Job", "Median", "RatioSD", "Alloc Ratio")]
[ColumnChart(Title ="Try enter ({JOB})")]
public class LockBench
{
private int _value;
private readonly object monitorLock = new object();
#if NET9_0_OR_GREATER
private readonly Lock threadingLock = new Lock();
#endif

[Benchmark(Baseline = true)]
public void UseMonitor()
{
bool lockTaken = false;
Monitor.TryEnter(monitorLock, ref lockTaken);

if (lockTaken)
{
try
{
_value++;
}
finally
{
if (lockTaken)
{
Monitor.Exit(monitorLock);
}
}
}
}

[Benchmark()]
public void UseLock()
{
#if NET9_0_OR_GREATER
if (threadingLock.TryEnter())
{
try
{
_value++;
}
finally
{
threadingLock.Exit();
}
}
#endif
}
}
}
4 changes: 2 additions & 2 deletions BitFaster.Caching/BitFaster.Caching.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netcoreapp3.1;net6.0</TargetFrameworks>
<LangVersion>11.0</LangVersion>
<TargetFrameworks>netstandard2.0;netcoreapp3.1;net6.0;net9.0</TargetFrameworks>
<LangVersion>13.0</LangVersion>
<Authors>Alex Peck</Authors>
<Company />
<Product>BitFaster.Caching</Product>
Expand Down
33 changes: 22 additions & 11 deletions BitFaster.Caching/Lfu/ConcurrentLfuCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,12 @@ internal struct ConcurrentLfuCore<K, V, N, P> : IBoundedPolicy
private readonly LfuCapacityPartition capacity;

internal readonly DrainStatus drainStatus = new();
private readonly object maintenanceLock = new();

#if NET9_0_OR_GREATER
private readonly Lock maintenanceLock = new();
#else
private readonly object maintenanceLock = new();
#endif
private readonly IScheduler scheduler;
private readonly Action drainBuffers;

Expand Down Expand Up @@ -481,12 +485,15 @@ private void TryScheduleDrain()
return;
}

#if NET9_0_OR_GREATER
if (maintenanceLock.TryEnter())
#else
bool lockTaken = false;
try
Monitor.TryEnter(maintenanceLock, ref lockTaken);
if (lockTaken)
#endif
{
Monitor.TryEnter(maintenanceLock, ref lockTaken);

if (lockTaken)
try
{
int status = this.drainStatus.NonVolatileRead();

Expand All @@ -498,12 +505,16 @@ private void TryScheduleDrain()
this.drainStatus.VolatileWrite(DrainStatus.ProcessingToIdle);
scheduler.Run(this.drainBuffers);
}
}
finally
{
if (lockTaken)
{
Monitor.Exit(maintenanceLock);
finally
{
#if NET9_0_OR_GREATER
maintenanceLock.Exit();
#else
if (lockTaken)
{
Monitor.Exit(maintenanceLock);
}
#endif
}
}
}
Expand Down
145 changes: 145 additions & 0 deletions BitFaster.Caching/Lru/ConcurrentLruCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,151 @@ private static Optional<ICacheEvents<K, V>> CreateEvents(ConcurrentLruCore<K, V,
return new(new Proxy(lru));
}

#if NET9_0_OR_GREATER

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsCompatibleKey<TAlternateKey>(ConcurrentDictionary<K, I> d)
where TAlternateKey : notnull, allows ref struct
{
return d.Comparer is IAlternateEqualityComparer<TAlternateKey, K>;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static IAlternateEqualityComparer<TAlternateKey, K> GetAlternateComparer<TAlternateKey>(ConcurrentDictionary<K, I> d)
where TAlternateKey : notnull, allows ref struct
{
Debug.Assert(IsCompatibleKey<TAlternateKey>(d));
return Unsafe.As<IAlternateEqualityComparer<TAlternateKey, K>>(d.Comparer!);
}

public IAlternateCache<TAlternateKey, K, V> GetAlternateCache<TAlternateKey>() where TAlternateKey : notnull, allows ref struct
{
if (!IsCompatibleKey<TAlternateKey>(this.dictionary))
{
Throw.IncompatibleComparer();
}

return new AlternateCache<TAlternateKey>(this);
}

public bool TryGetAlternateCache<TAlternateKey>([MaybeNullWhen(false)] out IAlternateCache<TAlternateKey, K, V> lookup) where TAlternateKey : notnull, allows ref struct
{
if (IsCompatibleKey<TAlternateKey>(this.dictionary))
{
lookup = new AlternateCache<TAlternateKey>(this);
return true;
}

lookup = default;
return false;
}

// Rough idea of alternate cache interface
// Note: we need a sync and async variant, plumbed into ICache and IAsyncCache.
public interface IAlternateCache<TAlternateKey, K, V> where TAlternateKey : notnull, allows ref struct
{
bool TryGet(TAlternateKey key, [MaybeNullWhen(false)] out V value);

bool TryRemove(TAlternateKey key, [MaybeNullWhen(false)] out K actualKey, [MaybeNullWhen(false)] out V value);

V GetOrAdd(TAlternateKey altKey, Func<TAlternateKey, V> valueFactory);

V GetOrAdd<TArg>(TAlternateKey altKey, Func<TAlternateKey, TArg, V> valueFactory, TArg factoryArgument);

// TryUpdate
// AddOrUpdate
}

internal readonly struct AlternateCache<TAlternateKey> : IAlternateCache<TAlternateKey, K, V> where TAlternateKey : notnull, allows ref struct
{
/// <summary>Initialize the instance. The dictionary must have already been verified to have a compatible comparer.</summary>
internal AlternateCache(ConcurrentLruCore<K, V, I, P, T> lru)
{
Debug.Assert(lru is not null);
Debug.Assert(IsCompatibleKey<TAlternateKey>(lru.dictionary));
Lru = lru;
}

internal ConcurrentLruCore<K, V, I, P, T> Lru { get; }

public bool TryGet(TAlternateKey key, [MaybeNullWhen(false)] out V value)
{
var alternate = this.Lru.dictionary.GetAlternateLookup<TAlternateKey>();

if (alternate.TryGetValue(key, out var item))
{
return Lru.GetOrDiscard(item, out value);
}

value = default;
Lru.telemetryPolicy.IncrementMiss();
return false;
}

public bool TryRemove(TAlternateKey key, [MaybeNullWhen(false)] out K actualKey, [MaybeNullWhen(false)] out V value)
{
var alternate = this.Lru.dictionary.GetAlternateLookup<TAlternateKey>();

if (alternate.TryGetValue(key, out var item))
{
Lru.OnRemove(item.Key, item, ItemRemovedReason.Removed);
actualKey = item.Key;
value = item.Value;
return true;
}

actualKey = default;
value = default;
return false;
}

public V GetOrAdd(TAlternateKey altKey, Func<TAlternateKey, V> valueFactory)
{
var alternate = this.Lru.dictionary.GetAlternateLookup<TAlternateKey>();

while (true)
{
if (alternate.TryGetValue(altKey, out var item))
{
return item.Value;
}

// We cannot avoid allocating the key since it is required for item policy etc. Thus fall back to Lru for add.
// The value factory may be called concurrently for the same key, but the first write to the dictionary wins.
K key = GetAlternateComparer<TAlternateKey>(this.Lru.dictionary).Create(altKey);
V value = valueFactory(altKey);
if (Lru.TryAdd(key, value))
{
return value;
}
}
}

public V GetOrAdd<TArg>(TAlternateKey altKey, Func<TAlternateKey, TArg, V> valueFactory, TArg factoryArgument)
{
var alternate = this.Lru.dictionary.GetAlternateLookup<TAlternateKey>();

while (true)
{
if (alternate.TryGetValue(altKey, out var item))
{
return item.Value;
}

// We cannot avoid allocating the key since it is required for item policy etc. Thus fall back to Lru for add.
// The value factory may be called concurrently for the same key, but the first write to the dictionary wins.
K key = GetAlternateComparer<TAlternateKey>(this.Lru.dictionary).Create(altKey);
V value = valueFactory(altKey, factoryArgument);
if (Lru.TryAdd(key, value))
{
return value;
}
}
}
}

#endif

// To get JIT optimizations, policies must be structs.
// If the structs are returned directly via properties, they will be copied. Since
// telemetryPolicy is a mutable struct, copy is bad. One workaround is to store the
Expand Down
3 changes: 3 additions & 0 deletions BitFaster.Caching/Throw.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ internal static class Throw
[DoesNotReturn]
public static void Disposed<T>() => throw CreateObjectDisposedException<T>();

[DoesNotReturn]
public static void IncompatibleComparer() => throw new InvalidOperationException("Incompatible comparer");

[MethodImpl(MethodImplOptions.NoInlining)]
private static ArgumentNullException CreateArgumentNullException(ExceptionArgument arg) => new ArgumentNullException(GetArgumentString(arg));

Expand Down