Skip to content

Commit

Permalink
Intra-block cache (#7039)
Browse files Browse the repository at this point in the history
  • Loading branch information
benaadams authored May 20, 2024
1 parent f0e7fdf commit 091d255
Show file tree
Hide file tree
Showing 9 changed files with 347 additions and 31 deletions.
163 changes: 163 additions & 0 deletions src/Nethermind/Nethermind.Core/Caching/LruCacheNonConcurrent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Nethermind.Core.Extensions;

namespace Nethermind.Core.Caching
{
public sealed class LruCacheNonConcurrent<TKey, TValue> where TKey : notnull
{
private readonly int _maxCapacity;
private readonly Dictionary<TKey, LinkedListNode<LruCacheItem>> _cacheMap;
private readonly string _name;
private LinkedListNode<LruCacheItem>? _leastRecentlyUsed;

public LruCacheNonConcurrent(int maxCapacity, int startCapacity, string name)
{
ArgumentOutOfRangeException.ThrowIfLessThan(maxCapacity, 1);

_name = name;
_maxCapacity = maxCapacity;
_cacheMap = typeof(TKey) == typeof(byte[])
? new Dictionary<TKey, LinkedListNode<LruCacheItem>>((IEqualityComparer<TKey>)Bytes.EqualityComparer)
: new Dictionary<TKey, LinkedListNode<LruCacheItem>>(startCapacity); // do not initialize it at the full capacity
}

public LruCacheNonConcurrent(int maxCapacity, string name)
: this(maxCapacity, 0, name)
{
}

public void Clear()
{
_leastRecentlyUsed = null;
_cacheMap.Clear();
}

public TValue Get(TKey key)
{
if (_cacheMap.TryGetValue(key, out LinkedListNode<LruCacheItem>? node))
{
TValue value = node.Value.Value;
LinkedListNode<LruCacheItem>.MoveToMostRecent(ref _leastRecentlyUsed, node);
return value;
}

#pragma warning disable 8603
// fixed C# 9
return default;
#pragma warning restore 8603
}

public bool TryGet(TKey key, out TValue value)
{
if (_cacheMap.TryGetValue(key, out LinkedListNode<LruCacheItem>? node))
{
value = node.Value.Value;
LinkedListNode<LruCacheItem>.MoveToMostRecent(ref _leastRecentlyUsed, node);
return true;
}

#pragma warning disable 8601
// fixed C# 9
value = default;
#pragma warning restore 8601
return false;
}

public bool Set(TKey key, TValue val)
{
if (val is null)
{
return DeleteNoLock(key);
}

if (_cacheMap.TryGetValue(key, out LinkedListNode<LruCacheItem>? node))
{
node.Value.Value = val;
LinkedListNode<LruCacheItem>.MoveToMostRecent(ref _leastRecentlyUsed, node);
return false;
}

if (_cacheMap.Count >= _maxCapacity)
{
Replace(key, val);
}
else
{
LinkedListNode<LruCacheItem> newNode = new(new(key, val));
LinkedListNode<LruCacheItem>.AddMostRecent(ref _leastRecentlyUsed, newNode);
_cacheMap.Add(key, newNode);
}

return true;
}

public bool DeleteNoLock(TKey key)
{
if (_cacheMap.TryGetValue(key, out LinkedListNode<LruCacheItem>? node))
{
LinkedListNode<LruCacheItem>.Remove(ref _leastRecentlyUsed, node);
_cacheMap.Remove(key);
return true;
}

return false;
}

public bool Contains(TKey key)
{
return _cacheMap.ContainsKey(key);
}

public int Size
{
get
{
return _cacheMap.Count;
}
}

private void Replace(TKey key, TValue value)
{
LinkedListNode<LruCacheItem>? node = _leastRecentlyUsed;
if (node is null)
{
ThrowInvalidOperationException();
}

_cacheMap.Remove(node!.Value.Key);

node.Value = new(key, value);
LinkedListNode<LruCacheItem>.MoveToMostRecent(ref _leastRecentlyUsed, node);
_cacheMap.Add(key, node);

[DoesNotReturn]
static void ThrowInvalidOperationException()
{
throw new InvalidOperationException(
$"{nameof(LruCache<TKey, TValue>)} called {nameof(Replace)} when empty.");
}
}

private struct LruCacheItem(TKey k, TValue v)
{
public readonly TKey Key = k;
public TValue Value = v;
}

public long MemorySize => CalculateMemorySize(0, _cacheMap.Count);

public static long CalculateMemorySize(int keyPlusValueSize, int currentItemsCount)
{
// it may actually be different if the initial capacity not equal to max (depending on the dictionary growth path)

const int preInit = 48 /* LinkedList */ + 80 /* Dictionary */ + 24;
int postInit = 52 /* lazy init of two internal dictionary arrays + dictionary size times (entry size + int) */ + MemorySizes.FindNextPrime(currentItemsCount) * 28 + currentItemsCount * 80 /* LinkedListNode and CacheItem times items count */;
return MemorySizes.Align(preInit + postInit + keyPlusValueSize * currentItemsCount);
}
}
}
115 changes: 115 additions & 0 deletions src/Nethermind/Nethermind.Core/Caching/LruKeyCacheNonConcurrent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Nethermind.Core.Extensions;

namespace Nethermind.Core.Caching
{
public sealed class LruKeyCacheNonConcurrent<TKey> where TKey : notnull
{
private readonly int _maxCapacity;
private readonly string _name;
private readonly Dictionary<TKey, LinkedListNode<TKey>> _cacheMap;
private LinkedListNode<TKey>? _leastRecentlyUsed;

public LruKeyCacheNonConcurrent(int maxCapacity, int startCapacity, string name)
{
_maxCapacity = maxCapacity;
_name = name ?? throw new ArgumentNullException(nameof(name));
_cacheMap = typeof(TKey) == typeof(byte[])
? new Dictionary<TKey, LinkedListNode<TKey>>((IEqualityComparer<TKey>)Bytes.EqualityComparer)
: new Dictionary<TKey, LinkedListNode<TKey>>(startCapacity); // do not initialize it at the full capacity
}

public LruKeyCacheNonConcurrent(int maxCapacity, string name)
: this(maxCapacity, 0, name)
{
}

public void Clear()
{
_leastRecentlyUsed = null;
_cacheMap.Clear();
}

public bool Get(TKey key)
{
if (_cacheMap.TryGetValue(key, out LinkedListNode<TKey>? node))
{
LinkedListNode<TKey>.MoveToMostRecent(ref _leastRecentlyUsed, node);
return true;
}

return false;
}

public bool Set(TKey key)
{
if (_cacheMap.TryGetValue(key, out LinkedListNode<TKey>? node))
{
LinkedListNode<TKey>.MoveToMostRecent(ref _leastRecentlyUsed, node);
return false;
}
else
{
if (_cacheMap.Count >= _maxCapacity)
{
Replace(key);
}
else
{
LinkedListNode<TKey> newNode = new(key);
LinkedListNode<TKey>.AddMostRecent(ref _leastRecentlyUsed, newNode);
_cacheMap.Add(key, newNode);
}

return true;
}
}

public void Delete(TKey key)
{
if (_cacheMap.TryGetValue(key, out LinkedListNode<TKey>? node))
{
LinkedListNode<TKey>.Remove(ref _leastRecentlyUsed, node);
_cacheMap.Remove(key);
}
}

private void Replace(TKey key)
{
LinkedListNode<TKey>? node = _leastRecentlyUsed;
if (node is null)
{
ThrowInvalidOperation();
}

_cacheMap.Remove(node.Value);
node.Value = key;
LinkedListNode<TKey>.MoveToMostRecent(ref _leastRecentlyUsed, node);
_cacheMap.Add(key, node);

[DoesNotReturn]
static void ThrowInvalidOperation()
{
throw new InvalidOperationException(
$"{nameof(LruKeyCache<TKey>)} called {nameof(Replace)} when empty.");
}
}

public long MemorySize => CalculateMemorySize(0, _cacheMap.Count);

// TODO: memory size on the KeyCache will be smaller because we do not create LruCacheItems
public static long CalculateMemorySize(int keyPlusValueSize, int currentItemsCount)
{
// it may actually be different if the initial capacity not equal to max (depending on the dictionary growth path)

const int preInit = 48 /* LinkedList */ + 80 /* Dictionary */ + 24;
int postInit = 52 /* lazy init of two internal dictionary arrays + dictionary size times (entry size + int) */ + MemorySizes.FindNextPrime(currentItemsCount) * 28 + currentItemsCount * 80 /* LinkedListNode and CacheItem times items count */;
return MemorySizes.Align(preInit + postInit + keyPlusValueSize * currentItemsCount);
}
}
}
18 changes: 16 additions & 2 deletions src/Nethermind/Nethermind.Db/Metrics.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using Nethermind.Core.Attributes;

Expand All @@ -14,10 +12,18 @@ public static class Metrics
[Description("Number of Code DB cache reads.")]
public static long CodeDbCache { get; set; }

[CounterMetric]
[Description("Number of State Trie cache hits.")]
public static long StateTreeCache { get; set; }

[CounterMetric]
[Description("Number of State Trie reads.")]
public static long StateTreeReads { get; set; }

[CounterMetric]
[Description("Number of State Reader reads.")]
public static long StateReaderReads { get; set; }

[CounterMetric]
[Description("Number of Blocks Trie writes.")]
public static long StateTreeWrites { get; set; }
Expand All @@ -26,10 +32,18 @@ public static class Metrics
[Description("Number of State DB duplicate writes during full pruning.")]
public static int StateDbInPruningWrites;

[CounterMetric]
[Description("Number of storage trie cache hits.")]
public static long StorageTreeCache { get; set; }

[CounterMetric]
[Description("Number of storage trie reads.")]
public static long StorageTreeReads { get; set; }

[CounterMetric]
[Description("Number of storage reader reads.")]
public static long StorageReaderReads { get; set; }

[CounterMetric]
[Description("Number of storage trie writes.")]
public static long StorageTreeWrites { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion src/Nethermind/Nethermind.Evm/MemoryAllowance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ namespace Nethermind.Evm
{
public static class MemoryAllowance
{
public static int CodeCacheSize { get; } = 16_384;
public static int CodeCacheSize { get; } = 4_096 + 1_024;
}
}
Loading

0 comments on commit 091d255

Please sign in to comment.