From 1b4b9f534bf763cd94609b1e8b394bdc9fe282a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sat, 12 Jul 2025 02:46:17 +0200 Subject: [PATCH 1/2] Align stack enumerator with .NET Stack --- Recyclable.Collections/RecyclableStack.cs | 285 +++++++++++++----- .../RecyclableStackTests.cs | 10 +- 2 files changed, 213 insertions(+), 82 deletions(-) diff --git a/Recyclable.Collections/RecyclableStack.cs b/Recyclable.Collections/RecyclableStack.cs index 3b63bc0..8d7e40b 100644 --- a/Recyclable.Collections/RecyclableStack.cs +++ b/Recyclable.Collections/RecyclableStack.cs @@ -1,94 +1,225 @@ -using System.Collections; +using System.Collections; +using System.Numerics; +using Recyclable.Collections.Pools; namespace Recyclable.Collections { - internal class RecyclableStack : IList, IDisposable - { - private static readonly bool _needsClearing = !typeof(T).IsValueType; - private bool _disposedValue; + internal sealed class RecyclableStack : IEnumerable, IDisposable + { + private static readonly bool _needsClearing = !typeof(T).IsValueType; - protected RecyclableLongList List { get; } - public int Count => checked((int)List._longCount); - public long LongCount - { - get => List._longCount; - set => List._longCount = value; - } - public bool IsReadOnly => false; + private sealed class Chunk + { + internal T[] Buffer; + internal int Index; + internal Chunk? Previous; + internal Chunk? Next; - public T this[int index] { get => this[(long)index]; set => this[(long)index] = value; } - public T this[long index] { get => List[index]; set => List[index] = value; } + internal Chunk(int size, Chunk? previous) + { + Buffer = size >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(size) + : new T[size]; + Previous = previous; + Next = null; + Index = 0; + } + } - public RecyclableStack(int blockSize = RecyclableDefaults.BlockSize) - { - List = new(blockSize); - } + private Chunk _current; + private Chunk _bottom; + private bool _disposed; + private long _capacity; + private long _count; - public RecyclableStack(IEnumerable list, int blockSize = RecyclableDefaults.BlockSize) - { - List = new(list, blockSize); - } + public RecyclableStack(int initialCapacity = RecyclableDefaults.InitialCapacity) + { + if (initialCapacity < 1) + { + initialCapacity = 1; + } - public RecyclableStack(RecyclableLongList list, int blockSize = RecyclableDefaults.BlockSize) - { - List = new(list, blockSize); - } + if (!BitOperations.IsPow2((uint)initialCapacity)) + { + initialCapacity = (int)BitOperations.RoundUpToPowerOf2((uint)initialCapacity); + } - public void Push(T item) - { - List.Add(item); - } + _current = new Chunk(initialCapacity, null); + _bottom = _current; + _capacity = initialCapacity; + _count = 0; + } - public T Pop() - { - RecyclableLongList list = List; - var itemIndex = (int)(list.LongCount & list._blockSizeMinus1) - 1; - var blockIndex = (int)(LongCount >> list._blockSizePow2BitShift); - var toRemove = list._memoryBlocks[blockIndex][itemIndex]; - - LongCount--; + public RecyclableStack(IEnumerable source, int initialCapacity = RecyclableDefaults.InitialCapacity) : this(initialCapacity) + { + foreach (var item in source) + { + Push(item); + } + } - if (_needsClearing) - { -#nullable disable - new Span(list._memoryBlocks[blockIndex])[itemIndex] = default; -#nullable restore - } + public int Count => checked((int)_count); + public long LongCount => _count; - if (itemIndex == 0) - { - list.RemoveBlock(blockIndex); - } + public void Push(T item) + { + if (_current.Index == _current.Buffer.Length) + { + Grow(); + } - return toRemove; - } + _current.Buffer[_current.Index++] = item; + _count++; + } - public int IndexOf(T item) => List.IndexOf(item); - public long LongIndexOf(T item) => List.LongIndexOf(item); - public void Insert(int index, T item) => List.Insert(index, item); - public void RemoveAt(int index) => List.RemoveAt(index); - public void Add(T item) => List.Add(item); - public void Clear() => List.Clear(); - public bool Contains(T item) => List.Contains(item); - public void CopyTo(T[] array, int arrayIndex) => List.CopyTo(array, arrayIndex); - public bool Remove(T item) => List.Remove(item); - public IEnumerator GetEnumerator() => List.Reverse().GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public void Add(T item) => Push(item); - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - Clear(); - _disposedValue = true; - } - } + public T Pop() + { + if (_count == 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(_count), "Stack is empty"); + } - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - } + _count--; + _current.Index--; + var value = _current.Buffer[_current.Index]; + if (_needsClearing) + { + _current.Buffer[_current.Index] = default!; + } + + if (_current.Index == 0 && _current.Previous != null) + { + ReleaseCurrentChunk(); + } + + return value; + } + + public void Clear() + { + while (_current.Previous != null) + { + ReleaseCurrentChunk(); + } + + if (_needsClearing && _current.Index > 0) + { + Array.Clear(_current.Buffer, 0, _current.Index); + } + + _capacity = _current.Buffer.Length; + _current.Index = 0; + _count = 0; + _bottom = _current; + } + + public Enumerator GetEnumerator() => new(this); + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); + + private void Grow() + { + int newSize; + if (_capacity >= RecyclableDefaults.MaxPooledBlockSize) + { + newSize = RecyclableDefaults.MaxPooledBlockSize; + } + else + { + var doubled = _capacity * 2; + newSize = (int)Math.Min(doubled - _capacity, RecyclableDefaults.MaxPooledBlockSize); + } + + var newChunk = new Chunk(newSize, _current); + _current.Next = newChunk; + _current = newChunk; + _capacity += newSize; + } + + private void ReleaseCurrentChunk() + { + var toReturn = _current.Buffer; + _current = _current.Previous!; + _current.Next = null; + _capacity -= toReturn.Length; + if (_current.Previous == null) + { + _bottom = _current; + } + if (toReturn.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(toReturn, _needsClearing); + } + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + Clear(); + if (_current.Buffer.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(_current.Buffer, _needsClearing); + } + _disposed = true; + GC.SuppressFinalize(this); + } + + public struct Enumerator : IEnumerator + { + private readonly Chunk? _start; + private Chunk? _chunk; + private int _index; + private T? _current; + + internal Enumerator(RecyclableStack stack) + { + _start = stack._current; + _chunk = stack._current; + _index = _chunk != null ? _chunk.Index - 1 : -1; + _current = default; + } + + public T Current => _current!; + object IEnumerator.Current => _current!; + + public bool MoveNext() + { + if (_chunk == null) + { + return false; + } + + if (_index < 0) + { + _chunk = _chunk.Previous; + if (_chunk == null) + { + return false; + } + _index = _chunk.Index - 1; + } + + _current = _chunk.Buffer[_index--]; + return true; + } + + public void Reset() + { + _chunk = _start; + _index = _chunk != null ? _chunk.Index - 1 : -1; + } + + public void Dispose() + { + _chunk = null; + _current = default; + } + } + } } diff --git a/Recyclable.CollectionsTests/RecyclableStackTests.cs b/Recyclable.CollectionsTests/RecyclableStackTests.cs index dffdd52..b0deeec 100644 --- a/Recyclable.CollectionsTests/RecyclableStackTests.cs +++ b/Recyclable.CollectionsTests/RecyclableStackTests.cs @@ -32,8 +32,8 @@ namespace Recyclable.CollectionsTests using var sortedList = new RecyclableStack(_testData, 2); // Validate - _ = sortedList.Should().HaveCount(_testData.Length) - .And.ContainInConsecutiveOrder(_testData); + _ = sortedList.Should().HaveCount(_testData.Length) + .And.ContainInConsecutiveOrder(_testData.Reverse()); } [Fact] @@ -60,9 +60,9 @@ namespace Recyclable.CollectionsTests }; // Validate - _ = sortedList.Should().NotBeEmpty() - .And.ContainInConsecutiveOrder(_testData) - .And.BeEquivalentTo(_testData); + _ = sortedList.Should().NotBeEmpty() + .And.ContainInConsecutiveOrder(_testData.Reverse()) + .And.BeEquivalentTo(_testData); } [Fact] From ac163014d8103f3998827049772a20b3d4e9813d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sat, 12 Jul 2025 03:17:00 +0200 Subject: [PATCH 2/2] Add chunk pooling and optimize stack growth --- Recyclable.Collections/RecyclableStack.cs | 59 ++++++++++++++--------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/Recyclable.Collections/RecyclableStack.cs b/Recyclable.Collections/RecyclableStack.cs index 8d7e40b..c84cf0e 100644 --- a/Recyclable.Collections/RecyclableStack.cs +++ b/Recyclable.Collections/RecyclableStack.cs @@ -1,5 +1,7 @@ using System.Collections; using System.Numerics; +using Microsoft.Extensions.ObjectPool; +using System.Runtime.CompilerServices; using Recyclable.Collections.Pools; namespace Recyclable.Collections @@ -7,23 +9,42 @@ namespace Recyclable.Collections internal sealed class RecyclableStack : IEnumerable, IDisposable { private static readonly bool _needsClearing = !typeof(T).IsValueType; + private static readonly ObjectPool _chunkPool = new DefaultObjectPool(new DefaultPooledObjectPolicy()); private sealed class Chunk { - internal T[] Buffer; + internal T[] Buffer = Array.Empty(); internal int Index; internal Chunk? Previous; internal Chunk? Next; + } - internal Chunk(int size, Chunk? previous) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Chunk RentChunk(int size, Chunk? previous) + { + var chunk = _chunkPool.Get(); + chunk.Buffer = size >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(size) + : new T[size]; + chunk.Index = 0; + chunk.Previous = previous; + chunk.Next = null; + return chunk; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ReturnChunk(Chunk chunk) + { + var buffer = chunk.Buffer; + if (buffer.Length >= RecyclableDefaults.MinPooledArrayLength) { - Buffer = size >= RecyclableDefaults.MinPooledArrayLength - ? RecyclableArrayPool.RentShared(size) - : new T[size]; - Previous = previous; - Next = null; - Index = 0; + RecyclableArrayPool.ReturnShared(buffer, _needsClearing); } + chunk.Buffer = Array.Empty(); + chunk.Index = 0; + chunk.Previous = null; + chunk.Next = null; + _chunkPool.Return(chunk); } private Chunk _current; @@ -44,7 +65,7 @@ namespace Recyclable.Collections initialCapacity = (int)BitOperations.RoundUpToPowerOf2((uint)initialCapacity); } - _current = new Chunk(initialCapacity, null); + _current = RentChunk(initialCapacity, null); _bottom = _current; _capacity = initialCapacity; _count = 0; @@ -128,11 +149,11 @@ namespace Recyclable.Collections } else { - var doubled = _capacity * 2; + var doubled = _capacity << 1; newSize = (int)Math.Min(doubled - _capacity, RecyclableDefaults.MaxPooledBlockSize); } - var newChunk = new Chunk(newSize, _current); + var newChunk = RentChunk(newSize, _current); _current.Next = newChunk; _current = newChunk; _capacity += newSize; @@ -140,18 +161,15 @@ namespace Recyclable.Collections private void ReleaseCurrentChunk() { - var toReturn = _current.Buffer; - _current = _current.Previous!; + var toReturn = _current; + _current = toReturn.Previous!; _current.Next = null; - _capacity -= toReturn.Length; + _capacity -= toReturn.Buffer.Length; if (_current.Previous == null) { _bottom = _current; } - if (toReturn.Length >= RecyclableDefaults.MinPooledArrayLength) - { - RecyclableArrayPool.ReturnShared(toReturn, _needsClearing); - } + ReturnChunk(toReturn); } public void Dispose() @@ -162,10 +180,7 @@ namespace Recyclable.Collections } Clear(); - if (_current.Buffer.Length >= RecyclableDefaults.MinPooledArrayLength) - { - RecyclableArrayPool.ReturnShared(_current.Buffer, _needsClearing); - } + ReturnChunk(_current); _disposed = true; GC.SuppressFinalize(this); }