diff --git a/README.md b/README.md index 973068b..cad1ada 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,12 @@ This is the direct equivalent of `List` class, except that the arrays are tak The architecture of the classes included in this project is driven primarily by performance & memory footprints. The following decisions were made to support it. ## Code duplication -There is high level of code duplication in each of the classes. When you look at them, you will see they are alike & some of the code could be extracted to extension methods. It has been done & has proved to perform worse. For this reason, properties & methods expected much of the code is duplicated to get the most out of it. +There is high level of code duplication in each of the classes. When you look at them, you will see they are alike & some of the code could be extracted to extension methods. It has been done & has proven to perform worse. For this reason, properties & methods expected to be on the hot-path, duplicate much of the code to get the most out of it. -## Common base `ILongList` -Having a common base class for all the classes looks very attractive. But it is also proved to perform worse, if you start making properties & methods virtual & there are subsequent calls to `base`. For this reason, each of the classes is the root class & there are hardly any virtual methods & properties. All classes are meant to be `final`, but I've not marked them as `sealed`. If you feel that you'd benefit from it, you're welcomed to inherit from them. +## Common base - `ILongList` +Having a common base class for all the classes looks very attractive. But it is also proved to perform worse, if you start making properties & methods virtual & there are subsequent calls to `base`. For this reason, each of the classes is the root class & there are hardly any virtual methods & properties. All classes are meant to be `final`, but I've not marked them as `sealed`. If you feel that you'd benefit from it, you're welcomed to inherit from them. The protected stuff is there for your use. -If you desire to have a common base for calls, each of the classes implements `IList` & `ILongList` interfaces. `ILongList` is a new interface, made alike `IList`, but unlocking full support for `long` indexing & use. Considering the characteristics of the classes I feel that the generic name is justified, instead of `IRecyclableList` what you could expect. +If you desire to have a common base for calls, each of the classes implements `IList` & `ILongList` interfaces. `ILongList` is a new interface, made alike `IList`, but unlocking full support for `long` indexing & use. Considering the characteristics of the classes I feel that the generic name is justified, instead of `IRecyclableList` what you could expect. ## Protection against inccorrect use & state Having protection against invalid state of the object is an important part of software engineering. The classes included in this project provide the same level of protection as the regular `array` & `List` class. They will raise exceptions when invalid index is given. But there is no additional protection that the content & state of the objects internally used by these classes are valid. You can break them, if you want to. @@ -62,19 +62,32 @@ This is by design, for the best performance. Initially there have been checks in If you foresee the overflowing as an issue, you're welcomed to propose an improvement & file a PR with proposed solution. +# Auto-properties & new language features +When you look at the code you may note, that it looks like in early c# days. Where the new language features yield the same or better performance, they are & will be used. + +However, many of the new language features have proved to provide worse performance at some point in my testing. It includes auto-properties, for which I could see getters & setters generated in IL, instead of fields. Because all the classes are expected to be on the hot-paths, I've eliminated most of them. + +If you know & can show it in your benchmarks that the code can be simplified without impacting the performance, feel free to file a PR with proposed changes. Please include benchmark results from your testing to speed up things. + # Thread Safety The `public static` members of the classes are thread safe. Any instance members are not guaraneteed to be thread safe. You need to implement locking mechanism to safely use the classes in a multi-threading environment. # Use All the classes included in this package are meant to be direct replacements of their corresponding system classes. +* `IList` ⇨ `ILongList` * `List` ⇨ `RecyclableList` -* `SortedList` ⇨ `RecyclableSortedList` * `Queue` ⇨ `RecyclableQueue` * `PriorityQueue` ⇨ `RecyclableSortedList` +* `SortedList` ⇨ `RecyclableSortedList` * `Stack` ⇨ `RecyclableStack` -* `IList` ⇨ `ILongList` Examples of each of the above classes are provided below. Please refer automated tests for more. + +## `RecyclableArrayList` +It's the closest equivalent to `List` in regards to functionality. It supports items' removal & delivers the best performance. But it's limited to `int` range, tough. If you find that acceptable, my recommendation is to use this class as the direct replacement for `List`. + +Do note, that it implements `ILongList` interface, too. This is to allow having common code base regardless, if you use the basic `RecyclableArrayList` or it's `long`-range version - `RecyclableList`. By doing that you can switch between them with highly limited changes in code. + ## `RecyclableList` ```CSharp @@ -110,4 +123,10 @@ public void RecyclableListExample() list.Clear(); } -``` \ No newline at end of file +``` +## `RecyclableArrayList` + +## `RecyclableArrayList` + +## `RecyclableArrayList` + diff --git a/Recyclable.Collections.Benchmarks/BenchmarkBase.cs b/Recyclable.Collections.Benchmarks/BenchmarkBase.cs index 3c10498..8fd7802 100644 --- a/Recyclable.Collections.Benchmarks/BenchmarkBase.cs +++ b/Recyclable.Collections.Benchmarks/BenchmarkBase.cs @@ -21,6 +21,8 @@ namespace Recyclable.Collections.Benchmarks protected RecyclableArrayList? _testObjectsAsRecyclableArrayList; protected RecyclableArrayList TestObjectsAsRecyclableArrayList => _testObjectsAsRecyclableArrayList ?? throw new NullReferenceException("Something is wrong and the field is not initialized"); + protected RecyclableList? _testObjectsAsRecyclableList; + protected RecyclableList TestObjectsAsRecyclableList => _testObjectsAsRecyclableList ?? throw new NullReferenceException("Something is wrong and the field is not initialized"); protected IEnumerable TestObjectsAsIEnumerable => Enumerable.Range(1, TestObjectCount).Select(static x => new object()); protected static void DoNothing(T obj) diff --git a/Recyclable.Collections.Benchmarks/Program.cs b/Recyclable.Collections.Benchmarks/Program.cs index a25e11f..c5afe52 100644 --- a/Recyclable.Collections.Benchmarks/Program.cs +++ b/Recyclable.Collections.Benchmarks/Program.cs @@ -1,16 +1,50 @@ -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Running; using Recyclable.Collections.Benchmarks; //BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(new[] { "-f*" }); -//BenchmarkRunner.Run(); //BenchmarkRunner.Run(); -var benchmark = new RecyclableCollectionsBenchmarks(); -benchmark.Setup(); -//benchmark.RecyclableArrayList_AddRange_WithCapacity(); -//benchmark.RecyclableArrayList_Add_WithCapacity(); -benchmark.RecyclableList_Add_WithCapacity(); + + +internal class Program +{ + void RunBenchmark() + { + var benchmark = new RecyclableCollectionsBenchmarks(); + benchmark.Setup(); + foreach (var _ in Enumerable.Range(1, 1000)) + { + //benchmark.RecyclableList_Contains(); + //benchmark.RecyclableArrayList_AddRange_WithCapacity(); + //benchmark.RecyclableArrayList_Add_WithCapacity(); + + //benchmark.RecyclableList_Create(); + //benchmark.RecyclableList_Create_WithCapacity(); + //benchmark.RecyclableList_Add(); + //benchmark.RecyclableList_Add_WithCapacity(); + } + + benchmark.Cleanup(); + } + + private static void Main(string[] args) + { + BenchmarkRunner.Run( + ManualConfig.Create(DefaultConfig.Instance).WithOptions(ConfigOptions.DisableOptimizationsValidator | ConfigOptions.JoinSummary) + ); + + //RunBenchmark(); + } +} +// benchmark.Setup(); +// benchmark.Cleanup(); + +//benchmark.Setup(); +//benchmark.Cleanup(); + +//} //BenchmarkRunner.Run(); diff --git a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.List.cs b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.List.cs index 2996574..b6cb1a4 100644 --- a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.List.cs +++ b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.List.cs @@ -111,5 +111,53 @@ namespace Recyclable.Collections.Benchmarks var data = TestObjectsAsList; DoNothing(data.Count); } + + [Benchmark(Baseline = true)] + public void List_Contains_1_000_FirstItems() + { + var data = TestObjects; + var list = TestObjectsAsList; + var dataCount = 1_000; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.Contains(data[i])); + } + } + + [Benchmark] + public void List_Contains_1_000_LastItems() + { + var data = TestObjects; + var list = TestObjectsAsList; + var dataCount = 1_000; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.Contains(data[^(i + 1)])); + } + } + + //[Benchmark(Baseline = true)] + public void List_IndexOf_1_000_FirstItems() + { + var data = TestObjects; + var list = TestObjectsAsList; + var dataCount = 1_000; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.IndexOf(data[i])); + } + } + + //[Benchmark] + public void List_IndexOf_1_000_LastItems() + { + var data = TestObjects; + var list = TestObjectsAsList; + var dataCount = 1_000; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.IndexOf(data[^(i + 1)])); + } + } } } diff --git a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableArrayList.cs b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableArrayList.cs index 49945f1..e970042 100644 --- a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableArrayList.cs +++ b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableArrayList.cs @@ -4,21 +4,21 @@ namespace Recyclable.Collections.Benchmarks { public partial class RecyclableCollectionsBenchmarks : BenchmarkBase { - [Benchmark] + //[Benchmark] public void RecyclableArrayList_Create() { using var list = new RecyclableArrayList(); DoNothing(list); } - [Benchmark] + //[Benchmark] public void RecyclableArrayList_Create_WithCapacity() { using var list = new RecyclableArrayList(TestObjectCount); DoNothing(list); } - [Benchmark] + //[Benchmark] public void RecyclableArrayList_Add() { var data = TestObjects; @@ -30,7 +30,7 @@ namespace Recyclable.Collections.Benchmarks } } - [Benchmark] + //[Benchmark] public void RecyclableArrayList_Add_WithCapacity() { var data = TestObjects; diff --git a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableList.cs b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableList.cs index e253cd5..f8fb793 100644 --- a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableList.cs +++ b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableList.cs @@ -4,21 +4,21 @@ namespace Recyclable.Collections.Benchmarks { public partial class RecyclableCollectionsBenchmarks : BenchmarkBase { - [Benchmark] + //[Benchmark] public void RecyclableList_Create() { using var list = new RecyclableList(); DoNothing(list); } - [Benchmark] + //[Benchmark] public void RecyclableList_Create_WithCapacity() { - using var list = new RecyclableList(initialCapacity: TestObjectCount); + using var list = new RecyclableList(minBlockSize: 102_400, expectedItemsCount: TestObjectCount); DoNothing(list); } - [Benchmark] + //[Benchmark] public void RecyclableList_Add() { var data = TestObjects; @@ -30,12 +30,12 @@ namespace Recyclable.Collections.Benchmarks } } - [Benchmark] + //[Benchmark] public void RecyclableList_Add_WithCapacity() { var data = TestObjects; var dataCount = TestObjectCount; - using var list = new RecyclableList(dataCount); + using var list = new RecyclableList(minBlockSize: 102_400, expectedItemsCount: dataCount); for (var i = 0; i < dataCount; i++) { list.Add(data[i]); @@ -77,5 +77,53 @@ namespace Recyclable.Collections.Benchmarks var data = _testRecyclableList; DoNothing(data.LongCount); } + + [Benchmark] + public void RecyclableList_Contains_1_000_FirstItems() + { + var data = TestObjects; + var list = TestObjectsAsRecyclableList; + var dataCount = 1000; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.Contains(data[i])); + } + } + + [Benchmark] + public void RecyclableList_Contains_1_000_LastItems() + { + var data = TestObjects; + var list = TestObjectsAsRecyclableList; + var dataCount = 1_000; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.Contains(data[^(i + 1)])); + } + } + + //[Benchmark] + public void RecyclableList_IndexOf_1_000_FirstItems() + { + var data = TestObjects; + var list = TestObjectsAsRecyclableList; + var dataCount = 1000; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.IndexOf(data[i])); + } + } + + //[Benchmark] + public void RecyclableList_IndexOf_1_000_LastItems() + { + var data = TestObjects; + var list = TestObjectsAsRecyclableList; + var dataCount = 1_000; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.IndexOf(data[^(i + 1)])); + } + } } } diff --git a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.cs b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.cs index 9ab8cd9..3c21495 100644 --- a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.cs +++ b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.cs @@ -1,9 +1,4 @@ using BenchmarkDotNet.Attributes; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Recyclable.Collections.Benchmarks { @@ -18,11 +13,12 @@ namespace Recyclable.Collections.Benchmarks { Console.WriteLine("******* GLOBAL SETUP RAISED *******"); _testObjects = Enumerable.Range(1, TestObjectCount).Select(x => new object()).ToArray(); - _testRecyclableArrayList = new(TestObjects, TestObjectCount); - _testRecyclableList = new(TestObjects, TestObjectCount); + _testRecyclableArrayList = new(TestObjects, initialCapacity: TestObjectCount); + _testRecyclableList = new(TestObjects, expectedItemsCount: TestObjectCount); _testArray = TestObjects.ToArray(); _testObjectsAsList = TestObjects.ToList(); _testObjectsAsRecyclableArrayList = TestObjects.ToRecyclableArrayList(); + _testObjectsAsRecyclableList = TestObjects.ToRecyclableList(); } [GlobalCleanup] @@ -33,6 +29,7 @@ namespace Recyclable.Collections.Benchmarks _testRecyclableList?.Dispose(); TestObjectsAsRecyclableArrayList?.Dispose(); _testObjectsAsRecyclableArrayList = default; + _testObjectsAsRecyclableList = default; _testObjectsAsList = default; _testObjects = default; } diff --git a/Recyclable.Collections/RecyclableArrayList.cs b/Recyclable.Collections/RecyclableArrayList.cs index b811fbd..581916f 100644 --- a/Recyclable.Collections/RecyclableArrayList.cs +++ b/Recyclable.Collections/RecyclableArrayList.cs @@ -21,27 +21,22 @@ namespace Recyclable.Collections { ArrayPool arrayPool = _arrayPool; var sourceLength = source?.Length ?? 0; - T[] newMemoryBlock = newSize.RentArrayFromPool(arrayPool); - switch (newSize >= sourceLength) + T[] newMemoryBlock = newSize >= RecyclableDefaults.MinPooledArrayLength + ? arrayPool.Rent(newSize) + : new T[newSize]; + + if (sourceLength > 0) { - case true: - if (sourceLength > 0) - { - Memory sourceMemory = new(source); - Memory newArrayMemory = new(newMemoryBlock); - sourceMemory.CopyTo(newArrayMemory); - source!.ReturnToPool(arrayPool); - } - - return newMemoryBlock; - - case false: - var sourceSpan = new Span(source)[..newSize]; - Span newArraySpan = new(newMemoryBlock); - sourceSpan.CopyTo(newArraySpan); - source!.ReturnToPool(arrayPool); - return newMemoryBlock; + var sourceSpan = new Memory(source); + var newArraySpan = new Memory(newMemoryBlock); + sourceSpan.CopyTo(newArraySpan); + if (sourceLength >= RecyclableDefaults.MinPooledArrayLength) + { + arrayPool.Return(source!); + } } + + return newMemoryBlock; } protected int EnsureCapacity(in int requestedCapacity) @@ -153,7 +148,7 @@ namespace Recyclable.Collections public bool IsReadOnly { get; } = false; - //[MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(T item) { int requiredCapacity = _count + 1; @@ -247,41 +242,41 @@ namespace Recyclable.Collections return; } - int capacity = _capacity; - int targetIndex = _count; + int targetItemIdx = _count; Span memorySpan; if (source.TryGetNonEnumeratedCount(out var requiredAdditionalCapacity)) { - _ = EnsureCapacity(targetIndex + requiredAdditionalCapacity); + _ = EnsureCapacity(targetItemIdx + requiredAdditionalCapacity); memorySpan = new(_memoryBlock); foreach (var item in source) { - memorySpan[targetIndex++] = item; + memorySpan[targetItemIdx++] = item; } - _count = targetIndex; + _count = targetItemIdx; return; } int i; using var enumerator = source.GetEnumerator(); + int capacity = _capacity; memorySpan = new(_memoryBlock); if (enumerator.MoveNext()) { - int available = capacity - targetIndex; + int available = capacity - targetItemIdx; while (true) { - if (targetIndex + growByCount > capacity) + if (targetItemIdx + growByCount > capacity) { capacity = EnsureCapacity(capacity + growByCount); memorySpan = new(_memoryBlock); - available = capacity - targetIndex; + available = capacity - targetItemIdx; } for (i = 0; i < available; i++) { - memorySpan[targetIndex++] = enumerator.Current; + memorySpan[targetItemIdx++] = enumerator.Current; if (!enumerator.MoveNext()) { break; @@ -295,7 +290,7 @@ namespace Recyclable.Collections } } - _count = targetIndex; + _count = targetItemIdx; } public void Clear() diff --git a/Recyclable.Collections/RecyclableDefaults.cs b/Recyclable.Collections/RecyclableDefaults.cs index b7ea187..9012a0e 100644 --- a/Recyclable.Collections/RecyclableDefaults.cs +++ b/Recyclable.Collections/RecyclableDefaults.cs @@ -2,8 +2,8 @@ { public class RecyclableDefaults { - public const int BlockSize = 10_240; + public const int BlockSize = 16_384; public const int Capacity = 1; - public const int MinPooledArrayLength = 100; + public const int MinPooledArrayLength = 128; } } diff --git a/Recyclable.Collections/RecyclableList.cs b/Recyclable.Collections/RecyclableList.cs index 7d098d3..5fecbe4 100644 --- a/Recyclable.Collections/RecyclableList.cs +++ b/Recyclable.Collections/RecyclableList.cs @@ -1,5 +1,4 @@ -using System; -using System.Buffers; +using System.Buffers; using System.Collections; using System.Runtime.CompilerServices; @@ -7,11 +6,18 @@ namespace Recyclable.Collections { public class RecyclableList : IDisposable, IList { - private static readonly ArrayPool _arrayPool = ArrayPool.Create(); - private static readonly ArrayPool _blockArrayPool = ArrayPool.Create(); - private static readonly T[][] _emptyBlockArray = new T[0][]; + private const int ItemNotFoundIndex = -1; + private static readonly ArrayPool _defaultMemoryBlocksPool = ArrayPool.Create(); + private static readonly ArrayPool _defaultBlockArrayPool = ArrayPool.Create(); + private static readonly T[][] _emptyMemoryBlocksArray = new T[0][]; + private static readonly T[] _emptyBlockArray = new T[0]; private static readonly IEqualityComparer _equalityComparer = EqualityComparer.Default; - private readonly int _blockSize; + + private int _blockSize; + private int _lastBlockIndex; + private int _nextItemIndex; + protected ArrayPool _memoryBlocksPool; + protected ArrayPool _blockArrayPool; protected T[][] _memoryBlocks; private long _capacity; @@ -21,7 +27,7 @@ namespace Recyclable.Collections protected set => _capacity = value; } - public int Count => (int)LongCount; + public int Count => (int)_longCount; protected long _longCount; public long LongCount @@ -30,13 +36,12 @@ namespace Recyclable.Collections set => _longCount = value; } - public bool IsReadOnly { get; } = false; + public bool IsReadOnly { get; } public int BlockCount => _memoryBlocks.Length; - private static void RemoveAt(RecyclableList list, long index) + private static void RemoveAt(RecyclableList list, long index, ArrayPool blockArrayPool) { - long oldCount = list._longCount; - long oldCountMinus1 = oldCount - 1; + long oldCountMinus1 = list._longCount - 1; if (index != oldCountMinus1) { ThrowArgumentOutOfRangeException(); @@ -44,10 +49,25 @@ namespace Recyclable.Collections list._longCount--; int blockSize = list._blockSize; + if (list._nextItemIndex > 0) + { + list._nextItemIndex--; + } + else + { + list._lastBlockIndex--; + list._nextItemIndex = blockSize; + } + if ((list._capacity * blockSize) - oldCountMinus1 == blockSize) { T[][] memoryBlocks = list._memoryBlocks; - ReleaseBlock(list, memoryBlocks, blockSize, memoryBlocks.Length - 1); + if (blockSize >= RecyclableDefaults.MinPooledArrayLength) + { + blockArrayPool.Return(memoryBlocks[list.BlockCount - 1]); + } + + list._capacity -= blockSize; } } @@ -57,124 +77,155 @@ namespace Recyclable.Collections } [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static T[][] SetNewLength(in T[][]? source, in int blockSize, in long newSize) + protected static T[][] SetNewLength(in T[][]? source, int minBlockSize, in long newCapacity, ArrayPool memoryBlocksPool, ArrayPool blockArrayPool) { - ArrayPool arrayPool = _arrayPool; - ArrayPool blockArrayPool = _blockArrayPool; - var sourceBlockCount = source?.Length ?? 0; - int requiredBlockCount = (int)(newSize / blockSize) + (newSize % blockSize > 0 ? 1 : 0); - if (requiredBlockCount == sourceBlockCount) + const int sourceBlockCount = 0, requiredBlockCount = 1, uninitializedBlocksCount = 2, i = 3; + + Span localInts = stackalloc int[4] { - return source!; + source?.Length ?? 0, // sourceBlockCount + (int)(newCapacity / minBlockSize) + (newCapacity % minBlockSize > 0 ? 1 : 0), // requiredBlockCount + 0, // uninitializedBlocksCount + 0, // i + }; + + T[][] newMemoryBlocks = localInts[requiredBlockCount] < RecyclableDefaults.MinPooledArrayLength + ? new T[localInts[requiredBlockCount]][] + : memoryBlocksPool.Rent(localInts[requiredBlockCount]); + + Span newMemoryBlocksSpan = new(newMemoryBlocks); + if (localInts[sourceBlockCount] > 0) + { + Span sourceSpan = new(source); + sourceSpan.CopyTo(newMemoryBlocksSpan); + if (localInts[sourceBlockCount] >= RecyclableDefaults.MinPooledArrayLength) + { + memoryBlocksPool.Return(source!); + } + + newMemoryBlocksSpan = newMemoryBlocksSpan[localInts[sourceBlockCount]..]; } - Span newArraySpan; - T[][] newMemoryBlocks = requiredBlockCount.RentArrayFromPool(arrayPool); - switch (requiredBlockCount >= sourceBlockCount) + localInts[uninitializedBlocksCount] = newMemoryBlocksSpan.Length; + if (localInts[uninitializedBlocksCount] == localInts[requiredBlockCount]) { - case true: - if (sourceBlockCount > 0) - { - Memory sourceMemory = new(source); - Memory newArrayMemory = new(newMemoryBlocks); - sourceMemory.CopyTo(newArrayMemory); - source!.ReturnToPool(arrayPool); - newArraySpan = new Span(newMemoryBlocks)[sourceBlockCount..]; - } - else - { - newArraySpan = new Span(newMemoryBlocks); - } + if (minBlockSize >= RecyclableDefaults.MinPooledArrayLength) + { + newMemoryBlocksSpan[0] = blockArrayPool.Rent(minBlockSize); + minBlockSize = newMemoryBlocksSpan[0].Length; + } + else + { + newMemoryBlocksSpan[0] = new T[minBlockSize]; + } - for (int i = 0; i < newArraySpan.Length; i++) - { - newArraySpan[i] = blockSize.RentArrayFromPool(blockArrayPool); - } - - return newMemoryBlocks; - - case false: - var sourceSpan = new Span(source)[..requiredBlockCount]; - newArraySpan = new Span(newMemoryBlocks); - sourceSpan.CopyTo(newArraySpan); - sourceSpan = new Span(source)[newArraySpan.Length..]; - for (int i = 0; i < sourceSpan.Length; i++) - { - sourceSpan[i].ReturnToPool(_blockArrayPool); - } - - source!.ReturnToPool(arrayPool); - return newMemoryBlocks; + newMemoryBlocksSpan = newMemoryBlocksSpan[1..]; + localInts[uninitializedBlocksCount]--; } + + if (minBlockSize >= RecyclableDefaults.MinPooledArrayLength) + { + for (localInts[i] = 0; localInts[i] < localInts[uninitializedBlocksCount]; localInts[i]++) + { + newMemoryBlocksSpan[localInts[i]] = blockArrayPool.Rent(minBlockSize); + } + } + else + { + for (localInts[i] = 0; localInts[i] < localInts[uninitializedBlocksCount]; localInts[i]++) + { + newMemoryBlocksSpan[localInts[i]] = new T[minBlockSize]; + } + } + + return newMemoryBlocks; } protected long EnsureCapacity(in long requestedCapacity) { - long oldCapacity = _capacity; - ref T[][] memory = ref _memoryBlocks; - + T[][] memory = _memoryBlocks; long newCapacity; - switch (oldCapacity > 0) + + if (_capacity > 0) { - case true: - newCapacity = oldCapacity; - while (newCapacity < requestedCapacity) - { - newCapacity *= 2; - } - - break; - - case false: - newCapacity = requestedCapacity; - break; + newCapacity = _capacity; + while (newCapacity < requestedCapacity) + { + newCapacity *= 2; + } + } + else + { + newCapacity = requestedCapacity; } - int blockSize = _blockSize; - memory = SetNewLength(memory, blockSize, newCapacity); - newCapacity = memory.Length * blockSize; + memory = SetNewLength(memory, _blockSize, newCapacity, _memoryBlocksPool, _blockArrayPool); + if (newCapacity > 0 && _capacity == 0) + { + _blockSize = memory[0].Length; + } + + newCapacity = memory.Length * _blockSize; _capacity = newCapacity; + _memoryBlocks = memory; return newCapacity; } -#pragma warning disable CS8618 // _memoryBlocks will be initialized when the 1st item is added - public RecyclableList(int blockSize = RecyclableDefaults.BlockSize, long? initialCapacity = default) -#pragma warning restore CS8618 + public RecyclableList(int minBlockSize = RecyclableDefaults.BlockSize, long? expectedItemsCount = default, ArrayPool? memoryBlocksPool = default, ArrayPool? blockArrayPool = default) { - _blockSize = blockSize; - if (initialCapacity > 0) + memoryBlocksPool ??= _defaultMemoryBlocksPool; + blockArrayPool ??= _defaultBlockArrayPool; + _memoryBlocksPool = memoryBlocksPool; + _blockArrayPool = blockArrayPool; + + if (expectedItemsCount > 0) { - _memoryBlocks = SetNewLength(_memoryBlocks, blockSize, initialCapacity.Value); - _capacity = _memoryBlocks.Length * blockSize; + _memoryBlocks = SetNewLength(_memoryBlocks, minBlockSize, expectedItemsCount.Value, memoryBlocksPool, blockArrayPool); + if (_memoryBlocks.Length > 0) + { + minBlockSize = _memoryBlocks[0].Length; + } + + _blockSize = minBlockSize; + _capacity = _memoryBlocks.Length * minBlockSize; } else { - _memoryBlocks = _emptyBlockArray; + _blockSize = minBlockSize; + _memoryBlocks = _emptyMemoryBlocksArray; } } -#pragma warning disable CS8618 // _memoryBlocks will be initialized when the 1st item is added - public RecyclableList(IEnumerable source, int blockSize = RecyclableDefaults.BlockSize, long? expectedItemsCount = default) -#pragma warning restore CS8618 + public RecyclableList(IEnumerable source, int minBlockSize = RecyclableDefaults.BlockSize, long? expectedItemsCount = default, ArrayPool? memoryBlocksPool = default, ArrayPool? blockArrayPool = default) { - _blockSize = blockSize; + memoryBlocksPool ??= _defaultMemoryBlocksPool; + blockArrayPool ??= _defaultBlockArrayPool; + _memoryBlocksPool = memoryBlocksPool; + _blockArrayPool = blockArrayPool; if (expectedItemsCount > 0) { - _memoryBlocks = SetNewLength(_memoryBlocks, blockSize, expectedItemsCount.Value); - _capacity = _memoryBlocks.Length * blockSize; + _memoryBlocks = SetNewLength(_memoryBlocks, minBlockSize, expectedItemsCount.Value, memoryBlocksPool, blockArrayPool); + if (_memoryBlocks.Length > 0) + { + minBlockSize = _memoryBlocks[0].Length; + } + + _blockSize = minBlockSize; + _capacity = _memoryBlocks.Length * minBlockSize; } else { - _memoryBlocks = _emptyBlockArray; + _blockSize = minBlockSize; + _memoryBlocks = _emptyMemoryBlocksArray; } - AddRange(this, source); + AddRange(source); } public T this[long index] { get => _memoryBlocks[(int)(index / _blockSize)][index % _blockSize]; - set => new Span(_memoryBlocks[(int)(index / _blockSize)])[(int)index % _blockSize] = value; + set => new Span(_memoryBlocks[(int)(index / _blockSize)])[(int)(index % _blockSize)] = value; } public T this[int index] @@ -183,17 +234,23 @@ namespace Recyclable.Collections set => new Span(_memoryBlocks[index / _blockSize])[index % _blockSize] = value; } + public T[][] MemoryBlocks { get => _memoryBlocks; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(T item) { - int blockSize = _blockSize; - long oldCount = _longCount; - long requiredCapacity = oldCount + 1; - if (_capacity < requiredCapacity) + if (_capacity < _longCount + 1) { - _ = EnsureCapacity(requiredCapacity); + _ = EnsureCapacity(_longCount + 1); + } + + _memoryBlocks[_lastBlockIndex][_nextItemIndex++] = item; + if (_nextItemIndex == _blockSize) + { + _lastBlockIndex++; + _nextItemIndex = 0; } - _memoryBlocks[(int)(oldCount / blockSize)][oldCount % blockSize] = item; _longCount++; } @@ -210,20 +267,28 @@ namespace Recyclable.Collections int blockSize = _blockSize; int targetItemIdx = (int)(oldLongCount % blockSize); int targetBlockIdx = (int)(oldLongCount / blockSize) + (targetItemIdx > 0 ? 1 : 0); - var memoryBlockSpan = new Span(_memoryBlocks); - var targetBlockSpan = new Span(memoryBlockSpan[targetBlockIdx]); - for (long i = 0; i < sourceItemsCount; i++) + Span memoryBlocksSpan = new(_memoryBlocks); + int memoryBlocksCount = memoryBlocksSpan.Length; + Span targetBlockSpan = new(memoryBlocksSpan[targetBlockIdx]); + for (var i = 0L; i < sourceItemsCount; i++) { targetBlockSpan[targetItemIdx++] = items[i]; if (targetItemIdx == blockSize) { targetBlockIdx++; - targetBlockSpan = new Span(memoryBlockSpan[targetBlockIdx]); + if (targetBlockIdx == memoryBlocksCount) + { + break; + } + + targetBlockSpan = new Span(memoryBlocksSpan[targetBlockIdx]); targetItemIdx = 0; } } _longCount = targetCapacity; + _lastBlockIndex = targetBlockIdx - 1; + _nextItemIndex = targetItemIdx; } public void AddRange(in List items) @@ -239,20 +304,28 @@ namespace Recyclable.Collections int blockSize = _blockSize; int targetItemIdx = (int)(oldLongCount % blockSize); int targetBlockIdx = (int)(oldLongCount / blockSize) + (targetItemIdx > 0 ? 1 : 0); - var memoryBlockSpan = new Span(_memoryBlocks); - var targetBlockSpan = new Span(memoryBlockSpan[targetBlockIdx]); + Span memoryBlocksSpan = new(_memoryBlocks); + var memoryBlocksCount = memoryBlocksSpan.Length; + Span targetBlockSpan = new(memoryBlocksSpan[targetBlockIdx]); for (int i = 0; i < sourceItemsCount; i++) { targetBlockSpan[targetItemIdx++] = items[i]; if (targetItemIdx == blockSize) { targetBlockIdx++; - targetBlockSpan = new Span(memoryBlockSpan[targetBlockIdx]); + if (targetBlockIdx == memoryBlocksCount) + { + break; + } + + targetBlockSpan = new Span(memoryBlocksSpan[targetBlockIdx]); targetItemIdx = 0; } } _longCount = targetCapacity; + _lastBlockIndex = targetBlockIdx - 1; + _nextItemIndex = targetItemIdx; } public void AddRange(in IList items) @@ -268,114 +341,292 @@ namespace Recyclable.Collections int blockSize = _blockSize; int targetItemIdx = (int)(oldLongCount % blockSize); int targetBlockIdx = (int)(oldLongCount / blockSize) + (targetItemIdx > 0 ? 1 : 0); - var memoryBlockSpan = new Span(_memoryBlocks); - var targetBlockSpan = new Span(memoryBlockSpan[targetBlockIdx]); + Span memoryBlocksSpan = new(_memoryBlocks); + var memoryBlocksCount = memoryBlocksSpan.Length; + Span targetBlockSpan = new(memoryBlocksSpan[targetBlockIdx]); for (int i = 0; i < sourceItemsCount; i++) { targetBlockSpan[targetItemIdx++] = items[i]; if (targetItemIdx == blockSize) { targetBlockIdx++; - targetBlockSpan = new Span(memoryBlockSpan[targetBlockIdx]); + if (targetBlockIdx == memoryBlocksCount) + { + break; + } + + targetBlockSpan = new Span(memoryBlocksSpan[targetBlockIdx]); targetItemIdx = 0; } } _longCount = targetCapacity; + _lastBlockIndex = targetBlockIdx - 1; + _nextItemIndex = targetItemIdx; } - private static void AddRange(RecyclableList destination, IEnumerable source) + private void AddRange(IEnumerable source, int growByCount = RecyclableDefaults.MinPooledArrayLength) { - foreach (var item in source) + if (source is T[] sourceArray) { - destination.Add(item); + AddRange(sourceArray); + return; } + + if (source is List sourceList) + { + AddRange(sourceList); + return; + } + + if (source is RecyclableArrayList sourceRecyclableArrayList) + { + AddRange(sourceRecyclableArrayList); + return; + } + + if (source is IList sourceIList) + { + AddRange(sourceIList); + return; + } + + long capacity = _capacity; + long oldLongCount = _longCount; + int blockSize = _blockSize; + int targetItemIdx = (int)(oldLongCount % blockSize); + int targetBlockIdx = (targetItemIdx / blockSize) + (targetItemIdx > 0 ? 1 : 0); + + Span memoryBlocksSpan; + Span blockArraySpan; + if (source.TryGetNonEnumeratedCount(out var requiredAdditionalCapacity)) + { + + int requiredCapacity = targetItemIdx + requiredAdditionalCapacity; + if (capacity < requiredCapacity) + { + _ = EnsureCapacity(requiredCapacity); + blockSize = _blockSize; + targetBlockIdx = (targetItemIdx / blockSize) + (targetItemIdx > 0 ? 1 : 0); + targetItemIdx = (int)(oldLongCount % blockSize); + } + + memoryBlocksSpan = new(_memoryBlocks); + var memoryBlocksCount = memoryBlocksSpan.Length; + blockArraySpan = new Span(memoryBlocksSpan[targetBlockIdx]); + foreach (var item in source) + { + blockArraySpan[targetItemIdx++] = item; + if (targetItemIdx == blockSize) + { + targetBlockIdx++; + if (targetBlockIdx == memoryBlocksCount) + { + break; + } + + targetItemIdx = 0; + blockArraySpan = new Span(memoryBlocksSpan[targetBlockIdx]); + } + } + + _longCount = (targetBlockIdx * blockSize) + targetItemIdx; + _lastBlockIndex = targetBlockIdx; + _nextItemIndex = targetItemIdx; + return; + } + + long i; + using var enumerator = source.GetEnumerator(); + + if (enumerator.MoveNext()) + { + long available = capacity - targetItemIdx; + memoryBlocksSpan = new(_memoryBlocks); + var memoryBlocksCount = memoryBlocksSpan.Length; + while (true) + { + if (targetItemIdx + growByCount > capacity) + { + capacity = EnsureCapacity(capacity + growByCount); + memoryBlocksSpan = new(_memoryBlocks); + memoryBlocksCount = memoryBlocksSpan.Length; + available = capacity - targetItemIdx; + } + + blockArraySpan = memoryBlocksSpan[targetBlockIdx]; + for (i = 0; i < available; i++) + { + blockArraySpan[targetItemIdx++] = enumerator.Current; + if (!enumerator.MoveNext()) + { + break; + } + + if (targetItemIdx == blockSize) + { + targetBlockIdx++; + if (targetBlockIdx == memoryBlocksCount) + { + break; + } + + targetItemIdx = 0; + blockArraySpan = new(memoryBlocksSpan[targetBlockIdx]); + } + } + + if (i < available) + { + break; + } + } + } + + _longCount = targetItemIdx; + _lastBlockIndex = targetBlockIdx - 1; + _nextItemIndex = targetItemIdx; } public void Clear() { - try + if (_blockSize >= RecyclableDefaults.MinPooledArrayLength) { - // Remove in reversed order for performance savings - var toRemoveIdx = _memoryBlocks.Length - 1; - while (toRemoveIdx >= 0) + Span memoryBlocksSpan = new(_memoryBlocks); + int memoryBlocksCount = memoryBlocksSpan.Length; + ArrayPool blockArrayPool = _blockArrayPool; + for (int toRemoveIdx = 0; toRemoveIdx < memoryBlocksCount; toRemoveIdx++) { - try - { - ReleaseBlock(this, _memoryBlocks, _blockSize, toRemoveIdx); - } - catch (Exception) - { - // We want to try returning as many arrays, as possible, before - // the list is cleared. - } - - toRemoveIdx--; + blockArrayPool.Return(memoryBlocksSpan[toRemoveIdx]); } } - finally - { - _longCount = 0; - } + + _capacity = 0; + _lastBlockIndex = 0; + _nextItemIndex = 0; + _longCount = 0; } - public bool Contains(T item) => _memoryBlocks.Any(x => x.Contains(item)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(T item) + { + if (_longCount == 0) + { + return false; + } - public void CopyTo(T[] array, int arrayIndex) => _memoryBlocks.CopyTo(0, _blockSize, (int)LongCount % _blockSize, array, arrayIndex); + int itemIndex, blockIndex = 0, lastBlockIndex = _lastBlockIndex, blockSize = _blockSize; + Span memoryBlocksSpan = new(_memoryBlocks); + while (blockIndex < lastBlockIndex) + { + itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex], item, 0, blockSize); + if (itemIndex >= 0) + { + return true; + } + + blockIndex++; + } + + return Array.IndexOf(memoryBlocksSpan[blockIndex], item, 0, _nextItemIndex) >= 0; + } + + public void CopyTo(T[] array, int arrayIndex) => _memoryBlocks.CopyTo(0, _blockSize, _longCount, array, arrayIndex); public IEnumerator GetEnumerator() => _memoryBlocks.Enumerate(_blockSize, LongCount).GetEnumerator(); - public int IndexOf(T item) => (int)_memoryBlocks.LongIndexOf(_longCount, item, _equalityComparer); - public void Insert(int index, T item) => throw new NotSupportedException(); - public long LongIndexOf(T item) => _memoryBlocks.LongIndexOf(_blockSize, item, _equalityComparer); - public bool Remove(T item) => throw new NotSupportedException(); - public void RemoveBlock(int index) => ReleaseBlock(this, _memoryBlocks, _blockSize, index); - public void RemoveAt(int index) => RemoveAt(this, index); - public void RemoveAt(long index) => RemoveAt(this, index); - - IEnumerator IEnumerable.GetEnumerator() => _memoryBlocks.Enumerate(_blockSize, LongCount).GetEnumerator(); - - private static void ReleaseBlock(RecyclableList owner, in T[][] memoryBlocks, int blockSize, int blockIndex) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int IndexOf(T item) { - try + if (_longCount == 0) { - memoryBlocks[blockIndex].ReturnToPool(_blockArrayPool); + return ItemNotFoundIndex; } - finally + + int itemIndex, blockIndex = 0, lastBlockIndex = _lastBlockIndex, blockSize = _blockSize; + Span memoryBlocksSpan = new(_memoryBlocks); + while (blockIndex < lastBlockIndex) { - owner._capacity -= blockSize; + itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex], item, 0, blockSize); + if (itemIndex >= 0) + { + return (blockIndex * blockSize) + itemIndex; + } + + blockIndex++; + } + + itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex], item, 0, _nextItemIndex); + return itemIndex >= 0 + ? (blockIndex * blockSize) + itemIndex + : ItemNotFoundIndex; + } + + public void Insert(int index, T item) => throw new NotSupportedException(); + public long LongIndexOf(T item) + { + if (_longCount == 0) + { + return ItemNotFoundIndex; + } + + int itemIndex, blockIndex = 0, lastBlockIndex = _lastBlockIndex, blockSize = _blockSize; + Span memoryBlocksSpan = new(_memoryBlocks); + while (blockIndex < lastBlockIndex) + { + itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex], item, 0, blockSize); + if (itemIndex >= 0) + { + return ((long)blockIndex * blockSize) + itemIndex; + } + + blockIndex++; + } + + itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex], item, 0, _nextItemIndex); + return itemIndex >= 0 + ? ((long)blockIndex * blockSize) + itemIndex + : ItemNotFoundIndex; + } + + public bool Remove(T item) => throw new NotSupportedException(); + public void RemoveBlock(int index) + { + if (_blockSize < RecyclableDefaults.MinPooledArrayLength) + { + _memoryBlocks[index] = _emptyBlockArray; + } + else + { + _blockArrayPool.Return(_memoryBlocks[index]); + } + + _capacity -= _blockSize; + if (_lastBlockIndex == index) + { + _nextItemIndex = 0; + } + else if (_lastBlockIndex > index) + { + _lastBlockIndex--; } } - //private static RecyclableArrayList SetupMemoryBlocks(RecyclableList owner, int blockSize, long? totalItemsCount) - //{ - // if (blockSize <= 0) - // { - // return new(); - // } - - // totalItemsCount ??= 8 * blockSize; - // int additionalArray = totalItemsCount % blockSize > 0 ? 1 : 0; - // int memoryBlockCount = (int)(totalItemsCount.Value / blockSize).LimitTo(int.MaxValue) + additionalArray; - // RecyclableArrayList memoryBlocks = new(memoryBlockCount); - // for (var blockIdx = 0; blockIdx < memoryBlockCount; blockIdx++) - // { - // memoryBlocks[blockIdx] = RentArrayFromPool(blockSize); - // } - - // memoryBlocks.Count = memoryBlockCount; - // owner.Capacity = totalItemsCount.Value; - // return memoryBlocks; - //} + public void RemoveAt(int index) => RemoveAt(this, index, _blockArrayPool); + public void RemoveAt(long index) => RemoveAt(this, index, _blockArrayPool); + IEnumerator IEnumerable.GetEnumerator() => _memoryBlocks.Enumerate(_blockSize, LongCount).GetEnumerator(); public void Dispose() { if (_capacity > 0) { Clear(); - _capacity = 0; - _memoryBlocks.ReturnToPool(_arrayPool); + if (_memoryBlocks.Length >= RecyclableDefaults.MinPooledArrayLength) + { + _memoryBlocksPool.Return(_memoryBlocks); + } + GC.SuppressFinalize(this); } } diff --git a/Recyclable.Collections/RecyclableListExtensions.cs b/Recyclable.Collections/RecyclableListExtensions.cs index 2799c22..e873729 100644 --- a/Recyclable.Collections/RecyclableListExtensions.cs +++ b/Recyclable.Collections/RecyclableListExtensions.cs @@ -15,41 +15,26 @@ namespace Recyclable.Collections } } - public static void CopyTo(this T[][] arrays, long startingIndex, int blockSize, int lastBlockSize, T[] destinationArray, int destinationArrayIndex) + public static void CopyTo(this T[][] sourceMemoryBlocks, long startingIndex, int blockSize, long itemsCount, T[] destinationArray, int destinationArrayIndex) { - Span arrayMemory = destinationArray.AsSpan(); - arrayMemory = arrayMemory[destinationArrayIndex..]; - int startingArrayIndex = (int)(startingIndex / blockSize); - startingIndex %= blockSize; - if (arrays.LongLength > 0) + Memory sourceBlockMemory; + Memory destinationArrayMemory; + int startingBlockIndex = (int)(startingIndex / blockSize); + int lastBlockIndex = (int)((itemsCount / blockSize) + (itemsCount - (itemsCount / blockSize) > 0 ? 1 : 0)) - 1; + Span sourceMemoryBlocksSpan = new(sourceMemoryBlocks, startingBlockIndex, lastBlockIndex - startingBlockIndex + 1); + destinationArrayMemory = new Memory(destinationArray, destinationArrayIndex, destinationArray.Length); + int memoryBlockIndex; + for (memoryBlockIndex = 0; memoryBlockIndex < lastBlockIndex; memoryBlockIndex++) { - ReadOnlySpan sourceMemory = arrays[0].AsSpan(); - var maxToCopy = (int)Math.Min(blockSize - startingIndex, arrayMemory.Length); - if (arrays.LongLength == 1) - { - maxToCopy = (int)Math.Min(maxToCopy, lastBlockSize); - } - - sourceMemory = sourceMemory[(int)startingIndex..maxToCopy]; - sourceMemory.CopyTo(arrayMemory); - arrayMemory = arrayMemory[maxToCopy..]; + sourceBlockMemory = new(sourceMemoryBlocksSpan[memoryBlockIndex], 0, blockSize); + sourceBlockMemory.CopyTo(destinationArrayMemory); + destinationArrayMemory = destinationArrayMemory[blockSize..]; } - for (var arrayIdx = startingArrayIndex + 1; arrayIdx < arrays.Length - 1 && arrayMemory.Length > 0; arrayIdx++) + if (itemsCount % blockSize > 0) { - ReadOnlySpan sourceMemory = arrays[arrayIdx].AsSpan(); - var maxToCopy = Math.Min(blockSize, arrayMemory.Length); - sourceMemory = sourceMemory[..maxToCopy]; - sourceMemory.CopyTo(arrayMemory); - arrayMemory = arrayMemory[maxToCopy..]; - } - - if (arrays.Length > 1 && arrayMemory.Length > 0) - { - ReadOnlySpan sourceMemory = arrays[^1].AsSpan(); - var maxToCopy = Math.Min(lastBlockSize, arrayMemory.Length); - sourceMemory = sourceMemory[..maxToCopy]; - sourceMemory.CopyTo(arrayMemory); + sourceBlockMemory = new(sourceMemoryBlocksSpan[^1], 0, (int)(itemsCount % blockSize)); + sourceBlockMemory.CopyTo(destinationArrayMemory); } } diff --git a/Recyclable.CollectionsTests/RecyclableListTests.cs b/Recyclable.CollectionsTests/RecyclableListTests.cs index 7cff822..e89378a 100644 --- a/Recyclable.CollectionsTests/RecyclableListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableListTests.cs @@ -8,7 +8,7 @@ namespace Recyclable.CollectionsTests private const int _totalObjectCount = 5_000; private static RecyclableList NewRecyclableList => CreateReversedRecyclableList(_testData); private static readonly IEnumerable _testData = Enumerable.Range(1, _totalObjectCount); - private static RecyclableList CreateReversedRecyclableList(IEnumerable source) => new(source); + private static RecyclableList CreateReversedRecyclableList(IEnumerable source) => new(source, 1024); [Fact] public void AddShouldAddItems() @@ -85,6 +85,53 @@ namespace Recyclable.CollectionsTests _ = list.LongCount.Should().Be(0); } + [Fact] + public void ContainsShouldFindAllItems() + { + // Prepare + var testData = _testData; + using var list = NewRecyclableList; + _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); + + // Act + foreach (var item in testData) + { + // Validate + _ = list.Contains(item).Should().BeTrue(); + } + } + + [Fact] + public void ContainsShouldNotFindNonExistingItems() + { + // Prepare + var testData = _testData; + using var list = NewRecyclableList; + _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); + + // Act + foreach (var item in testData) + { + // Validate + _ = list.Contains(-item).Should().BeFalse(); + } + + // Validate + _ = list.Contains(0).Should().BeFalse(); + } + + [Fact] + public void ContainsShouldNotFindAnythingWhenListEmpty() + { + // Prepare + using var list = new RecyclableList(); + + // Validate + _ = list.Contains(0).Should().BeFalse(); + _ = list.Contains(1).Should().BeFalse(); + } + + [Fact] public void CopyToShouldCopyAllItems() {