diff --git a/Recyclable.Collections/Pools/BiDirectionalRecyclableArrayPoolChunk.cs b/Recyclable.Collections/Pools/BiDirectionalRecyclableArrayPoolChunk.cs index d911ee1..d0ce568 100644 --- a/Recyclable.Collections/Pools/BiDirectionalRecyclableArrayPoolChunk.cs +++ b/Recyclable.Collections/Pools/BiDirectionalRecyclableArrayPoolChunk.cs @@ -6,7 +6,7 @@ namespace Recyclable.Collections.Pools internal static T[] Empty => _empty; - internal T[] Buffer; + internal T[] Value; internal int Top; internal int Bottom; internal BiDirectionalRecyclableArrayPoolChunk? Previous; @@ -14,7 +14,7 @@ namespace Recyclable.Collections.Pools public BiDirectionalRecyclableArrayPoolChunk() { - Buffer = _empty; + Value = _empty; } } } diff --git a/Recyclable.Collections/Pools/BiDirectionalRecyclableArrayPoolChunkPool.cs b/Recyclable.Collections/Pools/BiDirectionalRecyclableArrayPoolChunkPool.cs index 27206f3..5b36fc8 100644 --- a/Recyclable.Collections/Pools/BiDirectionalRecyclableArrayPoolChunkPool.cs +++ b/Recyclable.Collections/Pools/BiDirectionalRecyclableArrayPoolChunkPool.cs @@ -20,13 +20,13 @@ namespace Recyclable.Collections.Pools public static void Dispose(BiDirectionalRecyclableArrayPoolChunk chunk, bool needsClearing) { - var buffer = chunk.Buffer; + var buffer = chunk.Value; if (buffer.Length >= RecyclableDefaults.MinPooledArrayLength) { RecyclableArrayPool.ReturnShared(buffer, needsClearing); } - chunk.Buffer = BiDirectionalRecyclableArrayPoolChunk.Empty; + chunk.Value = BiDirectionalRecyclableArrayPoolChunk.Empty; chunk.Top = 0; chunk.Bottom = 0; chunk.Previous = null; diff --git a/Recyclable.Collections/Pools/RecyclableArrayPoolChunk.cs b/Recyclable.Collections/Pools/RecyclableArrayPoolChunk.cs index 847c3cd..da4fe49 100644 --- a/Recyclable.Collections/Pools/RecyclableArrayPoolChunk.cs +++ b/Recyclable.Collections/Pools/RecyclableArrayPoolChunk.cs @@ -6,14 +6,14 @@ namespace Recyclable.Collections.Pools internal static T[] Empty => _empty; - internal T[] Buffer; + internal T[] Value; internal int Index; internal RecyclableArrayPoolChunk? Previous; internal RecyclableArrayPoolChunk? Next; public RecyclableArrayPoolChunk() { - Buffer = _empty; + Value = _empty; } } } diff --git a/Recyclable.Collections/Pools/RecyclableArrayPoolChunkPool.cs b/Recyclable.Collections/Pools/RecyclableArrayPoolChunkPool.cs index 2738239..d64ff90 100644 --- a/Recyclable.Collections/Pools/RecyclableArrayPoolChunkPool.cs +++ b/Recyclable.Collections/Pools/RecyclableArrayPoolChunkPool.cs @@ -19,12 +19,12 @@ namespace Recyclable.Collections.Pools public static void Dispose(RecyclableArrayPoolChunk chunk, bool needsClearing) { - var buffer = chunk.Buffer; + var buffer = chunk.Value; if (buffer.Length >= RecyclableDefaults.MinPooledArrayLength) { RecyclableArrayPool.ReturnShared(buffer, needsClearing); } - chunk.Buffer = RecyclableArrayPoolChunk.Empty; + chunk.Value = RecyclableArrayPoolChunk.Empty; chunk.Index = 0; chunk.Previous = null; chunk.Next = null; diff --git a/Recyclable.Collections/RecyclableLinkedList.cs b/Recyclable.Collections/RecyclableLinkedList.cs new file mode 100644 index 0000000..b832de3 --- /dev/null +++ b/Recyclable.Collections/RecyclableLinkedList.cs @@ -0,0 +1,323 @@ +using System.Collections; +using System.Numerics; +using System.Runtime.CompilerServices; +using Recyclable.Collections.Pools; + +namespace Recyclable.Collections +{ + internal sealed class RecyclableLinkedList : IEnumerable, IDisposable + { + private static readonly bool _needsClearing = !typeof(T).IsValueType; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static BiDirectionalRecyclableArrayPoolChunk RentChunk(int size, BiDirectionalRecyclableArrayPoolChunk? previous, bool forHead) + { + var chunk = BiDirectionalRecyclableArrayPoolChunkPool.Rent(); + if (chunk.Value.Length < size) + { + if (chunk.Value.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(chunk.Value, _needsClearing); + } + + chunk.Value = size >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(size) + : new T[size]; + } + + if (forHead) + { + chunk.Top = size; + chunk.Bottom = size; + } + else + { + chunk.Top = 0; + chunk.Bottom = 0; + } + + chunk.Previous = previous; + chunk.Next = null; + return chunk; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ReturnChunk(BiDirectionalRecyclableArrayPoolChunk chunk) + { + BiDirectionalRecyclableArrayPoolChunkPool.Return(chunk); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DisposeChunk(BiDirectionalRecyclableArrayPoolChunk chunk) + { + BiDirectionalRecyclableArrayPoolChunkPool.Dispose(chunk, _needsClearing); + } + + private BiDirectionalRecyclableArrayPoolChunk _head; + private BiDirectionalRecyclableArrayPoolChunk _tail; + private bool _disposed; + private long _capacity; + private long _count; + + public RecyclableLinkedList(int initialCapacity = RecyclableDefaults.InitialCapacity) + { + if (initialCapacity < 1) + { + initialCapacity = 1; + } + + if (!BitOperations.IsPow2((uint)initialCapacity)) + { + initialCapacity = (int)BitOperations.RoundUpToPowerOf2((uint)initialCapacity); + } + + _head = RentChunk(initialCapacity, null, false); + _tail = _head; + _capacity = initialCapacity; + _count = 0; + } + + public RecyclableLinkedList(IEnumerable source, int initialCapacity = RecyclableDefaults.InitialCapacity) : this(initialCapacity) + { + foreach (var item in source) + { + AddLast(item); + } + } + + public int Count => checked((int)_count); + public long LongCount => _count; + + public void AddLast(T item) + { + if (_tail.Top == _tail.Value.Length) + { + GrowTail(); + } + + _tail.Value[_tail.Top++] = item; + _count++; + } + + public void AddFirst(T item) + { + if (_head.Bottom == 0) + { + GrowHead(); + } + + _head.Value[--_head.Bottom] = item; + _count++; + } + + public T RemoveFirst() + { + if (_count == 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(_count), "List is empty"); + } + + _count--; + var value = _head.Value[_head.Bottom]; + if (_needsClearing) + { + _head.Value[_head.Bottom] = default!; + } + + _head.Bottom++; + if (_head.Bottom == _head.Top && _head.Next != null) + { + ReleaseHeadChunk(); + } + else if (_head.Bottom == _head.Top) + { + _head.Bottom = _head.Top; + } + + return value; + } + + public T RemoveLast() + { + if (_count == 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(_count), "List is empty"); + } + + _count--; + _tail.Top--; + var value = _tail.Value[_tail.Top]; + if (_needsClearing) + { + _tail.Value[_tail.Top] = default!; + } + + if (_tail.Top == _tail.Bottom && _tail.Previous != null) + { + ReleaseTailChunk(); + } + else if (_tail.Top == _tail.Bottom) + { + _tail.Top = _tail.Bottom; + } + + return value; + } + + public void Clear() + { + while (_head.Next != null) + { + ReleaseHeadChunk(); + } + + while (_tail.Previous != null) + { + ReleaseTailChunk(); + } + + if (_needsClearing && _head.Top > _head.Bottom) + { + Array.Clear(_head.Value, _head.Bottom, _head.Top - _head.Bottom); + } + + _capacity = _head.Value.Length; + _head.Bottom = _head.Top; + _tail = _head; + _count = 0; + } + + public Enumerator GetEnumerator() => new(this); + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); + + private void GrowTail() + { + int newSize; + if (_capacity >= RecyclableDefaults.MaxPooledBlockSize) + { + newSize = RecyclableDefaults.MaxPooledBlockSize; + } + else + { + var doubled = _capacity << 1; + newSize = (int)Math.Min(doubled - _capacity, RecyclableDefaults.MaxPooledBlockSize); + } + + var newChunk = RentChunk(newSize, _tail, false); + _tail.Next = newChunk; + _tail = newChunk; + _capacity += newSize; + } + + private void GrowHead() + { + int newSize; + if (_capacity >= RecyclableDefaults.MaxPooledBlockSize) + { + newSize = RecyclableDefaults.MaxPooledBlockSize; + } + else + { + var doubled = _capacity << 1; + newSize = (int)Math.Min(doubled - _capacity, RecyclableDefaults.MaxPooledBlockSize); + } + + var newChunk = RentChunk(newSize, null, true); + newChunk.Next = _head; + _head.Previous = newChunk; + _head = newChunk; + _capacity += newSize; + } + + private void ReleaseHeadChunk() + { + var toReturn = _head; + _head = toReturn.Next!; + _head.Previous = null; + _capacity -= toReturn.Value.Length; + ReturnChunk(toReturn); + } + + private void ReleaseTailChunk() + { + var toReturn = _tail; + _tail = toReturn.Previous!; + _tail.Next = null; + _capacity -= toReturn.Value.Length; + ReturnChunk(toReturn); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + var chunk = _head; + while (chunk != null) + { + var next = chunk.Next; + DisposeChunk(chunk); + chunk = next; + } + + _disposed = true; + GC.SuppressFinalize(this); + } + + public struct Enumerator : IEnumerator + { + private readonly BiDirectionalRecyclableArrayPoolChunk? _start; + private BiDirectionalRecyclableArrayPoolChunk? _chunk; + private int _index; + private T? _current; + + internal Enumerator(RecyclableLinkedList list) + { + _start = list._head; + _chunk = list._head; + _index = _chunk != null ? _chunk.Bottom : 0; + _current = default; + } + + public T Current => _current!; + object IEnumerator.Current => _current!; + + public bool MoveNext() + { + if (_chunk == null) + { + return false; + } + + if (_index >= _chunk.Top) + { + _chunk = _chunk.Next; + if (_chunk == null) + { + return false; + } + _index = _chunk.Bottom; + } + + _current = _chunk.Value[_index++]; + return true; + } + + public void Reset() + { + _chunk = _start; + _index = _chunk != null ? _chunk.Bottom : 0; + } + + public void Dispose() + { + _chunk = null; + _current = default; + } + } + } +} + diff --git a/Recyclable.Collections/RecyclableQueue.cs b/Recyclable.Collections/RecyclableQueue.cs index 6b6d9d8..30dd59d 100644 --- a/Recyclable.Collections/RecyclableQueue.cs +++ b/Recyclable.Collections/RecyclableQueue.cs @@ -13,14 +13,14 @@ namespace Recyclable.Collections private static BiDirectionalRecyclableArrayPoolChunk RentChunk(int size, BiDirectionalRecyclableArrayPoolChunk? previous) { var chunk = BiDirectionalRecyclableArrayPoolChunkPool.Rent(); - if (chunk.Buffer.Length < size) + if (chunk.Value.Length < size) { - if (chunk.Buffer.Length >= RecyclableDefaults.MinPooledArrayLength) + if (chunk.Value.Length >= RecyclableDefaults.MinPooledArrayLength) { - RecyclableArrayPool.ReturnShared(chunk.Buffer, _needsClearing); + RecyclableArrayPool.ReturnShared(chunk.Value, _needsClearing); } - chunk.Buffer = size >= RecyclableDefaults.MinPooledArrayLength + chunk.Value = size >= RecyclableDefaults.MinPooledArrayLength ? RecyclableArrayPool.RentShared(size) : new T[size]; } @@ -81,12 +81,12 @@ namespace Recyclable.Collections public void Enqueue(T item) { - if (_tail.Top == _tail.Buffer.Length) + if (_tail.Top == _tail.Value.Length) { Grow(); } - _tail.Buffer[_tail.Top++] = item; + _tail.Value[_tail.Top++] = item; _count++; } @@ -100,10 +100,10 @@ namespace Recyclable.Collections } _count--; - var value = _head.Buffer[_head.Bottom]; + var value = _head.Value[_head.Bottom]; if (_needsClearing) { - _head.Buffer[_head.Bottom] = default!; + _head.Value[_head.Bottom] = default!; } _head.Bottom++; @@ -141,10 +141,10 @@ namespace Recyclable.Collections if (_needsClearing && _head.Top > _head.Bottom) { - Array.Clear(_head.Buffer, _head.Bottom, _head.Top - _head.Bottom); + Array.Clear(_head.Value, _head.Bottom, _head.Top - _head.Bottom); } - _capacity = _head.Buffer.Length; + _capacity = _head.Value.Length; _head.Top = 0; _head.Bottom = 0; _tail = _head; @@ -179,7 +179,7 @@ namespace Recyclable.Collections var toReturn = _head; _head = toReturn.Next!; _head.Previous = null; - _capacity -= toReturn.Buffer.Length; + _capacity -= toReturn.Value.Length; ReturnChunk(toReturn); } @@ -237,7 +237,7 @@ namespace Recyclable.Collections _index = _chunk.Bottom; } - _current = _chunk.Buffer[_index++]; + _current = _chunk.Value[_index++]; return true; } diff --git a/Recyclable.Collections/RecyclableStack.cs b/Recyclable.Collections/RecyclableStack.cs index def9ba0..27b9e04 100644 --- a/Recyclable.Collections/RecyclableStack.cs +++ b/Recyclable.Collections/RecyclableStack.cs @@ -13,14 +13,14 @@ namespace Recyclable.Collections private static RecyclableArrayPoolChunk RentChunk(int size, RecyclableArrayPoolChunk? previous) { var chunk = RecyclableArrayPoolChunkPool.Rent(); - if (chunk.Buffer.Length < size) + if (chunk.Value.Length < size) { - if (chunk.Buffer.Length >= RecyclableDefaults.MinPooledArrayLength) + if (chunk.Value.Length >= RecyclableDefaults.MinPooledArrayLength) { - RecyclableArrayPool.ReturnShared(chunk.Buffer, _needsClearing); + RecyclableArrayPool.ReturnShared(chunk.Value, _needsClearing); } - chunk.Buffer = size >= RecyclableDefaults.MinPooledArrayLength + chunk.Value = size >= RecyclableDefaults.MinPooledArrayLength ? RecyclableArrayPool.RentShared(size) : new T[size]; } @@ -77,12 +77,12 @@ namespace Recyclable.Collections public void Push(T item) { - if (_current.Index == _current.Buffer.Length) + if (_current.Index == _current.Value.Length) { Grow(); } - _current.Buffer[_current.Index++] = item; + _current.Value[_current.Index++] = item; _count++; } @@ -97,10 +97,10 @@ namespace Recyclable.Collections _count--; _current.Index--; - var value = _current.Buffer[_current.Index]; + var value = _current.Value[_current.Index]; if (_needsClearing) { - _current.Buffer[_current.Index] = default!; + _current.Value[_current.Index] = default!; } if (_current.Index == 0 && _current.Previous != null) @@ -120,10 +120,10 @@ namespace Recyclable.Collections if (_needsClearing && _current.Index > 0) { - Array.Clear(_current.Buffer, 0, _current.Index); + Array.Clear(_current.Value, 0, _current.Index); } - _capacity = _current.Buffer.Length; + _capacity = _current.Value.Length; _current.Index = 0; _count = 0; } @@ -156,7 +156,7 @@ namespace Recyclable.Collections var toReturn = _current; _current = toReturn.Previous!; _current.Next = null; - _capacity -= toReturn.Buffer.Length; + _capacity -= toReturn.Value.Length; ReturnChunk(toReturn); } @@ -214,7 +214,7 @@ namespace Recyclable.Collections _index = _chunk.Index - 1; } - _current = _chunk.Buffer[_index--]; + _current = _chunk.Value[_index--]; return true; } diff --git a/Recyclable.CollectionsTests/RecyclableLinkedListTests.cs b/Recyclable.CollectionsTests/RecyclableLinkedListTests.cs new file mode 100644 index 0000000..bf458c1 --- /dev/null +++ b/Recyclable.CollectionsTests/RecyclableLinkedListTests.cs @@ -0,0 +1,75 @@ +using FluentAssertions; +using Recyclable.Collections; +using System.Linq; + +namespace Recyclable.CollectionsTests +{ + public class RecyclableLinkedListTests + { + private static readonly string[] _testData = new[] { "a", "d", "c", "b", "a" }; + + [Fact] + public void AddLastShouldPreserveOrder() + { + using var list = new RecyclableLinkedList(); + foreach (var item in _testData) + { + list.AddLast(item); + } + + var actual = new RecyclableLongList(); + foreach (var item in list) + { + actual.Add(item); + } + + _ = actual.Should().ContainInConsecutiveOrder(_testData) + .And.BeEquivalentTo(_testData); + } + + [Fact] + public void AddFirstShouldReverseOrder() + { + using var list = new RecyclableLinkedList(); + foreach (var item in _testData) + { + list.AddFirst(item); + } + + var expected = _testData.Reverse().ToArray(); + var actual = new RecyclableLongList(); + foreach (var item in list) + { + actual.Add(item); + } + + _ = actual.Should().ContainInConsecutiveOrder(expected); + } + + [Fact] + public void RemoveFirstShouldReturnItemsInOrder() + { + using var list = new RecyclableLinkedList(_testData); + var actual = new RecyclableLongList(); + while (list.LongCount > 0) + { + actual.Add(list.RemoveFirst()); + } + + _ = actual.Should().ContainInConsecutiveOrder(_testData); + } + + [Fact] + public void RemoveLastShouldReturnItemsInReverseOrder() + { + using var list = new RecyclableLinkedList(_testData); + var actual = new RecyclableLongList(); + while (list.LongCount > 0) + { + actual.Add(list.RemoveLast()); + } + + _ = actual.Should().ContainInConsecutiveOrder(_testData.Reverse()); + } + } +}