Skip to content
Merged
57 changes: 57 additions & 0 deletions src/Nethermind/Nethermind.Core/Caching/StaticPoolcs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;
using System.Collections.Concurrent;
using System.Threading;
using Nethermind.Core.Resettables;

namespace Nethermind.Core.Caching;

public static class StaticPool<T> where T : class, IResettable, new()
{
// Hard cap for shared pool growth.
// Prevents unbounded accumulation under bursty workloads while still allowing per-thread caching.
private const int MaxPooledCount = 4096;

// Global fallback pool shared between threads.
// ConcurrentQueue is lock-free but not free of contention.
// It's used only when the thread-local fast path misses.
private static readonly ConcurrentQueue<T> _pool = [];

// Manual count of items in the queue.
// We maintain this separately because ConcurrentQueue.Count
// is an O(n) traversal — it walks the internal segment chain.
// Keeping our own count avoids that cost and keeps the hot path O(1).
private static int _poolCount;

public static T Rent()
{
// Try to pop from the global pool — this is only hit when a thread
// has exhausted its own fast slot or is cross-thread renting.
if (Volatile.Read(ref _poolCount) > 0 && _pool.TryDequeue(out T? item))
{
// We track count manually with Interlocked ops instead of using queue.Count.
Interlocked.Decrement(ref _poolCount);
return item;
}

// Nothing available, allocate new instance
return new();
}
Comment thread
benaadams marked this conversation as resolved.
Outdated

public static void Return(T item)
{
// We use Interlocked.Increment to reserve a slot up front.
// This guarantees a bounded queue length without relying on slow Count().
if (Interlocked.Increment(ref _poolCount) > MaxPooledCount)
{
// Roll back reservation if we'd exceed the cap.
Interlocked.Decrement(ref _poolCount);
return;
}

item.Reset();
_pool.Enqueue(item);
}
}
Comment thread
benaadams marked this conversation as resolved.
Outdated
25 changes: 20 additions & 5 deletions src/Nethermind/Nethermind.Core/Collections/StackList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Nethermind.Core.Caching;
using Nethermind.Core.Resettables;

namespace Nethermind.Core.Collections
{
public sealed class StackList<T> : List<T>
public sealed class StackList<T> : List<T>, IResettable, IReturnable
where T : struct, IComparable<T>
{
public T Peek() => this[^1];
Expand Down Expand Up @@ -47,10 +49,7 @@ public bool TryPop(out T item)
}
}

public void Push(T item)
{
Add(item);
}
public void Push(T item) => Add(item);

public bool TryGetSearchedItem(T activation, out T item)
{
Expand Down Expand Up @@ -79,5 +78,21 @@ public bool TryGetSearchedItem(T activation, out T item)

return result;
}

internal static StackList<T> Rent()
=> StaticPool<StackList<T>>.Rent();

public void Return() => Return(this);
public void Reset() => Clear();

private static void Return(StackList<T> value)
{
const int MaxPooledCapacity = 128;

if (value.Capacity > MaxPooledCapacity)
return;

StaticPool<StackList<T>>.Return(value);
}
}
}
21 changes: 21 additions & 0 deletions src/Nethermind/Nethermind.Core/Extensions/DictionaryExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;
Comment thread
benaadams marked this conversation as resolved.
Outdated
using System.Collections.Generic;
using Nethermind.Core.Resettables;

namespace Nethermind.Core.Extensions;

public static class DictionaryExtensions
{
Comment thread
benaadams marked this conversation as resolved.
public static void ResetAndClear<TKey, TValue>(this IDictionary<TKey, TValue> dictionary)
where TValue : class, IReturnable
{
foreach (TValue value in dictionary.Values)
{
value.Return();
}
dictionary.Clear();
}
}
11 changes: 11 additions & 0 deletions src/Nethermind/Nethermind.Core/Resettables/IResettable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;

Comment thread
benaadams marked this conversation as resolved.
Outdated
namespace Nethermind.Core.Resettables;

public interface IResettable
{
Comment thread
benaadams marked this conversation as resolved.
void Reset();
}
11 changes: 11 additions & 0 deletions src/Nethermind/Nethermind.Core/Resettables/IReturnable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;

Comment thread
benaadams marked this conversation as resolved.
Outdated
namespace Nethermind.Core.Resettables;

public interface IReturnable
{
Comment thread
benaadams marked this conversation as resolved.
void Return();
}
18 changes: 14 additions & 4 deletions src/Nethermind/Nethermind.Evm/StackPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Concurrent;
using System.Runtime.Intrinsics;
using System.Threading;

using static Nethermind.Evm.EvmState;

Expand All @@ -29,24 +30,33 @@ private readonly struct StackItem(byte[] dataStack, ReturnState[] returnStack)
/// <param name="returnStack"></param>
public void ReturnStacks(byte[] dataStack, ReturnState[] returnStack)
{
if (_stackPool.Count <= MaxStacksPooled)
// Reserve a slot first - O(1) bound without touching ConcurrentQueue.Count.
if (Interlocked.Increment(ref _poolCount) > MaxStacksPooled)
{
_stackPool.Enqueue(new(dataStack, returnStack));
// Cap hit - roll back the reservation and drop the item.
Interlocked.Decrement(ref _poolCount);
return;
}

_stackPool.Enqueue(new StackItem(dataStack, returnStack));
}

// Manual reservation count - upper bound on items actually in the queue.
private int _poolCount;

public const int StackLength = (EvmStack.MaxStackSize + EvmStack.RegisterLength) * 32;

public (byte[], ReturnState[]) RentStacks()
{
if (_stackPool.TryDequeue(out StackItem result))
if (Volatile.Read(ref _poolCount) > 0 && _stackPool.TryDequeue(out StackItem result))
{
Interlocked.Decrement(ref _poolCount);
return (result.DataStack, result.ReturnStack);
}

// Count was positive but we lost the race or the enqueuer has not published yet.
Comment thread
benaadams marked this conversation as resolved.
return
(
// Include extra Vector256<byte>.Count and pin so we can align to 32 bytes
GC.AllocateUninitializedArray<byte>(StackLength + Vector256<byte>.Count, pinned: true),
new ReturnState[EvmStack.ReturnStackSize]
);
Expand Down
41 changes: 8 additions & 33 deletions src/Nethermind/Nethermind.State/PartialStorageProviderBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;
Expand All @@ -7,6 +7,7 @@
using Nethermind.Core;
using Nethermind.Core.Collections;
using Nethermind.Core.Resettables;
using Nethermind.Core.Extensions;
using Nethermind.Evm.Tracing.State;
using Nethermind.Logging;

Expand Down Expand Up @@ -148,29 +149,6 @@ public void Commit(bool commitRoots = true)
Commit(NullStateTracer.Instance, commitRoots);
}

protected struct ChangeTrace
{
public static readonly ChangeTrace _zeroBytes = new(StorageTree.ZeroBytes, StorageTree.ZeroBytes);
public static ref readonly ChangeTrace ZeroBytes => ref _zeroBytes;

public ChangeTrace(byte[]? before, byte[]? after)
{
After = after ?? StorageTree.ZeroBytes;
Before = before ?? StorageTree.ZeroBytes;
}

public ChangeTrace(byte[]? after)
{
After = after ?? StorageTree.ZeroBytes;
Before = StorageTree.ZeroBytes;
IsInitialValue = true;
}

public byte[] Before;
public byte[] After;
public bool IsInitialValue;
}

/// <summary>
/// Commit persistent storage
/// </summary>
Expand Down Expand Up @@ -202,22 +180,19 @@ protected virtual void CommitStorageRoots()
/// Used for storage-specific logic
/// </summary>
/// <param name="tracer">Storage tracer</param>
protected virtual void CommitCore(IStorageTracer tracer)
{
_changes.Clear();
_intraBlockCache.Clear();
_transactionChangesSnapshots.Clear();
}
protected virtual void CommitCore(IStorageTracer tracer) => Reset();

/// <summary>
/// Reset the storage state
/// </summary>
Comment thread
benaadams marked this conversation as resolved.
public virtual void Reset(bool resetBlockChanges = true)
public virtual void Reset(bool resetBlockChanges = true) => Reset();

private void Reset()
{
if (_logger.IsTrace) _logger.Trace("Resetting storage");

_changes.Clear();
_intraBlockCache.Clear();
_intraBlockCache.ResetAndClear();
_transactionChangesSnapshots.Clear();
}
Comment thread
benaadams marked this conversation as resolved.

Expand Down Expand Up @@ -270,7 +245,7 @@ protected StackList<int> SetupRegistry(in StorageCell cell)
ref StackList<int>? value = ref CollectionsMarshal.GetValueRefOrAddDefault(_intraBlockCache, cell, out bool exists);
if (!exists)
{
value = new StackList<int>();
value = StackList<int>.Rent();
}

return value;
Expand Down
Loading
Loading