Skip to content

Add alternate key support to ConcurrentLfu#740

Merged
bitfaster merged 6 commits intomainfrom
copilot/implement-ialternatelookup-concurrentlfucore
Mar 30, 2026
Merged

Add alternate key support to ConcurrentLfu#740
bitfaster merged 6 commits intomainfrom
copilot/implement-ialternatelookup-concurrentlfucore

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 29, 2026

This adds a net9-only API for alternate lookups on ConcurrentLfu and ConcurrentTLfu, following the same template established in PR #739 for ConcurrentLru.

Alternate lookup API

  • Add GetAlternateLookup<TAlternateKey>() and TryGetAlternateLookup<TAlternateKey>() to ConcurrentLfuCore.
  • Expose on ConcurrentLfu<K,V> and ConcurrentTLfu<K,V> wrapper classes.
  • Implement AlternateLookup<TAlternateKey> inner struct with:
    • TryGet — performs alternate-key dictionary lookup with read buffer tracking, drain scheduling, and expiry handling
    • TryRemove — removes by alternate key, marks node as removed, and triggers write buffer
    • TryUpdate — locks node and updates value with write buffer scheduling
    • AddOrUpdate — update-or-add loop with cached primary key materialization
    • GetOrAdd — get-or-add loop converting alternate key via IAlternateEqualityComparer.Create()
    • GetOrAdd<TArg> — same with factory argument

ConcurrentDictionary alternate lookup integration

  • Use ConcurrentDictionary<K,N>.GetAlternateLookup<TAlternateKey>() on net9 for alternate-key operations without materializing the primary key on hits.
  • Cache the converted primary key across AddOrUpdate retries to avoid repeated alternate-key materialization under contention.
  • Preserve existing cache semantics for metrics, eviction, disposal, and write buffering.

Code deduplication

  • Extract shared GetOrDiscard private method with [MethodImpl(MethodImplOptions.AggressiveInlining)] for TryGet node processing, used by both TryGetImpl and AlternateLookup.TryGet.
  • Extract shared TryUpdateValue private method with [MethodImpl(MethodImplOptions.AggressiveInlining)] for value mutation, used by both TryUpdate and AlternateLookup.TryUpdate.

Compatibility guardrails

  • Fail fast for unsupported alternate key types with Throw.IncompatibleComparer().
  • Feature is behind NET9_0_OR_GREATER so the public surface remains unchanged on earlier targets.

Tests

  • Unit tests (ConcurrentLfuAlternateLookupTests.cs): 6 focused tests covering compatible/incompatible comparer, TryGet, TryRemove, TryUpdate, AddOrUpdate, and GetOrAdd.
  • ConcurrentTLfu tests (ConcurrentTLfuTests.cs): 4 tests covering GetAlternateLookup and TryGetAlternateLookup for both compatible and incompatible comparers.
  • Soak tests (ConcurrentLfuSoakTests.cs): 3 concurrent soak tests ×10 iterations each for GetOrAdd, GetOrAdd, and TryRemove+AddOrUpdate with ReadOnlySpan<char> / StringComparer.Ordinal.

Example

var cache = new ConcurrentLfu<string, string>(1, 20, scheduler, StringComparer.Ordinal);
var alt = cache.GetAlternateLookup<ReadOnlySpan<char>>();

string value = alt.GetOrAdd("42".AsSpan(), key => $"value-{key.ToString()}");
alt.AddOrUpdate("42".AsSpan(), "updated");
bool updated = alt.TryUpdate("42".AsSpan(), "updated-again");
bool found = alt.TryGet("42".AsSpan(), out var existing);
bool removed = alt.TryRemove("42".AsSpan(), out var actualKey, out var removedValue);

⌨️ Start Copilot coding agent tasks without leaving your editor — available in VS Code, Visual Studio, JetBrains IDEs and Eclipse.

{
if (this.Alternate.TryGetValue(key, out var node))
{
if (!this.Lfu.policy.IsExpired(node))
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot extract this into a private class level method to avoid duplicating the code. Inline the new method to make it fast.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted into TryUpdateValue with [MethodImpl(MethodImplOptions.AggressiveInlining)], shared by both TryUpdate and AlternateLookup.TryUpdate. Addressed in 289e1a8.

{
if (this.Alternate.TryGetValue(key, out var node))
{
lock (node)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot extract this into a private class level method to avoid code duplication. Inline the new method to make it fast.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted into GetOrDiscard with [MethodImpl(MethodImplOptions.AggressiveInlining)], shared by both TryGetImpl and AlternateLookup.TryGet — following the same pattern as ConcurrentLruCore. The TryUpdateValue extraction from the previous commit already addressed the TryUpdate duplication at this line. Addressed in 0748af8.

@coveralls
Copy link
Copy Markdown

coveralls commented Mar 30, 2026

Coverage Status

coverage: 99.158% (+0.07%) from 99.091%
when pulling 88a9b19 on copilot/implement-ialternatelookup-concurrentlfucore
into 1359d7e on main.

@bitfaster
Copy link
Copy Markdown
Owner

Bench is always quite variable for LFU, any change here appears to be noise.

BenchmarkDotNet v0.14.0, Windows 11 (10.0.26200.7985)
Intel Xeon W-2133 CPU 3.60GHz, 1 CPU, 12 logical and 6 physical cores
  [Host] : .NET Framework 4.8.1 (4.8.9221.0), X64 RyuJIT VectorSize=256
  net9.0 : .NET 9.0.14 (9.0.1426.11910), X64 RyuJIT AVX-512F+CD+BW+DQ+VL

Job=net9.0  Runtime=.NET 9.0  Alloc Ratio=NA

Before:

Method Mean Error StdDev Ratio Allocated
ConcurrentDictionary 3.868 ns 0.0363 ns 0.0322 ns 1.00 -
FastConcurrentLru 3.816 ns 0.0237 ns 0.0222 ns 0.99 -
ConcurrentLru 13.335 ns 0.0455 ns 0.0426 ns 3.45 -
AtomicFastLru 5.545 ns 0.0388 ns 0.0324 ns 1.43 -
FastConcurrentTLru 5.523 ns 0.0431 ns 0.0403 ns 1.43 -
FastConcLruAfterAccess 5.909 ns 0.0296 ns 0.0262 ns 1.53 -
FastConcLruAfter 7.002 ns 0.0592 ns 0.0494 ns 1.81 -
ConcurrentTLru 9.295 ns 0.0301 ns 0.0267 ns 2.40 -
ConcurrentLfu 12.078 ns 0.2704 ns 0.4048 ns 3.12 -
ClassicLru 35.467 ns 0.1175 ns 0.1041 ns 9.17 -
RuntimeMemoryCacheGet 104.680 ns 0.3425 ns 0.3036 ns 27.07 32 B
ExtensionsMemoryCacheGet 38.265 ns 0.3608 ns 0.3013 ns 9.89 24 B

After

Method Mean Error StdDev Ratio Allocated
ConcurrentDictionary 3.941 ns 0.0516 ns 0.0458 ns 1.00 -
FastConcurrentLru 3.824 ns 0.0175 ns 0.0435 ns 0.97 -
ConcurrentLru 7.509 ns 0.0184 ns 0.0143 ns 1.91 -
AtomicFastLru 5.543 ns 0.0311 ns 0.0275 ns 1.41 -
FastConcurrentTLru 5.504 ns 0.0290 ns 0.0257 ns 1.40 -
FastConcLruAfterAccess 5.864 ns 0.0229 ns 0.0203 ns 1.49 -
FastConcLruAfter 7.647 ns 0.0536 ns 0.0447 ns 1.94 -
ConcurrentTLru 9.326 ns 0.0233 ns 0.0206 ns 2.37 -
ConcurrentLfu 12.306 ns 0.2400 ns 0.2128 ns 3.12 -
ClassicLru 35.874 ns 0.3185 ns 0.2979 ns 9.10 -
RuntimeMemoryCacheGet 104.800 ns 0.5832 ns 0.5170 ns 26.60 32 B
ExtensionsMemoryCacheGet 39.099 ns 0.6001 ns 0.5319 ns 9.92 24 B
Method Mean Error StdDev Ratio Allocated
ConcurrentDictionary 3.890 ns 0.0107 ns 0.0083 ns 1.00 -
FastConcurrentLru 3.784 ns 0.0239 ns 0.0212 ns 0.97 -
ConcurrentLru 7.463 ns 0.0386 ns 0.0343 ns 1.92 -
AtomicFastLru 5.669 ns 0.0556 ns 0.0520 ns 1.46 -
FastConcurrentTLru 5.492 ns 0.0232 ns 0.0194 ns 1.41 -
FastConcLruAfterAccess 5.870 ns 0.0249 ns 0.0233 ns 1.51 -
FastConcLruAfter 6.976 ns 0.0254 ns 0.0226 ns 1.79 -
ConcurrentTLru 9.287 ns 0.0242 ns 0.0215 ns 2.39 -
ConcurrentLfu 13.551 ns 0.3046 ns 0.7180 ns 3.48 -
ClassicLru 35.399 ns 0.1072 ns 0.0951 ns 9.10 -
RuntimeMemoryCacheGet 108.048 ns 1.7589 ns 1.6452 ns 27.78 32 B
ExtensionsMemoryCacheGet 40.694 ns 0.8333 ns 1.3691 ns 10.46 24 B
Method Mean Error StdDev Ratio Allocated
ConcurrentDictionary 4.706 ns 0.0777 ns 0.0689 ns 1.00 -
FastConcurrentLru 3.790 ns 0.0194 ns 0.0162 ns 0.81 -
ConcurrentLru 7.462 ns 0.0385 ns 0.0342 ns 1.59 -
AtomicFastLru 5.499 ns 0.0107 ns 0.0089 ns 1.17 -
FastConcurrentTLru 5.503 ns 0.0480 ns 0.0425 ns 1.17 -
FastConcLruAfterAccess 5.856 ns 0.0285 ns 0.0253 ns 1.24 -
FastConcLruAfter 6.948 ns 0.0198 ns 0.0176 ns 1.48 -
ConcurrentTLru 9.330 ns 0.0757 ns 0.0708 ns 1.98 -
ConcurrentLfu 11.538 ns 0.0939 ns 0.0833 ns 2.45 -
ClassicLru 35.393 ns 0.1305 ns 0.1090 ns 7.52 -
RuntimeMemoryCacheGet 106.362 ns 0.3868 ns 0.3429 ns 22.61 32 B
ExtensionsMemoryCacheGet 38.200 ns 0.2164 ns 0.2025 ns 8.12 24 B

@bitfaster bitfaster marked this pull request as ready for review March 30, 2026 01:04
{
string key = (i + 1).ToString();
alternate.TryRemove(key.AsSpan(), out _, out _);
alternate.GetOrAdd(key.AsSpan(), static keySpan => keySpan.ToString());
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot change this line to use AddOrUpdate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to alternate.AddOrUpdate(key.AsSpan(), key). Addressed in be22f15.

…AddOrUpdate scenario

Agent-Logs-Url: https://github.com/bitfaster/BitFaster.Caching/sessions/de1c6dd4-b745-491a-940f-e2e7f24ee099

Co-authored-by: bitfaster <12851828+bitfaster@users.noreply.github.com>
Copilot AI requested a review from bitfaster March 30, 2026 01:10
@bitfaster
Copy link
Copy Markdown
Owner

@copilot Add tests to ConcurrentTLfuTests for GetAlternateLookup and TryGetAlternateLookup.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 30, 2026

@copilot Add tests to ConcurrentTLfuTests for GetAlternateLookup and TryGetAlternateLookup.

Added 4 tests to ConcurrentTLfuTests: GetAlternateLookupReturnsLookupForCompatibleComparer, TryGetAlternateLookupReturnsTrueForCompatibleComparer, GetAlternateLookupThrowsForIncompatibleComparer, and TryGetAlternateLookupReturnsFalseForIncompatibleComparer. Addressed in 7b340b6.

@bitfaster bitfaster merged commit 94cf12f into main Mar 30, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature request] Add alternate lookup support

3 participants