From 93b8dc5317fc7a2ce2b5bea34432b64d1ca615e5 Mon Sep 17 00:00:00 2001 From: mlemanczyk Date: Sat, 7 Jan 2023 20:52:01 +0100 Subject: [PATCH] Initial version of RecyclableBlockList implemented. Tests switched to xUnit in hope to avoid stability issues. --- .../DelegateVsComparerBenchmark.cs | 1 - .../ListVsLinkedListBenchmark.cs | 160 ++++++--- Recyclable.Collections.Benchmarks/Program.cs | 2 +- Recyclable.Collections/ListExtensions.cs | 93 +++++ .../Recyclable.Collections.csproj | 2 +- Recyclable.Collections/RecyclableBlockList.cs | 334 ++++++++++++++++++ Recyclable.Collections/RecyclableDefaults.cs | 1 + Recyclable.Collections/RecyclableList.cs | 96 +++-- .../RecyclableListExtensions.cs | 44 ++- Recyclable.Collections/RecyclableQueue.cs | 174 +++++---- .../Recyclable.CollectionsTests.csproj | 8 +- .../RecyclableBlockListTests.cs | 191 ++++++++++ .../RecyclableListTests.cs | 35 +- .../RecyclableQueueTests.cs | 25 +- .../RecyclableSortedListTests.cs | 11 +- .../RecyclableStackTests.cs | 18 +- Recyclable.CollectionsTests/Usings.cs | 2 +- 17 files changed, 969 insertions(+), 228 deletions(-) create mode 100644 Recyclable.Collections/RecyclableBlockList.cs create mode 100644 Recyclable.CollectionsTests/RecyclableBlockListTests.cs diff --git a/Recyclable.Collections.Benchmarks/DelegateVsComparerBenchmark.cs b/Recyclable.Collections.Benchmarks/DelegateVsComparerBenchmark.cs index c438886..904f509 100644 --- a/Recyclable.Collections.Benchmarks/DelegateVsComparerBenchmark.cs +++ b/Recyclable.Collections.Benchmarks/DelegateVsComparerBenchmark.cs @@ -1,6 +1,5 @@ using BenchmarkDotNet.Attributes; using MiscUtil; -using MiscUtil.Extensions; namespace Recyclable.Collections.Benchmarks { diff --git a/Recyclable.Collections.Benchmarks/ListVsLinkedListBenchmark.cs b/Recyclable.Collections.Benchmarks/ListVsLinkedListBenchmark.cs index 79d6b71..c43858e 100644 --- a/Recyclable.Collections.Benchmarks/ListVsLinkedListBenchmark.cs +++ b/Recyclable.Collections.Benchmarks/ListVsLinkedListBenchmark.cs @@ -5,9 +5,23 @@ namespace Recyclable.Collections.Benchmarks [MemoryDiagnoser] public class ListVsLinkedListBenchmark { - private const int _objectCount = 1_000_000; - private const int _blockSize = 100_000; - private static readonly object[] _testObjects = Enumerable.Range(1, _objectCount).Select(x => new object()).ToArray(); + //[Params(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 128, 256, 512, 1024, 2048, 4096, 8192, 1_000_000)] + public int TestObjectCount = 1_000_000; + public int BlockSize => TestObjectCount switch + { + 0 => 1, + > 0 and <= 10 => TestObjectCount, + _ => TestObjectCount / 10 + }; + + private object[]? _testObjects; + + public ListVsLinkedListBenchmark() + { + _ = TestObjects; + } + + private object[] TestObjects => _testObjects ??= Enumerable.Range(1, TestObjectCount).Select(x => new object()).ToArray(); private static void DoNothing(T obj) { @@ -17,10 +31,12 @@ namespace Recyclable.Collections.Benchmarks [Benchmark] public void Array() { - var list = new object[_testObjects.LongLength]; - for (var i = 0; i < _testObjects.Length; i++) + var data = TestObjects; + var dataCount = TestObjectCount; + var list = new object[dataCount]; + for (var i = 0; i < dataCount; i++) { - list[i] = _testObjects[i]; + list[i] = data[i]; } for (var i = 0; i < list.LongLength; i++) @@ -29,18 +45,39 @@ namespace Recyclable.Collections.Benchmarks } } + [Benchmark] + public void RecyclableBlockList() + { + var data = TestObjects; + var dataCount = TestObjectCount; + using var list = new RecyclableBlockList(initialCapacity: dataCount); + for (var i = 0; i < dataCount; i++) + { + list.Add(data[i]); + } + + for (var i = 0; i < list.LongCount; i++) + { + DoNothing(list[i]); + } + + list.Clear(); + } + [Benchmark(Baseline = true)] public void RecyclableList() { - using var list = new RecyclableList(_blockSize); - for (var i = 0; i < _testObjects.LongLength; i++) + var data = TestObjects; + var dataCount = TestObjectCount; + using var list = new RecyclableList(BlockSize, initialCapacity: dataCount); + for (var i = 0; i < dataCount; i++) { - list.Add(_testObjects[i]); + list.Add(data[i]); } - for (var i = 0; i < list.Count; i++) + for (var i = 0; i < list.LongCount; i++) { - DoNothing(_testObjects[i]); + DoNothing(list[i]); } list.Clear(); @@ -49,10 +86,12 @@ namespace Recyclable.Collections.Benchmarks [Benchmark] public void List() { - var list = new List(); - for (var i = 0; i < _testObjects.LongLength; i++) + var data = TestObjects; + var dataCount = TestObjectCount; + var list = new List(capacity: dataCount); + for (var i = 0; i < dataCount; i++) { - list.Add(_testObjects[i]); + list.Add(data[i]); } for (var i = 0; i < list.Count; i++) @@ -66,10 +105,12 @@ namespace Recyclable.Collections.Benchmarks //[Benchmark] public void LinkedList() { + var data = TestObjects; + var dataCount = TestObjectCount; var list = new LinkedList(); - for (var i = 0; i < _testObjects.LongLength; i++) + for (var i = 0; i < dataCount; i++) { - _ = list.AddLast(_testObjects[i]); + _ = list.AddLast(data[i]); } foreach (var node in list) @@ -80,18 +121,20 @@ namespace Recyclable.Collections.Benchmarks list.Clear(); } - [Benchmark] + //[Benchmark] public void RecyclableQueue() { - var list = new RecyclableQueue(_blockSize); - for (var i = 0; i < _testObjects.LongLength; i++) + var data = TestObjects; + var dataCount = TestObjectCount; + var list = new RecyclableQueue(BlockSize); + for (var i = 0; i < dataCount; i++) { - list.Enqueue(_testObjects[i]); + list.Enqueue(data[i]); } - foreach (var node in list) + while (list.LongCount > 0) { - DoNothing(node); + DoNothing(list.Dequeue()); } list.Clear(); @@ -100,15 +143,17 @@ namespace Recyclable.Collections.Benchmarks //[Benchmark] public void Queue() { - var list = new Queue(); - for (var i = 0; i < _testObjects.LongLength; i++) + var data = TestObjects; + var dataCount = TestObjectCount; + var list = new Queue(capacity: dataCount); + for (var i = 0; i < dataCount; i++) { - list.Enqueue(_testObjects[i]); + list.Enqueue(data[i]); } - foreach (var node in list) + while (list.Count > 0) { - DoNothing(node); + DoNothing(list.Dequeue()); } list.Clear(); @@ -117,16 +162,17 @@ namespace Recyclable.Collections.Benchmarks //[Benchmark] public void PriorityQueue() { - var list = new PriorityQueue(); - for (var i = 0; i < _testObjects.LongLength; i++) + var data = TestObjects; + var dataCount = TestObjectCount; + var list = new PriorityQueue(initialCapacity: dataCount); + for (var i = 0; i < dataCount; i++) { - list.Enqueue(_testObjects[i], i); + list.Enqueue(data[i], i); } while (list.Count > 0) { - var node = list.Dequeue(); - DoNothing(node); + DoNothing(list.Dequeue()); } list.Clear(); @@ -135,18 +181,20 @@ namespace Recyclable.Collections.Benchmarks //[Benchmark] public void RecyclableSortedList_InUpdateMode() { - var list = new RecyclableSortedList(_blockSize); + var data = TestObjects; + var dataCount = TestObjectCount; + var list = new RecyclableSortedList(BlockSize); list.BeginUpdate(); - for (var i = 0; i < _testObjects.LongLength; i++) + for (var i = 0; i < dataCount; i++) { - list.Add(i, _testObjects[i]); + list.Add(i, data[i]); } list.EndUpdate(); for (var i = 0; i < list.LongCount; i++) { - DoNothing(_testObjects[i]); + DoNothing(list[i]); } list.Clear(); @@ -155,15 +203,17 @@ namespace Recyclable.Collections.Benchmarks //[Benchmark] public void RecyclableSortedList() { - var list = new RecyclableSortedList(_blockSize); - for (var i = 0; i < _testObjects.LongLength; i++) + var data = TestObjects; + var dataCount = TestObjectCount; + var list = new RecyclableSortedList(BlockSize); + for (var i = 0; i < dataCount; i++) { - list.Add(i, _testObjects[i]); + list.Add(i, data[i]); } for (var i = 0; i < list.LongCount; i++) { - DoNothing(_testObjects[i]); + DoNothing(list[i]); } list.Clear(); @@ -172,15 +222,17 @@ namespace Recyclable.Collections.Benchmarks //[Benchmark] public void SortedList() { + var data = TestObjects; + var dataCount = TestObjectCount; var list = new SortedList(); - for (var i = 0; i < _testObjects.LongLength; i++) + for (var i = 0; i < dataCount; i++) { - list.Add(i, _testObjects[i]); + list.Add(i, data[i]); } for (var i = 0; i < list.Count; i++) { - DoNothing(_testObjects[i]); + DoNothing(list[i]); } list.Clear(); @@ -189,15 +241,17 @@ namespace Recyclable.Collections.Benchmarks //[Benchmark] public void RecyclableStack() { - var list = new RecyclableStack(_blockSize); - for (var i = 0; i < _testObjects.LongLength; i++) + var data = TestObjects; + var dataCount = TestObjectCount; + var list = new RecyclableStack(BlockSize); + for (var i = 0; i < dataCount; i++) { - list.Push(_testObjects[i]); + list.Push(data[i]); } - foreach (var node in list) + while (list.LongCount > 0) { - DoNothing(node); + DoNothing(list.Pop()); } list.Clear(); @@ -206,15 +260,17 @@ namespace Recyclable.Collections.Benchmarks //[Benchmark] public void Stack() { + var data = TestObjects; + var dataCount = TestObjectCount; var list = new Stack(); - for (var i = 0; i < _testObjects.LongLength; i++) + for (var i = 0; i < dataCount; i++) { - list.Push(_testObjects[i]); + list.Push(data[i]); } - foreach (var node in list) + while (list.Count > 0) { - DoNothing(node); + DoNothing(list.Pop()); } list.Clear(); diff --git a/Recyclable.Collections.Benchmarks/Program.cs b/Recyclable.Collections.Benchmarks/Program.cs index 68c401c..e35ca2b 100644 --- a/Recyclable.Collections.Benchmarks/Program.cs +++ b/Recyclable.Collections.Benchmarks/Program.cs @@ -5,4 +5,4 @@ BenchmarkRunner.Run(); //BenchmarkRunner.Run(); //var tests = new ListVsLinkedListBenchmark(); -//tests.RecyclableList(); \ No newline at end of file +//tests.RecyclableBlockList(); diff --git a/Recyclable.Collections/ListExtensions.cs b/Recyclable.Collections/ListExtensions.cs index ab8dafa..58f034b 100644 --- a/Recyclable.Collections/ListExtensions.cs +++ b/Recyclable.Collections/ListExtensions.cs @@ -6,6 +6,79 @@ namespace Recyclable.Collections // It was only beautified for .Net 6.0+. public static class ListExtensions { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Contains(this RecyclableBlockList arrays, T item) => arrays.Any(x => x.Contains(item)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CopyTo(this RecyclableBlockList arrays, long startingIndex, int blockSize, int lastBlockSize, T[] destinationArray, int destinationArrayIndex) + { + Span arrayMemory = destinationArray.AsSpan(); + arrayMemory = arrayMemory[destinationArrayIndex..]; + int startingArrayIndex = (int)(startingIndex / blockSize); + startingIndex %= blockSize; + if (arrays.Count > 0) + { + ReadOnlySpan sourceMemory = arrays[0].AsSpan(); + var maxToCopy = (int)Math.Min(blockSize - startingIndex, arrayMemory.Length); + if (arrays.Count == 1) + { + maxToCopy = (int)Math.Min(maxToCopy, lastBlockSize); + } + + sourceMemory = sourceMemory[(int)startingIndex..maxToCopy]; + sourceMemory.CopyTo(arrayMemory); + arrayMemory = arrayMemory[maxToCopy..]; + } + + for (var arrayIdx = startingArrayIndex + 1; arrayIdx < arrays.Count - 1 && arrayMemory.Length > 0; arrayIdx++) + { + ReadOnlySpan sourceMemory = arrays[arrayIdx].AsSpan(); + var maxToCopy = Math.Min(blockSize, arrayMemory.Length); + sourceMemory = sourceMemory[..maxToCopy]; + sourceMemory.CopyTo(arrayMemory); + arrayMemory = arrayMemory[maxToCopy..]; + } + + if (arrays.Count > 1 && arrayMemory.Length > 0) + { + ReadOnlySpan sourceMemory = arrays[^1].AsSpan(); + var maxToCopy = Math.Min(lastBlockSize, arrayMemory.Length); + sourceMemory = sourceMemory[..maxToCopy]; + sourceMemory.CopyTo(arrayMemory); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEnumerable Enumerate(this RecyclableBlockList arrays, int chunkSize, long totalCount) + { + long currentCount = 0; + for (var arrayIdx = 0; (arrayIdx < arrays.Count) && (currentCount < totalCount); arrayIdx++) + { + var array = arrays[arrayIdx]; + currentCount += Math.Min(chunkSize, totalCount); + switch (currentCount < totalCount) + { + case true: + for (var valueIdx = 0; valueIdx < chunkSize; valueIdx++) + { + yield return array[valueIdx]; + } + + break; + + case false: + var partialCount = (int)(totalCount % chunkSize); + int maxCount = partialCount > 0 ? partialCount : chunkSize; + for (var valueIdx = 0; valueIdx < maxCount; valueIdx++) + { + yield return array[valueIdx]; + } + + break; + } + } + } + public static void InsertionSort(this IList values, int startIndex, int endIndex, IComparer comparer) { var left = startIndex; @@ -29,6 +102,26 @@ namespace Recyclable.Collections { values.InsertionSort(Comparer.Default); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long LongIndexOf(this RecyclableBlockList arrays, int blockSize, T item, IEqualityComparer comparer) + { + for (var arrayIdx = 0; arrayIdx < arrays.Count; arrayIdx++) + { + var array = arrays[arrayIdx]; + Span arrayMemory = array.AsSpan(); + for (int memoryIdx = 0; memoryIdx < arrayMemory.Length; memoryIdx++) + { + if (comparer.Equals(arrayMemory[memoryIdx], item)) + { + return (arrayIdx * (long)blockSize) + memoryIdx; + } + } + } + + return -1; + } + public static void QuickSort(this IList values, int startIndex, int endIndex, IComparer comparer, IRandomNumberGenerator randomNumberGenerator) { var range = (startIndex, endIndex); diff --git a/Recyclable.Collections/Recyclable.Collections.csproj b/Recyclable.Collections/Recyclable.Collections.csproj index b2a40c8..c7aa76b 100644 --- a/Recyclable.Collections/Recyclable.Collections.csproj +++ b/Recyclable.Collections/Recyclable.Collections.csproj @@ -7,7 +7,7 @@ enable true Recyclable.Collections - 0.0.2 + 0.0.3 Marek Lemańczyk N/A Recyclable.Collections diff --git a/Recyclable.Collections/RecyclableBlockList.cs b/Recyclable.Collections/RecyclableBlockList.cs new file mode 100644 index 0000000..e2769fd --- /dev/null +++ b/Recyclable.Collections/RecyclableBlockList.cs @@ -0,0 +1,334 @@ +using System.Collections; +using System.Runtime.CompilerServices; + +namespace Recyclable.Collections +{ + public class RecyclableBlockList : IEnumerable, IDisposable + { + private static readonly IEqualityComparer _equalityComparer = EqualityComparer.Default; + private bool _disposedValue; + + + private void CopyItems(long startingIndex, long count, ref T[] destArray, long offset = 1) + { + for (long destItemIndex = startingIndex, sourceItemIndex = offset; destItemIndex < startingIndex + count; destItemIndex++, sourceItemIndex++) + { + destArray[destItemIndex] = _memory[sourceItemIndex]; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T GetItem(in T[] arrays, in long index) => arrays[index]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InternalRemoveAt(long index) + { + long oldCount = LongCount; + long oldCountMinus1 = oldCount - 1; + if (index == oldCountMinus1) + { + LongCount--; + return; + } + + if (index >= oldCount) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + CopyItems(index, oldCountMinus1, ref _memory); + + LongCount--; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long LongIndexOf(in T[] memory, in long itemCount, T? itemToFind, IEqualityComparer equalityComparer) + { + for (var itemIdx = 0; itemIdx < itemCount; itemIdx++) + { + var item = memory[itemIdx]; + if (equalityComparer.Equals(item, itemToFind)) + { + return itemIdx; + } + } + + return -1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetItem(in T[] arrays, in long index, in T value) => arrays[index] = value; + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private T[] SetNewLength(long newSize) + { + var source = _memory; + T[] newArray = newSize.RentArrayFromPool(); + if (newArray.LongLength >= source.LongLength) + { + source.CopyTo(newArray, 0); + } + else + { + CopyItems(0, newArray.LongLength, ref newArray, 0); + } + + source.ReturnToPool(); + return newArray; + } + + protected T[] _memory = Array.Empty(); + + public RecyclableBlockList(long initialCapacity = RecyclableDefaults.Capacity) + { + //EnsureCapacity(capacity); + if (_capacity < initialCapacity) + { + long newCapacity; + switch (_capacity > 0) + { + case true: + newCapacity = _capacity * 2; + while (newCapacity < initialCapacity) + { + newCapacity *= 2; + } + + break; + + case false: + newCapacity = initialCapacity; + break; + } + + _capacity = newCapacity; + _memory = SetNewLength(newCapacity); + } + } + + public RecyclableBlockList(IEnumerable source, int initialCapacity = RecyclableDefaults.Capacity) + { + //EnsureCapacity(capacity); + if (_capacity < initialCapacity) + { + long newCapacity; + switch (_capacity > 0) + { + case true: + newCapacity = _capacity * 2; + while (newCapacity < initialCapacity) + { + newCapacity *= 2; + } + + break; + + case false: + newCapacity = initialCapacity; + break; + } + + _capacity = newCapacity; + _memory = SetNewLength(newCapacity); + } + + foreach (var item in source) + { + Add(item); + } + } + + public T this[long index] + { + get => GetItem(_memory, index); + set => SetItem(_memory, index, value); + } + + public T this[int index] + { + get => GetItem(_memory, index); + set => SetItem(_memory, index, value); + } + + private long _capacity; + public long Capacity + { + get => _capacity; + set + { + if (_capacity != value) + { + _memory = SetNewLength(value); + _capacity = value; + } + } + } + public int Count => (int)LongCount; + public long LongCount { get; set; } + public bool IsReadOnly { get; } = false; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(in T item) + { + //EnsureCapacity(LongCount + 1); + long requestedCapacity = LongCount + 1; + if (_capacity < requestedCapacity) + { + long newCapacity; + switch (_capacity > 0) + { + case true: + newCapacity = _capacity * 2; + while (newCapacity < requestedCapacity) + { + newCapacity *= 2; + } + + break; + + case false: + newCapacity = requestedCapacity; + break; + } + + _capacity = newCapacity; + _memory = SetNewLength(newCapacity); + } + + _memory[LongCount] = item; + LongCount++; + } + + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + //public void EnsureCapacity(long requestedCapacity) + //{ + // if (Capacity < requestedCapacity) + // { + // long newCapacity; + // switch (Capacity > 0) + // { + // case true: + // newCapacity = Capacity * 2; + // while (newCapacity < requestedCapacity) + // { + // newCapacity *= 2; + // } + + // break; + + // case false: + // newCapacity = requestedCapacity; + // break; + // } + + // _capacity = newCapacity; + // _memory = SetNewLength(newCapacity); + // } + //} + + public void Clear() + { + LongCount = 0; + } + + public bool Contains(T item) => _memory.Contains(item); + + public void CopyTo(T[] array, int arrayIndex) => _memory.CopyTo(array, arrayIndex); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IEnumerable Enumerate() + { + for (var i = 0; i < LongCount; i++) + { + yield return _memory[i]; + } + } + + public int IndexOf(T item) => (int)LongIndexOf(_memory, LongCount, item, _equalityComparer); + + public void Insert(int index, T item) + { + long requestedCapacity = LongCount + 1; + if (Capacity < requestedCapacity) + { + long newCapacity; + switch (Capacity > 0) + { + case true: + newCapacity = Capacity * 2; + while (newCapacity < requestedCapacity) + { + newCapacity *= 2; + } + + break; + + case false: + newCapacity = requestedCapacity; + break; + } + + _capacity = newCapacity; + _memory = SetNewLength(newCapacity); + } + + for (var toMoveIdx = LongCount - 1; toMoveIdx >= index; toMoveIdx--) + { + _memory[toMoveIdx + 1] = _memory[toMoveIdx]; + } + + _memory[index] = item; + LongCount++; + } + + public long LongIndexOf(T item) => LongIndexOf(_memory, LongCount, item, _equalityComparer); + + public bool Remove(T item) + { + var itemIdx = LongIndexOf(item); + if (itemIdx >= 0) + { + RemoveAt(itemIdx); + return true; + } + + return false; + } + + public void RemoveAt(int index) => InternalRemoveAt(index); + public void RemoveAt(long index) => InternalRemoveAt(index); + + public IEnumerator GetEnumerator() => Enumerate().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Enumerate().GetEnumerator(); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + Clear(); + _memory.ReturnToPool(); + Capacity = 0; + } + + _disposedValue = true; + } + } + + // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~RecyclableList() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Recyclable.Collections/RecyclableDefaults.cs b/Recyclable.Collections/RecyclableDefaults.cs index e726503..8c9b5d1 100644 --- a/Recyclable.Collections/RecyclableDefaults.cs +++ b/Recyclable.Collections/RecyclableDefaults.cs @@ -3,5 +3,6 @@ public class RecyclableDefaults { public const int BlockSize = 10_240; + public const int Capacity = 8; } } diff --git a/Recyclable.Collections/RecyclableList.cs b/Recyclable.Collections/RecyclableList.cs index 3389241..e76f9d0 100644 --- a/Recyclable.Collections/RecyclableList.cs +++ b/Recyclable.Collections/RecyclableList.cs @@ -7,17 +7,38 @@ namespace Recyclable.Collections { private static readonly IEqualityComparer _equalityComparer = EqualityComparer.Default; - private readonly List _arrays = new(); + private readonly RecyclableBlockList _memoryBlocks; private bool _disposedValue; - public RecyclableList(int blockSize = RecyclableDefaults.BlockSize) + public RecyclableList(int blockSize = RecyclableDefaults.BlockSize, long? initialCapacity = default) { BlockSize = blockSize; + initialCapacity ??= 8 * BlockSize; + int additionalArray = initialCapacity % BlockSize > 0 ? 1 : 0; + long targetCapacity = (initialCapacity.Value / BlockSize) + additionalArray; + _memoryBlocks = new(targetCapacity); + Capacity = initialCapacity.Value; + for (var blockIdx = 0; blockIdx < targetCapacity; blockIdx++) + { + _memoryBlocks.Add(BlockSize.RentArrayFromPool()); + } + + //MemoryBlocks.LongCount = MemoryBlocks.Capacity; } - public RecyclableList(IEnumerable source, int blockSize = RecyclableDefaults.BlockSize) + public RecyclableList(IEnumerable source, int blockSize = RecyclableDefaults.BlockSize, long? initialCapacity = default) { BlockSize = blockSize; + initialCapacity ??= 8 * BlockSize; + int additionalCapacity = initialCapacity % BlockSize > 0 ? 1 : 0; + long targetCapacity = (initialCapacity.Value / BlockSize) + additionalCapacity; + _memoryBlocks = new(targetCapacity); + Capacity = initialCapacity.Value; + for (var blockIdx = 0; blockIdx < targetCapacity; blockIdx++) + { + _memoryBlocks.Add(BlockSize.RentArrayFromPool()); + } + foreach (var item in source) { Add(item); @@ -27,42 +48,47 @@ namespace Recyclable.Collections public int BlockSize { get; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetItem(List arrays, in int blockSize, in long index, in T value) + public static void SetItem(RecyclableBlockList arrays, in int blockSize, in long index, in T value) => arrays[(int)(index / blockSize)][index % blockSize] = value; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T GetItem(List arrays, in int blockSize, in long index) + public static T GetItem(RecyclableBlockList arrays, in int blockSize, in long index) => arrays[(int)(index / blockSize)][index % blockSize]; public T this[long index] { - get => GetItem(_arrays, BlockSize, index); - set => SetItem(_arrays, BlockSize, index, value); + //get => GetItem(MemoryBlocks, BlockSize, index); + //set => SetItem(MemoryBlocks, BlockSize, index, value); + get => _memoryBlocks[(int)(index / BlockSize)][index % BlockSize]; + set => _memoryBlocks[(int)(index / BlockSize)][index % BlockSize] = value; } public T this[int index] { - get => GetItem(_arrays, BlockSize, index); - set => SetItem(_arrays, BlockSize, index, value); + //get => GetItem(MemoryBlocks, BlockSize, index); + //set => SetItem(MemoryBlocks, BlockSize, index, value); + get => _memoryBlocks[(int)(index / BlockSize)][index % BlockSize]; + set => _memoryBlocks[(int)(index / BlockSize)][index % BlockSize] = value; } public long Capacity { get; set; } public int Count => (int)LongCount; public long LongCount { get; set; } public bool IsReadOnly { get; } = false; - public int BlockCount => _arrays.Count; + public long BlockCount => _memoryBlocks.LongCount; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(T item) { if (LongCount == Capacity) { - T[] newArray = BlockSize.RentArrayFromPool(); - _arrays.Add(newArray); + T[] newMemoryBlock = BlockSize.RentArrayFromPool(); + _memoryBlocks.Add(newMemoryBlock); Capacity += BlockSize; } - this[LongCount] = item; + SetItem(_memoryBlocks, BlockSize, LongCount, item); + //_arrays[_arrays.LongCount - 1][LongCount % BlockSize] = item; LongCount++; } @@ -71,11 +97,11 @@ namespace Recyclable.Collections try { // Remove in reversed order for performance savings - while (_arrays.Count > 0) + while (_memoryBlocks.Count > 0) { try { - RemoveBlock(_arrays.Count - 1); + RemoveBlock(_memoryBlocks.Count - 1); } catch (Exception) { @@ -87,29 +113,47 @@ namespace Recyclable.Collections finally { LongCount = 0; - _arrays.Clear(); + _memoryBlocks.Clear(); } } - public bool Contains(T item) => _arrays.Contains(item); + public bool Contains(T item) => _memoryBlocks.Contains(item); - public void CopyTo(T[] array, int arrayIndex) => _arrays.CopyTo(0, BlockSize, (int)LongCount % BlockSize, array, arrayIndex); + public void CopyTo(T[] array, int arrayIndex) => _memoryBlocks.CopyTo(0, BlockSize, (int)LongCount % BlockSize, array, arrayIndex); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IEnumerator GetEnumerator() => _arrays.Enumerate(BlockSize, LongCount).GetEnumerator(); + public IEnumerator GetEnumerator() => _memoryBlocks.Enumerate(BlockSize, LongCount).GetEnumerator(); - public int IndexOf(T item) => (int)_arrays.LongIndexOf(BlockSize, item, _equalityComparer); + public int IndexOf(T item) => (int)_memoryBlocks.LongIndexOf(BlockSize, item, _equalityComparer); public void Insert(int index, T item) => throw new NotSupportedException(); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long LongIndexOf(T item) => _arrays.LongIndexOf(BlockSize, item, _equalityComparer); + public long LongIndexOf(T item) => _memoryBlocks.LongIndexOf(BlockSize, item, _equalityComparer); public bool Remove(T item) => throw new NotSupportedException(); - public void RemoveAt(int index) => throw new NotSupportedException(); + public void RemoveAt(int index) => RemoveAt(this, index); + public void RemoveAt(long index) => RemoveAt(this, index); - IEnumerator IEnumerable.GetEnumerator() => _arrays.Enumerate(BlockSize, LongCount).GetEnumerator(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void RemoveAt(RecyclableList list, long index) + { + if (index == list.LongCount - 1) + { + list.LongCount--; + if (list.Capacity - list.LongCount == list.BlockSize) + { + list.RemoveBlock(list.BlockCount - 1); + } + } + else + { + throw new NotSupportedException("Only removal of the last element is supported"); + } + } + + IEnumerator IEnumerable.GetEnumerator() => _memoryBlocks.Enumerate(BlockSize, LongCount).GetEnumerator(); protected virtual void Dispose(bool disposing) { @@ -138,12 +182,12 @@ namespace Recyclable.Collections GC.SuppressFinalize(this); } - public void RemoveBlock(int index) + public void RemoveBlock(long index) { try { - _arrays[index].ReturnToPool(); - _arrays.RemoveAt(index); + _memoryBlocks[index].ReturnToPool(); + _memoryBlocks.RemoveAt(index); } finally { diff --git a/Recyclable.Collections/RecyclableListExtensions.cs b/Recyclable.Collections/RecyclableListExtensions.cs index 3a4e145..dbcde99 100644 --- a/Recyclable.Collections/RecyclableListExtensions.cs +++ b/Recyclable.Collections/RecyclableListExtensions.cs @@ -1,12 +1,15 @@ using System.Buffers; +using System.Buffers; using System.Runtime.CompilerServices; namespace Recyclable.Collections { public static class RecyclableListExtensions { + private const int _minPooledArraySize = 100; + [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CopyTo(this List arrays, long startingIndex, int blockSize, int lastBlockSize, T[] destinationArray, int destinationArrayIndex) + public static void CopyTo(this RecyclableList arrays, long startingIndex, int blockSize, int lastBlockSize, T[] destinationArray, int destinationArrayIndex) { Span arrayMemory = destinationArray.AsSpan(); arrayMemory = arrayMemory[destinationArrayIndex..]; @@ -45,10 +48,10 @@ namespace Recyclable.Collections } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Contains(this List arrays, T item) => arrays.Any(x => x.Contains(item)); + public static bool Contains(this RecyclableList arrays, T item) => arrays.Any(x => x.Contains(item)); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEnumerable Enumerate(this List arrays, int chunkSize, long totalCount) + public static IEnumerable Enumerate(this RecyclableList arrays, int chunkSize, long totalCount) { long currentCount = 0; for (var arrayIdx = 0; arrayIdx < arrays.Count; arrayIdx++) @@ -83,7 +86,7 @@ namespace Recyclable.Collections => comparer.Compare(value, maxValue) > 0 ? maxValue : value; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static long LongIndexOf(this List arrays, int blockSize, T item, IEqualityComparer comparer) + public static long LongIndexOf(this RecyclableList arrays, int blockSize, T item, IEqualityComparer comparer) { for (var arrayIdx = 0; arrayIdx < arrays.Count; arrayIdx++) { @@ -101,12 +104,6 @@ namespace Recyclable.Collections return -1; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int ToArrayIndex(this long index, int blockSize) => (int)(index / blockSize); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static long ToItemIndex(this long index, int blockSize) => index % blockSize; - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static RecyclableList ToRecyclableList(this IList values, int blockSize = RecyclableDefaults.BlockSize) => new(values, blockSize); @@ -114,18 +111,27 @@ namespace Recyclable.Collections public static RecyclableList ToRecyclableList(this IEnumerable values, int blockSize = RecyclableDefaults.BlockSize) => new(values, blockSize); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T[] RentArrayFromPool(this int minSize) => ArrayPool.Shared.Rent(minSize); + public static T[] RentArrayFromPool(this int minSize) => RentArrayFromPool((long)minSize); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ReturnToPool(this T[] array) => ArrayPool.Shared.Return(array); + public static T[] RentArrayFromPool(this long minSize) + { + return minSize switch + { + < _minPooledArraySize => new T[minSize], + <= int.MaxValue => ArrayPool.Shared.Rent((int)minSize), + _ => new T[minSize] + }; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetItem(this List arrays, in int blockSize, in long index, in T value) - => arrays[(int)(index / blockSize)][index % blockSize] = value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T GetItem(this List arrays, in int blockSize, in long index) - => arrays[(int)(index / blockSize)][index % blockSize]; - + public static void ReturnToPool(this T[] array) + { + long arrayLength = array.LongLength; + if (arrayLength is >= _minPooledArraySize and <= int.MaxValue) + { + ArrayPool.Shared.Return(array); + } + } } } diff --git a/Recyclable.Collections/RecyclableQueue.cs b/Recyclable.Collections/RecyclableQueue.cs index 46b69eb..0a70082 100644 --- a/Recyclable.Collections/RecyclableQueue.cs +++ b/Recyclable.Collections/RecyclableQueue.cs @@ -1,5 +1,4 @@ -using System.Buffers; -using System.Collections; +using System.Collections; using System.Runtime.CompilerServices; namespace Recyclable.Collections @@ -7,25 +6,55 @@ namespace Recyclable.Collections public class RecyclableQueue : IRecyclableOwner, IList, IDisposable { private static readonly IEqualityComparer _equalityComparer = EqualityComparer.Default; - private static readonly IComparer _comparer = Comparer.Default; private bool _disposedValue; + private RecyclableBlockList Memory { get; } - protected List Arrays { get; } - protected long GetAbsoluteIndex(long index) => index + RemovedCount; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T Dequeue(RecyclableQueue queue) + { + if (queue.LongCount <= 0) + { + throw new ArgumentOutOfRangeException(nameof(queue), "There are no items in the queue"); + } + + var toRemove = queue[0]; + queue.RemovedCount++; + queue.LongCount--; + if (queue.Capacity - queue.LongCount == queue.BlockSize) + { + queue.RemoveBlock(0); + queue.RemovedCount -= queue.BlockSize; + } + + return toRemove; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryDequeue(RecyclableQueue queue, out T? item) + { + if (queue.LongCount > 0) + { + item = queue.Dequeue(); + return true; + } + + item = default; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static long GetAbsoluteIndex(long index, long removedCount) => index + removedCount; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected long GetRelativeIndex(long index) => index - RemovedCount; + protected long RemovedCount { get; set; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static T[] RentArrayFromPool(int minSize) => ArrayPool.Shared.Rent(minSize); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ReturnToPool(T[] array) => ArrayPool.Shared.Return(array); - public RecyclableQueue(IEnumerable source, int blockSize = RecyclableDefaults.BlockSize) { BlockSize = blockSize; - Arrays = new(); + Memory = new(1); foreach (var item in source) { Enqueue(item); @@ -35,7 +64,7 @@ namespace Recyclable.Collections public RecyclableQueue(int blockSize = RecyclableDefaults.BlockSize) { BlockSize = blockSize; - Arrays = new(); + Memory = new(1); } public int BlockSize { get; protected set; } @@ -43,31 +72,47 @@ namespace Recyclable.Collections public int Count { - get => (int)LongCount.LimitTo(int.MaxValue, _comparer); + get => (int)LongCount; set => LongCount = value; } public long LongCount { get; set; } public bool IsReadOnly { get; } = false; - public T this[int index] { get => this[(long)index]; set => this[(long)index] = value; } + public T this[int index] + { + get => GetItem(index); + set => SetItem(index, value); + } + public T this[long index] { - get - { - long absoluteIndex = GetAbsoluteIndex(index); - int arrayIndex = absoluteIndex.ToArrayIndex(BlockSize); - long itemIndex = absoluteIndex.ToItemIndex(BlockSize); - return Arrays[arrayIndex][itemIndex]; - } + get => GetItem(index); + set => SetItem(index, value); + } - set - { - long absoluteIndex = GetAbsoluteIndex(index); - int arrayIndex = absoluteIndex.ToArrayIndex(BlockSize); - long itemIndex = absoluteIndex.ToItemIndex(BlockSize); - Arrays[arrayIndex][itemIndex] = value; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ToArrayIndex(long index, int blockSize) => (int)(index / blockSize); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long ToItemIndex(long index, int blockSize) => index % blockSize; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetItem(long index, T value) + { + long absoluteIndex = GetAbsoluteIndex(index, RemovedCount); + int arrayIndex = ToArrayIndex(absoluteIndex, BlockSize); + long itemIndex = ToItemIndex(absoluteIndex, BlockSize); + Memory[arrayIndex][itemIndex] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private T GetItem(long index) + { + long absoluteIndex = GetAbsoluteIndex(index, RemovedCount); + int arrayIndex = ToArrayIndex(absoluteIndex, BlockSize); + long itemIndex = ToItemIndex(absoluteIndex, BlockSize); + return Memory[arrayIndex][itemIndex]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -75,33 +120,22 @@ namespace Recyclable.Collections { if (LongCount == Capacity) { - T[] newArray = BlockSize.RentArrayFromPool(); - Arrays.Add(newArray); - Capacity += BlockSize; + AddBlock(); } - var newIndex = LongCount; - // We don't want this[newIndex] set to raise the update event, because - // the count would be wrong. We need to increase it, first. - this[newIndex] = item; + SetItem(LongCount, item); LongCount++; } - - public T Dequeue() + public void AddBlock() { - var toRemove = this[0]; - RemovedCount++; - LongCount--; - if (Capacity - LongCount == BlockSize) - { - RemoveBlock(0); - RemovedCount -= BlockSize; - } - - return toRemove; + T[] newArray = BlockSize.RentArrayFromPool(); + Memory.Add(newArray); + Capacity += BlockSize; } + public T Dequeue() => Dequeue(this); + public void Add(T item) => Enqueue(item); public void Clear() @@ -109,11 +143,11 @@ namespace Recyclable.Collections try { // Remove in reversed order for performance savings - while (Arrays.Count > 0) + while (Memory.Count > 0) { try { - RemoveBlock(Arrays.Count - 1); + RemoveBlock(Memory.Count - 1); } catch (Exception) { @@ -126,46 +160,32 @@ namespace Recyclable.Collections { LongCount = 0; RemovedCount = 0; - Arrays.Clear(); + Memory.Clear(); } } - public bool Contains(T item) => Arrays.Contains(item); - public void CopyTo(T[] array, int arrayIndex) => Arrays.CopyTo(RemovedCount, BlockSize, (int)(LongCount % BlockSize), array, arrayIndex); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IEnumerable Enumerate() => Arrays.Enumerate(BlockSize, LongCount); - public IEnumerator GetEnumerator() => Enumerate().GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public int IndexOf(T item) => (int)LongIndexOf(item).LimitTo(int.MaxValue, _comparer); - public long LongIndexOf(T item) => GetRelativeIndex(Arrays.LongIndexOf(BlockSize, item, _equalityComparer)); + public bool Contains(T item) => Memory.Contains(item); + public void CopyTo(T[] array, int arrayIndex) => Memory.CopyTo(RemovedCount, BlockSize, (int)(LongCount % BlockSize), array, arrayIndex); + public IEnumerable Enumerate() => Memory.Enumerate(BlockSize, LongCount); + public IEnumerator GetEnumerator() => Memory.Enumerate(BlockSize, LongCount).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Memory.Enumerate(BlockSize, LongCount).GetEnumerator(); + public int IndexOf(T item) => (int)LongIndexOf(item); + public long LongIndexOf(T item) => GetRelativeIndex(Memory.LongIndexOf(BlockSize, item, _equalityComparer)); public void Insert(int index, T item) => throw new NotSupportedException(); public void RemoveAt(int index) => throw new NotSupportedException(); - - public bool TryDequeue(out T? item) - { - if (LongCount > 0) - { - item = Dequeue(); - return true; - } - - item = default; - return false; - } - - public bool Remove(T item) => throw new NotSupportedException(); + public bool TryDequeue(out T? item) => TryDequeue(this, out item); + public bool Remove(T item) => TryDequeue(out var _); public void RemoveBlock(int index) { try { - ReturnToPool(Arrays[index]); - Arrays.RemoveAt(index); + Memory[index].ReturnToPool(); + Memory.RemoveAt(index); } finally { Capacity -= BlockSize; - LongCount -= BlockSize; + //LongCount -= BlockSize; } } diff --git a/Recyclable.CollectionsTests/Recyclable.CollectionsTests.csproj b/Recyclable.CollectionsTests/Recyclable.CollectionsTests.csproj index 12982dd..4a56fe7 100644 --- a/Recyclable.CollectionsTests/Recyclable.CollectionsTests.csproj +++ b/Recyclable.CollectionsTests/Recyclable.CollectionsTests.csproj @@ -11,12 +11,16 @@ - - + all runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Recyclable.CollectionsTests/RecyclableBlockListTests.cs b/Recyclable.CollectionsTests/RecyclableBlockListTests.cs new file mode 100644 index 0000000..514d0fe --- /dev/null +++ b/Recyclable.CollectionsTests/RecyclableBlockListTests.cs @@ -0,0 +1,191 @@ +using FluentAssertions; +using Recyclable.Collections; + +namespace Recyclable.CollectionsTests +{ + public class RecyclableBlockListTests + { + private readonly static IEnumerable _testData = Enumerable.Range(1, 20); + + [Fact] + public void AddCountShouldSucceed() + { + // Prepare + var testData = _testData.ToArray(); + var list = new RecyclableBlockList(); + + // Act + foreach (var item in testData) + { + list.Add(item); + _ = list.LongCount.Should().Be(item); + } + + // Validate + _ = list.Capacity.Should().Be(32); + _ = list.Should().HaveCount(testData.Length) + .And.ContainInConsecutiveOrder(testData) + .And.BeEquivalentTo(testData); + } + + [Fact] + public void InsertAtTheBeginningShouldMoveItems() + { + // Prepare + var testData = _testData.ToArray(); + var list = new RecyclableBlockList(); + + // Act + foreach (var item in testData) + { + list.Insert(0, item); + _ = list.LongCount.Should().Be(item); + } + + // Validate + _ = list.Capacity.Should().Be(32); + _ = list.Should().HaveCount(testData.Length) + .And.ContainInConsecutiveOrder(testData.Reverse()) + .And.BeEquivalentTo(testData); + } + + [Fact] + public void LongIndexOfShouldReturnCorrectIndexes() + { + // Prepare + var testData = _testData.ToArray(); + var list = new RecyclableBlockList(testData); + + // Act & Validate + foreach (var item in testData) + { + // Act + var index = list.IndexOf(item); + + // Validate + _ = index.Should().Be(item - 1); + } + } + + [Fact] + public void RemoveFromTheBeginningShouldRemoveTheCorrectItem() + { + // Prepare + var testData = _testData.ToArray(); + var list = new RecyclableBlockList(testData); + + // Act & Validate + for (var deleted = 1; deleted <= testData.LongLength; deleted++) + { + // Act + _ = list.Remove(deleted).Should().BeTrue(); + + // Validate + _ = list.Should().HaveCount(testData.Length - deleted) + .And.ContainInConsecutiveOrder(testData.Skip(deleted)) + .And.BeEquivalentTo(testData.Skip(deleted)); + } + } + + [Fact] + public void RemoveFromTheEndShouldRemoveTheCorrectItem() + { + // Prepare + var testData = _testData.ToArray(); + + // Act & Validate + var list = new RecyclableBlockList(testData); + for (var deleted = 1; deleted <= testData.LongLength; deleted++) + { + // Act + _ = list.Remove(testData[^deleted]).Should().BeTrue(); + + // Validate + _ = list.Should().HaveCount(testData.Length - deleted) + .And.ContainInConsecutiveOrder(testData.Take(testData.Length - deleted)) + .And.BeEquivalentTo(testData.Take(testData.Length - deleted)); + } + } + + [Fact] + public void RemoveAtFromTheBeginningShouldSucceed() + { + // Prepare + var testData = _testData.ToArray(); + var list = new RecyclableBlockList(testData); + + // Act & Validate + for (var deleted = 1; deleted <= testData.LongLength; deleted++) + { + // Act + list.RemoveAt(0); + + // Validate + _ = list.Should().HaveCount(testData.Length - deleted) + .And.ContainInConsecutiveOrder(testData.Skip(deleted)) + .And.BeEquivalentTo(testData.Skip(deleted)); + } + + _ = list.Should().BeEmpty(); + _ = list.Capacity.Should().Be(32); + } + + [Fact] + public void RemoveAtFromTheEndShouldSucceed() + { + // Prepare + var testData = _testData.ToArray(); + var list = new RecyclableBlockList(testData); + + // Act & Validate + for (var deleted = 1; deleted <= testData.LongLength; deleted++) + { + // Act + list.RemoveAt(list.LongCount - 1); + + // Validate + _ = list.Should().HaveCount(testData.Length - deleted) + .And.ContainInConsecutiveOrder(testData.Take(testData.Length - deleted)) + .And.BeEquivalentTo(testData.Take(testData.Length - deleted)); + } + + _ = list.Should().BeEmpty(); + _ = list.Capacity.Should().Be(32); + } + + [Fact] + public void ConstructorSourceShouldInitializeList() + { + // Prepare + var testData = _testData.ToArray(); + + // Act + var list = new RecyclableBlockList(testData); + + // Validate + _ = list.Should().HaveCount(testData.Length) + .And.ContainInConsecutiveOrder(testData) + .And.BeEquivalentTo(testData); + } + + [Fact] + public void EnumerateShouldYieldAllItemsInCorrectOrder() + { + // Prepare + var testData = _testData.ToArray(); + var list = new RecyclableBlockList(testData); + + // Act + var yieldedItems = new List(); + foreach (var item in list) + { + yieldedItems.Add(item); + } + + // Validate + _ = yieldedItems.Should().HaveCount(testData.Length) + .And.ContainInConsecutiveOrder(testData) + .And.BeEquivalentTo(testData); + } + } +} diff --git a/Recyclable.CollectionsTests/RecyclableListTests.cs b/Recyclable.CollectionsTests/RecyclableListTests.cs index 88ab0cb..43bef10 100644 --- a/Recyclable.CollectionsTests/RecyclableListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableListTests.cs @@ -3,7 +3,6 @@ using Recyclable.Collections; namespace Recyclable.CollectionsTests { - [TestClass()] public class RecyclableListTests { private const int _totalObjectCount = 5_000; @@ -11,7 +10,7 @@ namespace Recyclable.CollectionsTests private static readonly IEnumerable _testData = Enumerable.Range(1, _totalObjectCount); private static RecyclableList CreateReversedRecyclableList(IEnumerable source) => new(source); - [TestMethod] + [Fact] public void AddShouldAddItems() { // Prepare @@ -33,7 +32,7 @@ namespace Recyclable.CollectionsTests } } - [TestMethod] + [Fact] public void AddShouldAcceptDuplicates() { // Prepare @@ -51,7 +50,7 @@ namespace Recyclable.CollectionsTests _ = list.Should().BeEquivalentTo(testNumbers); } - [TestMethod] + [Fact] public void ConstructorShouldAcceptDuplicates() { // Prepare @@ -65,7 +64,7 @@ namespace Recyclable.CollectionsTests _ = list.Should().BeEquivalentTo(testNumbers); } - [TestMethod] + [Fact] public void InitializeSortClearShouldSucceed() { var expectedList = _testData; @@ -86,7 +85,7 @@ namespace Recyclable.CollectionsTests _ = list.LongCount.Should().Be(0); } - [TestMethod] + [Fact] public void CopyToShouldCopyAllItems() { // Prepare @@ -98,7 +97,7 @@ namespace Recyclable.CollectionsTests _ = actual.Should().BeEquivalentTo(_testData); } - [TestMethod] + [Fact] public void GetEnumeratorShouldYieldAllItems() { // Prepare @@ -116,7 +115,7 @@ namespace Recyclable.CollectionsTests _ = actual.Should().BeEquivalentTo(_testData); } - [TestMethod] + [Fact] public void IndexOfShouldFindTheIndexes() { // Prepare @@ -130,41 +129,37 @@ namespace Recyclable.CollectionsTests } } - [TestMethod] - [ExpectedException(typeof(NotSupportedException))] + [Fact] public void InsertShouldRaiseNotSupportedException() { // Prepare using var list = NewRecyclableList; // Act - list.Insert(0, -1); + _ = Assert.Throws(() => list.Insert(0, -1)); } - [TestMethod] - [ExpectedException(typeof(NotSupportedException))] + [Fact] public void RemoveShouldRaiseNotSupportedException() { // Prepare using var list = NewRecyclableList; // Act - bool removed = list.Remove(_testData.First()); - _ = removed.Should().BeFalse(); + _ = Assert.Throws(() => list.Remove(_testData.First())); } - [TestMethod] - [ExpectedException(typeof(NotSupportedException))] + [Fact] public void RemoveAtShouldRaiseNotSupportedException() { // Prepare using var list = NewRecyclableList; // Act - list.RemoveAt(0); + _ = Assert.Throws(() => list.RemoveAt(0)); } - [TestMethod] + [Fact] public void DisposeShouldSucceed() { // Prepare @@ -177,7 +172,7 @@ namespace Recyclable.CollectionsTests _ = list.LongCount.Should().Be(0L); } - [TestMethod] + [Fact] public void ConsecutiveDisposeShouldSucceed() { // Prepare diff --git a/Recyclable.CollectionsTests/RecyclableQueueTests.cs b/Recyclable.CollectionsTests/RecyclableQueueTests.cs index 29e3924..b09ed28 100644 --- a/Recyclable.CollectionsTests/RecyclableQueueTests.cs +++ b/Recyclable.CollectionsTests/RecyclableQueueTests.cs @@ -3,12 +3,11 @@ using Recyclable.Collections; namespace Recyclable.CollectionsTests { - [TestClass] public class RecyclableQueueTests { private static readonly string[] _testData = new[] { "a", "d", "c", "b", "a" }; - [TestMethod] + [Fact] public void ShouldNotBeSortedWhenCreated() { // Act @@ -19,7 +18,7 @@ namespace Recyclable.CollectionsTests .And.ContainInConsecutiveOrder(_testData); } - [TestMethod] + [Fact] public void ShouldBeEmptyWhenNotInitialized() { // Act @@ -29,7 +28,7 @@ namespace Recyclable.CollectionsTests _ = list.Should().BeEmpty(); } - [TestMethod] + [Fact] public void ShouldNotBeSortedWhenInitialized() { // Act @@ -48,7 +47,7 @@ namespace Recyclable.CollectionsTests .And.BeEquivalentTo(_testData); } - [TestMethod] + [Fact] public void ShouldBeEmptyAfterClear() { // Prepare @@ -61,7 +60,7 @@ namespace Recyclable.CollectionsTests _ = list.Should().BeEmpty(); } - [TestMethod] + [Fact] public void DequeueShouldReturnItemsInTheSameOrder() { // Prepare @@ -80,11 +79,11 @@ namespace Recyclable.CollectionsTests } // Validate - _ = dequeuedItems.Should().ContainInConsecutiveOrder(_testData); + _ = dequeuedItems.Should().ContainInConsecutiveOrder(_testData) + .And.BeEquivalentTo(_testData); } - [TestMethod] - [ExpectedException(typeof(ArgumentOutOfRangeException))] + [Fact] public void DequeueShoudRaiseArgumentOutOfRangeWhenNoMoreElementsFound() { // Prepare @@ -96,10 +95,11 @@ namespace Recyclable.CollectionsTests _ = list.Dequeue(); } - _ = list.Dequeue(); + _ = Assert.Throws(() => list.Dequeue()); + _ = list.LongCount.Should().Be(0); } - [TestMethod] + [Fact] public void GetEnumeratorShouldYieldAllItemsInTheSameOrder() { // Prepare @@ -114,7 +114,8 @@ namespace Recyclable.CollectionsTests } // Validate - _ = actual.Should().ContainInConsecutiveOrder(_testData); + _ = actual.Should().ContainInConsecutiveOrder(_testData) + .And.BeEquivalentTo(_testData); } } } diff --git a/Recyclable.CollectionsTests/RecyclableSortedListTests.cs b/Recyclable.CollectionsTests/RecyclableSortedListTests.cs index 82aecb9..6f72aa2 100644 --- a/Recyclable.CollectionsTests/RecyclableSortedListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableSortedListTests.cs @@ -3,12 +3,11 @@ using Recyclable.Collections; namespace Recyclable.CollectionsTests { - [TestClass()] public class RecyclableSortedListTests { private static readonly (int, string)[] _testData = new[] { (5, "a"), (4, "d"), (3, "c"), (2, "b"), (1, "a") }; - [TestMethod] + [Fact] public void ShouldBeSortedWhenCreated() { // Act @@ -20,7 +19,7 @@ namespace Recyclable.CollectionsTests .And.BeInAscendingOrder((item) => item.Key); } - [TestMethod] + [Fact] public void ShouldBeEmptyWhenNotInitialized() { // Act @@ -30,7 +29,7 @@ namespace Recyclable.CollectionsTests _ = sortedList.Should().BeEmpty(); } - [TestMethod] + [Fact] public void ShouldNotBeSortedWhenInitialized() { // Act @@ -48,7 +47,7 @@ namespace Recyclable.CollectionsTests .And.ContainInConsecutiveOrder(_testData); } - [TestMethod] + [Fact] public void ShouldBeSortedWhenUpdateEnds() { // Prepare @@ -71,7 +70,7 @@ namespace Recyclable.CollectionsTests _ = sortedList.Should().BeInAscendingOrder(x => x.Key); } - [TestMethod] + [Fact] public void ShouldBeEmptyAfterClear() { // Prepare diff --git a/Recyclable.CollectionsTests/RecyclableStackTests.cs b/Recyclable.CollectionsTests/RecyclableStackTests.cs index 0946615..9507043 100644 --- a/Recyclable.CollectionsTests/RecyclableStackTests.cs +++ b/Recyclable.CollectionsTests/RecyclableStackTests.cs @@ -3,12 +3,11 @@ using Recyclable.Collections; namespace Recyclable.CollectionsTests { - [TestClass] public class RecyclableStackTests { private static readonly string[] _testData = new[] { "a", "d", "c", "b", "a" }; - [TestMethod] + [Fact] public void GetEnumeratorShouldYieldAllItemsInReversedOrder() { // Prepare @@ -26,7 +25,7 @@ namespace Recyclable.CollectionsTests _ = actual.Should().ContainInConsecutiveOrder(_testData.Reverse()); } - [TestMethod] + [Fact] public void ShouldNotBeSortedWhenCreated() { // Act @@ -37,7 +36,7 @@ namespace Recyclable.CollectionsTests .And.ContainInConsecutiveOrder(_testData); } - [TestMethod] + [Fact] public void ShouldBeEmptyWhenNotInitialized() { // Act @@ -47,7 +46,7 @@ namespace Recyclable.CollectionsTests _ = sortedList.Should().BeEmpty(); } - [TestMethod] + [Fact] public void ShouldNotBeSortedWhenInitialized() { // Act @@ -66,7 +65,7 @@ namespace Recyclable.CollectionsTests .And.BeEquivalentTo(_testData); } - [TestMethod] + [Fact] public void ShouldBeEmptyAfterClear() { // Prepare @@ -79,7 +78,7 @@ namespace Recyclable.CollectionsTests _ = sortedList.Should().BeEmpty(); } - [TestMethod] + [Fact] public void PopShouldReturnItemsInReversedOrder() { // Prepare @@ -101,8 +100,7 @@ namespace Recyclable.CollectionsTests _ = dequeuedItems.Should().ContainInConsecutiveOrder(_testData.Reverse()); } - [TestMethod] - [ExpectedException(typeof(ArgumentOutOfRangeException))] + [Fact] public void PopShoudRaiseArgumentOutOfRangeWhenNoMoreElementsFound() { // Prepare @@ -114,7 +112,7 @@ namespace Recyclable.CollectionsTests _ = list.Dequeue(); } - _ = list.Dequeue(); + _ = Assert.Throws(() => list.Dequeue()); } } } diff --git a/Recyclable.CollectionsTests/Usings.cs b/Recyclable.CollectionsTests/Usings.cs index ab67c7e..8c927eb 100644 --- a/Recyclable.CollectionsTests/Usings.cs +++ b/Recyclable.CollectionsTests/Usings.cs @@ -1 +1 @@ -global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file +global using Xunit; \ No newline at end of file