-
Notifications
You must be signed in to change notification settings - Fork 673
Add SeqlockCache #10415
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add SeqlockCache #10415
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
47de013
Add SeqlockCache and use for Accounts and Values
benaadams 761ec61
Add tests
benaadams 3f4eab1
Spelling
benaadams dd1191d
By in
benaadams 7483289
Feedback
benaadams 9db1b6f
Apply suggestions from code review
benaadams 0f52aa4
Apply suggestions from code review
benaadams 9c9d3aa
Fix
benaadams afce1ad
Feedback
benaadams 6d9ab4e
Change to 2 way cache
benaadams f7d05a0
2-way skew-associative
benaadams 556735f
Add prefetch for way 1 and hash-bit alternating eviction
benaadams 0974114
Add Volatile.Read/Write for _hashes in BlockhashProvider
benaadams 3fb6a49
Convert NodeStorageCache and PreBlockCaches.RlpCache to SeqlockCache
benaadams aa7e33e
Inline Address comparison in StorageCell.Equals for SeqlockCache hot …
benaadams ba27b2e
Outline cold paths in SeqlockCache and fix IsPersisted delegate alloc…
benaadams File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
345 changes: 345 additions & 0 deletions
345
src/Nethermind/Nethermind.Benchmark/Core/SeqlockCacheBenchmarks.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,345 @@ | ||
| // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
|
|
||
| #nullable enable | ||
|
|
||
| using System; | ||
| using System.Collections.Concurrent; | ||
| using BenchmarkDotNet.Attributes; | ||
| using Nethermind.Core; | ||
| using Nethermind.Core.Collections; | ||
| using Nethermind.Int256; | ||
|
|
||
| namespace Nethermind.Benchmarks.Core; | ||
|
|
||
| [MemoryDiagnoser] | ||
| [DisassemblyDiagnoser(maxDepth: 3)] | ||
| public class SeqlockCacheBenchmarks | ||
| { | ||
| private SeqlockCache<StorageCell, byte[]> _seqlockCache = null!; | ||
| private ConcurrentDictionary<StorageCell, byte[]> _concurrentDict = null!; | ||
|
|
||
| private StorageCell[] _keys = null!; | ||
| private byte[][] _values = null!; | ||
| private StorageCell _missKey; | ||
|
|
||
| [Params(1000)] | ||
| public int KeyCount { get; set; } | ||
|
|
||
| [GlobalSetup] | ||
| public void Setup() | ||
| { | ||
| _seqlockCache = new SeqlockCache<StorageCell, byte[]>(); | ||
| _concurrentDict = new ConcurrentDictionary<StorageCell, byte[]>(); | ||
|
|
||
| _keys = new StorageCell[KeyCount]; | ||
| _values = new byte[KeyCount][]; | ||
|
|
||
| var random = new Random(42); | ||
| for (int i = 0; i < KeyCount; i++) | ||
| { | ||
| var addressBytes = new byte[20]; | ||
| random.NextBytes(addressBytes); | ||
| var address = new Address(addressBytes); | ||
| var index = new UInt256((ulong)i); | ||
|
|
||
| _keys[i] = new StorageCell(address, index); | ||
| _values[i] = new byte[32]; | ||
| random.NextBytes(_values[i]); | ||
|
|
||
| // Pre-populate both caches | ||
| _seqlockCache.Set(in _keys[i], _values[i]); | ||
| _concurrentDict[_keys[i]] = _values[i]; | ||
| } | ||
|
|
||
| // Create a key that won't be in the cache | ||
| var missAddressBytes = new byte[20]; | ||
| random.NextBytes(missAddressBytes); | ||
| _missKey = new StorageCell(new Address(missAddressBytes), UInt256.MaxValue); | ||
| } | ||
|
|
||
| // ==================== TryGetValue (Hit) ==================== | ||
|
|
||
| [Benchmark(Baseline = true)] | ||
| public bool SeqlockCache_TryGetValue_Hit() | ||
| { | ||
| return _seqlockCache.TryGetValue(in _keys[500], out _); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public bool ConcurrentDict_TryGetValue_Hit() | ||
| { | ||
| return _concurrentDict.TryGetValue(_keys[500], out _); | ||
| } | ||
|
|
||
| // ==================== TryGetValue (Miss) ==================== | ||
|
|
||
| [Benchmark] | ||
| public bool SeqlockCache_TryGetValue_Miss() | ||
| { | ||
| return _seqlockCache.TryGetValue(in _missKey, out _); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public bool ConcurrentDict_TryGetValue_Miss() | ||
| { | ||
| return _concurrentDict.TryGetValue(_missKey, out _); | ||
| } | ||
|
|
||
| // ==================== Set (Existing Key) ==================== | ||
|
|
||
| [Benchmark] | ||
| public void SeqlockCache_Set_Existing() | ||
| { | ||
| _seqlockCache.Set(in _keys[500], _values[500]); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public void ConcurrentDict_Set_Existing() | ||
| { | ||
| _concurrentDict[_keys[500]] = _values[500]; | ||
| } | ||
|
|
||
| // ==================== GetOrAdd (Hit) ==================== | ||
|
|
||
| [Benchmark] | ||
| public byte[]? SeqlockCache_GetOrAdd_Hit() | ||
| { | ||
| return _seqlockCache.GetOrAdd(in _keys[500], static (in StorageCell _) => new byte[32]); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public byte[] ConcurrentDict_GetOrAdd_Hit() | ||
| { | ||
| return _concurrentDict.GetOrAdd(_keys[500], static _ => new byte[32]); | ||
| } | ||
|
|
||
| // ==================== GetOrAdd (Miss - measures factory overhead) ==================== | ||
|
|
||
| private int _missCounter; | ||
|
|
||
| [Benchmark] | ||
| public byte[]? SeqlockCache_GetOrAdd_Miss() | ||
| { | ||
| // Use incrementing key to always miss | ||
| var key = new StorageCell(_keys[0].Address, new UInt256((ulong)(KeyCount + _missCounter++))); | ||
| return _seqlockCache.GetOrAdd(in key, static (in StorageCell _) => new byte[32]); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public byte[] ConcurrentDict_GetOrAdd_Miss() | ||
| { | ||
| var key = new StorageCell(_keys[0].Address, new UInt256((ulong)(KeyCount + _missCounter++))); | ||
| return _concurrentDict.GetOrAdd(key, static _ => new byte[32]); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Benchmark comparing read-heavy workloads (90% reads, 10% writes) | ||
| /// </summary> | ||
| [MemoryDiagnoser] | ||
| public class SeqlockCacheMixedWorkloadBenchmarks | ||
| { | ||
| private SeqlockCache<StorageCell, byte[]> _seqlockCache = null!; | ||
| private ConcurrentDictionary<StorageCell, byte[]> _concurrentDict = null!; | ||
|
|
||
| private StorageCell[] _keys = null!; | ||
| private byte[][] _values = null!; | ||
|
|
||
| private const int KeyCount = 10000; | ||
| private const int OperationsPerInvoke = 1000; | ||
|
|
||
| [GlobalSetup] | ||
| public void Setup() | ||
| { | ||
| _seqlockCache = new SeqlockCache<StorageCell, byte[]>(); | ||
| _concurrentDict = new ConcurrentDictionary<StorageCell, byte[]>(); | ||
|
|
||
| _keys = new StorageCell[KeyCount]; | ||
| _values = new byte[KeyCount][]; | ||
|
|
||
| var random = new Random(42); | ||
| for (int i = 0; i < KeyCount; i++) | ||
| { | ||
| var addressBytes = new byte[20]; | ||
| random.NextBytes(addressBytes); | ||
| var address = new Address(addressBytes); | ||
| var index = new UInt256((ulong)i); | ||
|
|
||
| _keys[i] = new StorageCell(address, index); | ||
| _values[i] = new byte[32]; | ||
| random.NextBytes(_values[i]); | ||
|
|
||
| // Pre-populate both caches | ||
| _seqlockCache.Set(in _keys[i], _values[i]); | ||
| _concurrentDict[_keys[i]] = _values[i]; | ||
| } | ||
| } | ||
|
|
||
| [Benchmark(Baseline = true, OperationsPerInvoke = OperationsPerInvoke)] | ||
| public int SeqlockCache_MixedWorkload_90Read_10Write() | ||
| { | ||
| int hits = 0; | ||
| for (int i = 0; i < OperationsPerInvoke; i++) | ||
| { | ||
| int keyIndex = i % KeyCount; | ||
| if (i % 10 == 0) | ||
| { | ||
| // 10% writes | ||
| _seqlockCache.Set(in _keys[keyIndex], _values[keyIndex]); | ||
| } | ||
| else | ||
| { | ||
| // 90% reads | ||
| if (_seqlockCache.TryGetValue(in _keys[keyIndex], out _)) | ||
| hits++; | ||
| } | ||
| } | ||
| return hits; | ||
| } | ||
|
|
||
| [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] | ||
| public int ConcurrentDict_MixedWorkload_90Read_10Write() | ||
| { | ||
| int hits = 0; | ||
| for (int i = 0; i < OperationsPerInvoke; i++) | ||
| { | ||
| int keyIndex = i % KeyCount; | ||
| if (i % 10 == 0) | ||
| { | ||
| // 10% writes | ||
| _concurrentDict[_keys[keyIndex]] = _values[keyIndex]; | ||
| } | ||
| else | ||
| { | ||
| // 90% reads | ||
| if (_concurrentDict.TryGetValue(_keys[keyIndex], out _)) | ||
| hits++; | ||
| } | ||
| } | ||
| return hits; | ||
| } | ||
|
|
||
| [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] | ||
| public int SeqlockCache_ReadOnly() | ||
| { | ||
| int hits = 0; | ||
| for (int i = 0; i < OperationsPerInvoke; i++) | ||
| { | ||
| int keyIndex = i % KeyCount; | ||
| if (_seqlockCache.TryGetValue(in _keys[keyIndex], out _)) | ||
| hits++; | ||
| } | ||
| return hits; | ||
| } | ||
|
|
||
| [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] | ||
| public int ConcurrentDict_ReadOnly() | ||
| { | ||
| int hits = 0; | ||
| for (int i = 0; i < OperationsPerInvoke; i++) | ||
| { | ||
| int keyIndex = i % KeyCount; | ||
| if (_concurrentDict.TryGetValue(_keys[keyIndex], out _)) | ||
| hits++; | ||
| } | ||
| return hits; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Benchmark measuring effective hit rate after populating with N keys. | ||
| /// This directly measures the impact of collision rate. | ||
| /// </summary> | ||
| public class SeqlockCacheHitRateBenchmarks | ||
| { | ||
| private SeqlockCache<StorageCell, byte[]> _seqlockCache = null!; | ||
| private StorageCell[] _keys = null!; | ||
| private byte[][] _values = null!; | ||
|
|
||
| [Params(1000, 5000, 10000, 20000)] | ||
| public int KeyCount { get; set; } | ||
|
|
||
| [GlobalSetup] | ||
| public void Setup() | ||
| { | ||
| _seqlockCache = new SeqlockCache<StorageCell, byte[]>(); | ||
| _keys = new StorageCell[KeyCount]; | ||
| _values = new byte[KeyCount][]; | ||
|
|
||
| var random = new Random(42); | ||
| for (int i = 0; i < KeyCount; i++) | ||
| { | ||
| var addressBytes = new byte[20]; | ||
| random.NextBytes(addressBytes); | ||
| _keys[i] = new StorageCell(new Address(addressBytes), new UInt256((ulong)i)); | ||
| _values[i] = new byte[32]; | ||
| random.NextBytes(_values[i]); | ||
| _seqlockCache.Set(in _keys[i], _values[i]); | ||
| } | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public double MeasureHitRate() | ||
| { | ||
| int hits = 0; | ||
| for (int i = 0; i < KeyCount; i++) | ||
| { | ||
| if (_seqlockCache.TryGetValue(in _keys[i], out byte[]? val) && ReferenceEquals(val, _values[i])) | ||
| hits++; | ||
| } | ||
| return (double)hits / KeyCount * 100; | ||
| } | ||
| } | ||
|
|
||
| [MemoryDiagnoser] | ||
| public class SeqlockCacheCallSiteBenchmarks | ||
| { | ||
| private SeqlockCache<StorageCell, byte[]> _cache = null!; | ||
| private SeqlockCache<StorageCell, byte[]>.ValueFactory _cachedFactory = null!; | ||
| private StorageCell _key; | ||
| private byte[] _value = null!; | ||
|
|
||
| [GlobalSetup] | ||
| public void Setup() | ||
| { | ||
| _cache = new SeqlockCache<StorageCell, byte[]>(); | ||
|
|
||
| byte[] addressBytes = new byte[20]; | ||
| new Random(123).NextBytes(addressBytes); | ||
| _key = new StorageCell(new Address(addressBytes), UInt256.One); | ||
| _value = new byte[32]; | ||
|
|
||
| _cache.Set(in _key, _value); | ||
| _cachedFactory = LoadFromBackingStore; | ||
| } | ||
|
|
||
| [Benchmark(Baseline = true)] | ||
| public byte[]? GetOrAdd_Hit_PerCallMethodGroup() | ||
| { | ||
| return _cache.GetOrAdd(in _key, LoadFromBackingStore); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public byte[]? GetOrAdd_Hit_CachedDelegate() | ||
| { | ||
| return _cache.GetOrAdd(in _key, _cachedFactory); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public bool TryGetValue_WithIn() | ||
| { | ||
| return _cache.TryGetValue(in _key, out _); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public bool TryGetValue_WithoutIn() | ||
| { | ||
| return _cache.TryGetValue(_key, out _); | ||
| } | ||
|
|
||
| private byte[] LoadFromBackingStore(in StorageCell _) | ||
| { | ||
| return _value; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.