diff --git a/README.md b/README.md index 9a9705b..973068b 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,39 @@ # Recyclable.Collections `Recyclable.Collections` project is an open source framework for operating dynamic lists at performance close to raw arrays, but fairly unlimited in size. It aims at providing minimal memory footprint. It implements `IList`'s interface and is targeted as direct replacements of `List`, `SortableList`, `PriorityQueue` & similar. +## Included +* `RecyclableArrayList` +* `RecyclableList` +* `RecyclableSortableList` + * Range: `long` + * Interfaces: `IList`, `ILongList`, `IEnumerable`, `IDisposable` +* `RecyclableQueue` + * Range: `long` + * Interfaces: `IList`, `ILongList`, `IEnumerable`, `IDisposable` +* `RecyclableStack` + * Range: `long` + * Interfaces: `IList`, `ILongList`, `IEnumerable`, `IDisposable` + # Characteristicts of the classes -* All classes provide large storage capabilities, in practice limited only by the memory available in your system. -* All data is stored in blocks, with the provided `int blockSize` or the default, currently being `10,240` items in each block. It's recommended to use smaller numbers, if you foresee storing low no. of items on the list. +## Common +* All classes implement `IDisposable` interface & SHOULD be disposed after use. That's to return block arrays taken from the shared pool. It may be foreseen as an issue for replacement in existing code, which obviously is missing `using` clause. But considering that `Dispose` will be called by `GC` anyway, it should cause issues in specific scenarios, only. In either case the fix is one-word addition of `using`. * Memory blocks are created in 2 ways, depending on `blockSize` value * when `blockSize < 100` ⇨ blocks are created as regular `arrays` * when `blockSize >= 100` ⇨ blocks are taken from and returned to `ArrayPool.Shared`, when blocks are removed -* All classes implement `IDisposable` interface & SHOULD be disposed after use. That's to return block arrays taken from the shared pool. It may be foreseen as an issue for replacement in existing code, which obviously is missing `using` clause. But considering that `Dispose` will be called by `GC` anyway, it should cause issues in specific scenarios, only. In either case the fix is one-word addition of `using`. +* Array pools are shared between the same `T` type. I.e. `List` will use a different pool from `List` and so on. For high concurrency environments you may want to provide your own pools, when this feature becomes available in the upcoming releases. +* Trying to access `this[int index]`, `this[long index]`, `Count`, `LongCount` etc. when `Capacity == 0` will raise `NullReferenceException`. This is by design to remove all non-critical code from the constructor. + +## `RecyclableArrayList` +* Range: `int` +* Interfaces: `IList`, `IEnumerable`, `IDisposable` +This is the direct equivalent of `List` class, except that the arrays are taken from the shared pool, instead of directly being allocated. It proves to out perform `List` & the standard arrays in certains operations and is fairly close in others. But they may perform worse in certain scenarios. If you're interested in details, please refer to benchmarks. + +## `RecyclableList`, `RecyclableSortableList`, `RecyclableQueue`, `RecyclableStack` +* Range: `long` +* Interfaces: `IList`, `ILongList`, `IEnumerable`, `IDisposable` +* All classes provide large storage capabilities, in practice limited only by the memory available in your system. +* All data is stored in blocks, with the provided `int blockSize` or the default, currently being `10,240` items in each block. It's recommended to use smaller numbers, if you foresee storing low no. of items on the list. * There is no memory copying to increase the capacity of the recyclable classes, anymore. If more capacity is needed, a new memory block is allocate & added to the internal list of blocks. This is the only list that may re-allocate memory block & copy their content to grow. That shouldn't be an issue considering limited no. of blocks stored by each class in practical scenarios. Currently I'm not planning any works around that. Shall there be a need to eliminate it, you're welcomed to file a PR with the proposed changes. * `Insert`, `Remove` & `RemoveAt` methods raise `NotSupportedException`. This is by design in the current version, because it involves performance degradadion. I'm planning works around this in the upcoming weeks. If you need that sooner, you're welcomed to file a PR with the proposed changes. @@ -16,7 +41,7 @@ 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 some 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 & proved to perform worse. For this reason, properties & methods expected to be on the hotpath duplicate some code. +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. ## 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. diff --git a/Recyclable.Collections.Benchmarks/BenchmarkBase.cs b/Recyclable.Collections.Benchmarks/BenchmarkBase.cs index e2a824b..3c10498 100644 --- a/Recyclable.Collections.Benchmarks/BenchmarkBase.cs +++ b/Recyclable.Collections.Benchmarks/BenchmarkBase.cs @@ -4,8 +4,8 @@ namespace Recyclable.Collections.Benchmarks { public class BenchmarkBase { - [Params(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; // = 1_000_000; + //[Params(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, diff --git a/Recyclable.Collections.Benchmarks/Program.cs b/Recyclable.Collections.Benchmarks/Program.cs index 52450ac..d5cb50b 100644 --- a/Recyclable.Collections.Benchmarks/Program.cs +++ b/Recyclable.Collections.Benchmarks/Program.cs @@ -3,12 +3,14 @@ using Recyclable.Collections.Benchmarks; //BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(new[] { "-f*" }); -BenchmarkRunner.Run(); +//BenchmarkRunner.Run(); //BenchmarkRunner.Run(); -//var benchmark = new RecyclableCollectionsBenchmarks(); +var benchmark = new RecyclableCollectionsBenchmarks(); +benchmark.Setup(); //benchmark.RecyclableArrayList_AddRange_WithCapacity(); //benchmark.RecyclableArrayList_Add_WithCapacity(); +benchmark.RecyclableList_Add(); //BenchmarkRunner.Run(); diff --git a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.List.cs b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.List.cs index 4f62236..8df675e 100644 --- a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.List.cs +++ b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.List.cs @@ -4,14 +4,14 @@ namespace Recyclable.Collections.Benchmarks { public partial class RecyclableCollectionsBenchmarks : BenchmarkBase { - //[Benchmark(Baseline = true)] + [Benchmark(Baseline = true)] public void List_Create() { var list = new List(); DoNothing(list); } - //[Benchmark] + [Benchmark] public void List_Create_WithCapacity() { var list = new List(TestObjectCount); @@ -30,7 +30,7 @@ namespace Recyclable.Collections.Benchmarks } } - [Benchmark(Baseline = true)] + [Benchmark(Baseline = false)] public void List_Add_WithCapacity() { var data = TestObjects; diff --git a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableArrayList.cs b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableArrayList.cs index 2640941..dfd26e8 100644 --- a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableArrayList.cs +++ b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableArrayList.cs @@ -31,14 +31,14 @@ namespace Recyclable.Collections.Benchmarks _testObjects = default; } - //[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); diff --git a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableList.cs b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableList.cs index 89f305b..37e8da2 100644 --- a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableList.cs +++ b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableList.cs @@ -6,28 +6,21 @@ namespace Recyclable.Collections.Benchmarks { private RecyclableList _testRecyclableList; - //[Benchmark] + [Benchmark] public void RecyclableList_Create() { using var list = new RecyclableList(); DoNothing(list); } - //[Benchmark] - public void RecyclableList_Create_0Capacity() - { - using var list = new RecyclableList(totalItemsCount: 0); - DoNothing(list); - } - - //[Benchmark] + [Benchmark] public void RecyclableList_Create_WithCapacity() { - using var list = new RecyclableList(totalItemsCount: TestObjectCount); + using var list = new RecyclableList(initialCapacity: TestObjectCount); DoNothing(list); } - //[Benchmark] + [Benchmark] public void RecyclableList_Add() { var data = TestObjects; @@ -39,7 +32,7 @@ namespace Recyclable.Collections.Benchmarks } } - //[Benchmark] + [Benchmark] public void RecyclableList_Add_WithCapacity() { var data = TestObjects; diff --git a/Recyclable.Collections/RecyclableArrayList.cs b/Recyclable.Collections/RecyclableArrayList.cs index 8294b0a..b811fbd 100644 --- a/Recyclable.Collections/RecyclableArrayList.cs +++ b/Recyclable.Collections/RecyclableArrayList.cs @@ -9,7 +9,7 @@ namespace Recyclable.Collections private static readonly ArrayPool _arrayPool = ArrayPool.Create(); private static readonly IEqualityComparer _equalityComparer = EqualityComparer.Default; - protected T[] _memory; + protected T[] _memoryBlock; private static void ThrowArgumentOutOfRangeException() { @@ -21,33 +21,33 @@ namespace Recyclable.Collections { ArrayPool arrayPool = _arrayPool; var sourceLength = source?.Length ?? 0; - T[] newArray = newSize.RentArrayFromPool(arrayPool); + T[] newMemoryBlock = newSize.RentArrayFromPool(arrayPool); switch (newSize >= sourceLength) { case true: if (sourceLength > 0) { Memory sourceMemory = new(source); - Memory newArrayMemory = new(newArray); + Memory newArrayMemory = new(newMemoryBlock); sourceMemory.CopyTo(newArrayMemory); source!.ReturnToPool(arrayPool); } - return newArray; + return newMemoryBlock; case false: var sourceSpan = new Span(source)[..newSize]; - Span newArraySpan = new(newArray); + Span newArraySpan = new(newMemoryBlock); sourceSpan.CopyTo(newArraySpan); source!.ReturnToPool(arrayPool); - return newArray; + return newMemoryBlock; } } protected int EnsureCapacity(in int requestedCapacity) { int oldCapacity = _capacity; - ref T[] memory = ref _memory; + ref T[] memory = ref _memoryBlock; int newCapacity; switch (oldCapacity > 0) @@ -84,8 +84,8 @@ namespace Recyclable.Collections { if (initialCapacity > 0) { - _memory = SetNewLength(_memory, initialCapacity); - _capacity = _memory.Length; + _memoryBlock = SetNewLength(_memoryBlock, initialCapacity); + _capacity = _memoryBlock.Length; } } @@ -119,15 +119,15 @@ namespace Recyclable.Collections public RecyclableArrayList(IEnumerable source, int initialCapacity = RecyclableDefaults.Capacity) { - _memory = SetNewLength(_memory, initialCapacity); - _capacity = _memory.Length; + _memoryBlock = SetNewLength(_memoryBlock, initialCapacity); + _capacity = _memoryBlock.Length; AddRange(source); } public T this[int index] { - get => _memory[index]; - set => new Span(_memory)[index] = value; + get => _memoryBlock[index]; + set => new Span(_memoryBlock)[index] = value; } protected int _capacity; @@ -138,8 +138,8 @@ namespace Recyclable.Collections { if (_capacity != value) { - _memory = SetNewLength(_memory, value); - _capacity = _memory.Length; + _memoryBlock = SetNewLength(_memoryBlock, value); + _capacity = _memoryBlock.Length; } } } @@ -156,13 +156,13 @@ namespace Recyclable.Collections //[MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(T item) { - int requestedCapacity = _count + 1; - if (_capacity < requestedCapacity) + int requiredCapacity = _count + 1; + if (_capacity < requiredCapacity) { - _ = EnsureCapacity(requestedCapacity); + _ = EnsureCapacity(requiredCapacity); } - _memory[_count++] = item; + _memoryBlock[_count++] = item; } public void AddRange(in T[] items) @@ -174,7 +174,7 @@ namespace Recyclable.Collections _ = EnsureCapacity(targetCapacity); } - var targetSpan = new Span(_memory)[_count..]; + var targetSpan = new Span(_memoryBlock)[_count..]; Span itemsSpan = new(items); itemsSpan.CopyTo(targetSpan); _count = targetCapacity; @@ -189,7 +189,7 @@ namespace Recyclable.Collections _ = EnsureCapacity(targetCapacity); } - items.CopyTo(_memory, _count); + items.CopyTo(_memoryBlock, _count); _count = targetCapacity; } @@ -202,7 +202,7 @@ namespace Recyclable.Collections _ = EnsureCapacity(targetCapacity); } - items.CopyTo(_memory, _count); + items.CopyTo(_memoryBlock, _count); _count = targetCapacity; } @@ -215,8 +215,8 @@ namespace Recyclable.Collections _ = EnsureCapacity(targetCapacity); } - var targetSpan = new Span(_memory)[_count..]; - Span itemsSpan = new(items._memory); + var targetSpan = new Span(_memoryBlock)[_count..]; + Span itemsSpan = new(items._memoryBlock); itemsSpan.CopyTo(targetSpan); _count = targetCapacity; } @@ -253,7 +253,7 @@ namespace Recyclable.Collections if (source.TryGetNonEnumeratedCount(out var requiredAdditionalCapacity)) { _ = EnsureCapacity(targetIndex + requiredAdditionalCapacity); - memorySpan = new(_memory); + memorySpan = new(_memoryBlock); foreach (var item in source) { memorySpan[targetIndex++] = item; @@ -266,7 +266,7 @@ namespace Recyclable.Collections int i; using var enumerator = source.GetEnumerator(); - memorySpan = new(_memory); + memorySpan = new(_memoryBlock); if (enumerator.MoveNext()) { int available = capacity - targetIndex; @@ -275,7 +275,7 @@ namespace Recyclable.Collections if (targetIndex + growByCount > capacity) { capacity = EnsureCapacity(capacity + growByCount); - memorySpan = new(_memory); + memorySpan = new(_memoryBlock); available = capacity - targetIndex; } @@ -303,14 +303,14 @@ namespace Recyclable.Collections _count = 0; } - public bool Contains(T item) => _memory.Contains(item); + public bool Contains(T item) => _memoryBlock.Contains(item); - public void CopyTo(T[] array, int arrayIndex) => _memory.CopyTo(array, arrayIndex); + public void CopyTo(T[] array, int arrayIndex) => _memoryBlock.CopyTo(array, arrayIndex); protected static IEnumerable Enumerate(RecyclableArrayList list) { int count = list._count; - var memory = list._memory; + var memory = list._memoryBlock; for (var i = 0; i < count; i++) { yield return memory[i]; @@ -319,7 +319,7 @@ namespace Recyclable.Collections public int IndexOf(T itemToFind) { - Span memorySpan = new(_memory); + Span memorySpan = new(_memoryBlock); int itemCount = _count; var equalityComparer = _equalityComparer; for (var itemIdx = 0; itemIdx < itemCount; itemIdx++) @@ -345,12 +345,12 @@ namespace Recyclable.Collections if (oldCount > 0) { - var sourceSpan = new Span(_memory)[index..oldCount]; - var targetSpan = new Span(_memory)[(index + 1)..]; + var sourceSpan = new Span(_memoryBlock)[index..oldCount]; + var targetSpan = new Span(_memoryBlock)[(index + 1)..]; sourceSpan.CopyTo(targetSpan); } - _memory[index] = item; + _memoryBlock[index] = item; _count++; } @@ -365,6 +365,7 @@ namespace Recyclable.Collections return false; } + public void RemoveAt(int index) { int oldCount = _count; @@ -381,7 +382,7 @@ namespace Recyclable.Collections return; } - _memory.CopyItems(index, oldCountMinus1, ref _memory); + _memoryBlock.CopyItems(index, oldCountMinus1, ref _memoryBlock); _count--; } @@ -394,7 +395,7 @@ namespace Recyclable.Collections { Clear(); _capacity = 0; - _memory.ReturnToPool(_arrayPool); + _memoryBlock.ReturnToPool(_arrayPool); GC.SuppressFinalize(this); } } diff --git a/Recyclable.Collections/RecyclableList.cs b/Recyclable.Collections/RecyclableList.cs index 45e6182..5263a80 100644 --- a/Recyclable.Collections/RecyclableList.cs +++ b/Recyclable.Collections/RecyclableList.cs @@ -1,20 +1,279 @@ -using System.Buffers; +using System; +using System.Buffers; using System.Collections; +using System.Runtime.CompilerServices; 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 static readonly IEqualityComparer _equalityComparer = EqualityComparer.Default; - private const int _minPooledArraySize = 100; private readonly int _blockSize; - private readonly RecyclableArrayList _memoryBlocks; + protected T[][] _memoryBlocks; - private static void AddCapacity(RecyclableList list, RecyclableArrayList memoryBlocks, int additionalCapacity) + private long _capacity; + public long Capacity { - T[] newMemoryBlock = RentArrayFromPool(additionalCapacity); - memoryBlocks.Add(newMemoryBlock); - list.Capacity += additionalCapacity; + get => _capacity; + protected set => _capacity = value; + } + + public int Count => (int)LongCount; + + protected long _longCount; + public long LongCount + { + get => _longCount; + set => _longCount = value; + } + + public bool IsReadOnly { get; } = false; + public int BlockCount => _memoryBlocks.Length; + + private static void RemoveAt(RecyclableList list, long index) + { + long oldCount = list._longCount; + long oldCountMinus1 = oldCount - 1; + if (index != oldCountMinus1) + { + ThrowArgumentOutOfRangeException(); + } + + list._longCount--; + if (list._capacity - oldCountMinus1 == list._blockSize) + { + list.RemoveBlock(list._memoryBlocks.Length - 1); + } + } + + private static void ThrowArgumentOutOfRangeException() + { + throw new ArgumentOutOfRangeException("index"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static T[][] SetNewLength(in T[][]? source, in int blockSize, in long newSize) + { + ArrayPool arrayPool = _arrayPool; + ArrayPool blockArrayPool = _blockArrayPool; + var sourceBlockCount = source?.Length ?? 0; + int requiredBlockCount = (int)(newSize / blockSize) + (newSize % blockSize > 0 ? 1 : 0); + Span newArraySpan; + T[][] newMemoryBlocks = requiredBlockCount.RentArrayFromPool(arrayPool); + switch (requiredBlockCount >= sourceBlockCount) + { + 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); + } + + 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; + } + } + + protected long EnsureCapacity(in long requestedCapacity) + { + long oldCapacity = _capacity; + ref T[][] memory = ref _memoryBlocks; + + long newCapacity; + switch (oldCapacity > 0) + { + case true: + newCapacity = oldCapacity; + while (newCapacity < requestedCapacity) + { + newCapacity *= 2; + } + + break; + + case false: + newCapacity = requestedCapacity; + break; + } + + memory = SetNewLength(memory, _blockSize, newCapacity); + newCapacity = memory.Length; + _capacity = newCapacity; + 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 + { + _blockSize = blockSize; + if (initialCapacity > 0) + { + _memoryBlocks = SetNewLength(_memoryBlocks, blockSize, initialCapacity.Value); + _capacity = _memoryBlocks.Length * blockSize; + } + else + { + _memoryBlocks = _emptyBlockArray; + } + } + +#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 + { + _blockSize = blockSize; + if (expectedItemsCount > 0) + { + _memoryBlocks = SetNewLength(_memoryBlocks, blockSize, expectedItemsCount.Value); + _capacity = _memoryBlocks.Length * blockSize; + } + else + { + _memoryBlocks = _emptyBlockArray; + } + + AddRange(this, source); + } + + public T this[long index] + { + get => _memoryBlocks[(int)(index / _blockSize)][index % _blockSize]; + set => new Span(_memoryBlocks[(int)(index / _blockSize)])[(int)index % _blockSize] = value; + } + + public T this[int index] + { + get => _memoryBlocks[index / _blockSize][index % _blockSize]; + set => new Span(_memoryBlocks[index / _blockSize])[index % _blockSize] = value; + } + + public void Add(T item) + { + int blockSize = _blockSize; + long oldCount = _longCount; + long requiredCapacity = oldCount + 1; + if (_capacity < requiredCapacity) + { + _ = EnsureCapacity(requiredCapacity); + } + + _memoryBlocks[(int)(oldCount / blockSize)][oldCount % blockSize] = item; + _longCount++; + } + + public void AddRange(in T[] items) + { + long sourceItemsCount = items.LongLength; + long oldLongCount = _longCount; + long targetCapacity = oldLongCount + sourceItemsCount; + if (_capacity < targetCapacity) + { + _ = EnsureCapacity(targetCapacity); + } + + 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++) + { + targetBlockSpan[targetItemIdx++] = items[i]; + if (targetItemIdx == blockSize) + { + targetBlockIdx++; + targetBlockSpan = new Span(memoryBlockSpan[targetBlockIdx]); + targetItemIdx = 0; + } + } + + _longCount = targetCapacity; + } + + public void AddRange(in List items) + { + long sourceItemsCount = items.Count; + long oldLongCount = _longCount; + long targetCapacity = oldLongCount + sourceItemsCount; + if (_capacity < targetCapacity) + { + _ = EnsureCapacity(targetCapacity); + } + + 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 (int i = 0; i < sourceItemsCount; i++) + { + targetBlockSpan[targetItemIdx++] = items[i]; + if (targetItemIdx == blockSize) + { + targetBlockIdx++; + targetBlockSpan = new Span(memoryBlockSpan[targetBlockIdx]); + targetItemIdx = 0; + } + } + + _longCount = targetCapacity; + } + + public void AddRange(in IList items) + { + long sourceItemsCount = items.Count; + long oldLongCount = _longCount; + long targetCapacity = oldLongCount + sourceItemsCount; + if (_capacity < targetCapacity) + { + _ = EnsureCapacity(targetCapacity); + } + + 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 (int i = 0; i < sourceItemsCount; i++) + { + targetBlockSpan[targetItemIdx++] = items[i]; + if (targetItemIdx == blockSize) + { + targetBlockIdx++; + targetBlockSpan = new Span(memoryBlockSpan[targetBlockIdx]); + targetItemIdx = 0; + } + } + + _longCount = targetCapacity; } private static void AddRange(RecyclableList destination, IEnumerable source) @@ -25,125 +284,16 @@ namespace Recyclable.Collections } } - 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"); - } - } - - private static T[] RentArrayFromPool(int minSize) => (minSize >= _minPooledArraySize) - ? ArrayPool.Shared.Rent(minSize) - : new T[minSize]; - - private static void ReturnToPool(in T[] array) - { - if (array.LongLength is < _minPooledArraySize or > int.MaxValue) - { - return; - } - - ArrayPool.Shared.Return(array); - } - - private static void RemoveBlock(RecyclableList owner, RecyclableArrayList memoryBlocks, int blockSize, int index) - { - try - { - ReturnToPool(memoryBlocks[index]); - memoryBlocks.RemoveAt(index); - } - finally - { - owner.Capacity -= blockSize; - } - } - - 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 RecyclableList(int blockSize = RecyclableDefaults.BlockSize, long? totalItemsCount = default) - { - _blockSize = blockSize; - _memoryBlocks = SetupMemoryBlocks(this, blockSize, totalItemsCount); - } - - public RecyclableList(IEnumerable source, int blockSize = RecyclableDefaults.BlockSize, long? totalItemsCount = default) - { - _blockSize = blockSize; - _memoryBlocks = SetupMemoryBlocks(this, blockSize, totalItemsCount); - AddRange(this, source); - } - - public T this[long index] - { - get => _memoryBlocks[(int)(index / _blockSize)][index % _blockSize]; - set => _memoryBlocks[(int)(index / _blockSize)][index % _blockSize] = value; - } - - public T this[int index] - { - get => _memoryBlocks[index / _blockSize][index % _blockSize]; - set => _memoryBlocks[index / _blockSize][index % _blockSize] = value; - } - - public long Capacity { get; protected set; } - public int Count => (int)LongCount; - public long LongCount { get; set; } - public bool IsReadOnly { get; } = false; - public int BlockCount => _memoryBlocks.Count; - - public void Add(T item) - { - long oldCount = LongCount; - int blockSize = _blockSize; - if (oldCount == Capacity) - { - AddCapacity(this, _memoryBlocks, blockSize); - } - - _memoryBlocks[(int)(oldCount / blockSize)][oldCount % blockSize] = item; - LongCount++; - } - public void Clear() { try { // Remove in reversed order for performance savings - while (_memoryBlocks.Count > 0) + while (_memoryBlocks.Length > 0) { try { - RemoveBlock(this, _memoryBlocks, _blockSize, _memoryBlocks.Count - 1); + _memoryBlocks = RemoveBlock(this, _memoryBlocks, _blockSize, _memoryBlocks.Length - 1); } catch (Exception) { @@ -154,36 +304,77 @@ namespace Recyclable.Collections } finally { - LongCount = 0; - _memoryBlocks.Clear(); + _longCount = 0; } } - public bool Contains(T item) => _memoryBlocks.Contains(item); + public bool Contains(T item) => _memoryBlocks.Any(x => x.Contains(item)); public void CopyTo(T[] array, int arrayIndex) => _memoryBlocks.CopyTo(0, _blockSize, (int)LongCount % _blockSize, array, arrayIndex); public IEnumerator GetEnumerator() => _memoryBlocks.Enumerate(_blockSize, LongCount).GetEnumerator(); - public int IndexOf(T item) => (int)_memoryBlocks.LongIndexOf(_blockSize, item, _equalityComparer); - + 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) => _memoryBlocks = RemoveBlock(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(); - public void Dispose() + //private static void AddCapacity(RecyclableList list, RecyclableArrayList memoryBlocks, int additionalCapacity) + //{ + // T[] newMemoryBlock = RentArrayFromPool(additionalCapacity); + // memoryBlocks.Add(newMemoryBlock); + // list.Capacity += additionalCapacity; + //} + + private static T[][] RemoveBlock(RecyclableList owner, in T[][] memoryBlocks, int blockSize, int blockIndex) { - Clear(); - GC.SuppressFinalize(this); + try + { + var memoryBlocksSpan = new Span(memoryBlocks); + return SetNewLength(memoryBlocks, blockSize, memoryBlocks.Length - 1); + } + finally + { + owner._capacity -= blockSize; + } } - public void RemoveBlock(int index) => RemoveBlock(this, _memoryBlocks, _blockSize, index); + //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 Dispose() + { + if (_capacity > 0) + { + Clear(); + _capacity = 0; + _memoryBlocks.ReturnToPool(_arrayPool); + GC.SuppressFinalize(this); + } + } } } diff --git a/Recyclable.Collections/RecyclableListExtensions.cs b/Recyclable.Collections/RecyclableListExtensions.cs index 9dc4d46..2799c22 100644 --- a/Recyclable.Collections/RecyclableListExtensions.cs +++ b/Recyclable.Collections/RecyclableListExtensions.cs @@ -15,17 +15,17 @@ namespace Recyclable.Collections } } - public static void CopyTo(this RecyclableList arrays, long startingIndex, int blockSize, int lastBlockSize, T[] destinationArray, int destinationArrayIndex) + public static void CopyTo(this T[][] 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.LongCount > 0) + if (arrays.LongLength > 0) { ReadOnlySpan sourceMemory = arrays[0].AsSpan(); var maxToCopy = (int)Math.Min(blockSize - startingIndex, arrayMemory.Length); - if (arrays.LongCount == 1) + if (arrays.LongLength == 1) { maxToCopy = (int)Math.Min(maxToCopy, lastBlockSize); } @@ -35,7 +35,7 @@ namespace Recyclable.Collections arrayMemory = arrayMemory[maxToCopy..]; } - for (var arrayIdx = startingArrayIndex + 1; arrayIdx < arrays.Count - 1 && arrayMemory.Length > 0; arrayIdx++) + for (var arrayIdx = startingArrayIndex + 1; arrayIdx < arrays.Length - 1 && arrayMemory.Length > 0; arrayIdx++) { ReadOnlySpan sourceMemory = arrays[arrayIdx].AsSpan(); var maxToCopy = Math.Min(blockSize, arrayMemory.Length); @@ -44,7 +44,7 @@ namespace Recyclable.Collections arrayMemory = arrayMemory[maxToCopy..]; } - if (arrays.Count > 1 && arrayMemory.Length > 0) + if (arrays.Length > 1 && arrayMemory.Length > 0) { ReadOnlySpan sourceMemory = arrays[^1].AsSpan(); var maxToCopy = Math.Min(lastBlockSize, arrayMemory.Length); @@ -56,35 +56,35 @@ namespace Recyclable.Collections //[MethodImpl(MethodImplOptions.AggressiveInlining)] //public static bool Contains(this RecyclableList arrays, T item) => arrays.Any(x => x.Contains(item)); - //public static IEnumerable Enumerate(this RecyclableList arrays, int chunkSize, long totalCount) - //{ - // long currentCount = 0; - // for (var arrayIdx = 0; arrayIdx < arrays.Count; arrayIdx++) - // { - // var array = arrays[arrayIdx]; - // currentCount += chunkSize; - // switch (currentCount < totalCount) - // { - // case true: - // for (var valueIdx = 0; valueIdx < chunkSize; valueIdx++) - // { - // yield return array[valueIdx]; - // } + public static IEnumerable Enumerate(this T[][] arrays, int chunkSize, long totalCount) + { + long currentCount = 0; + for (var arrayIdx = 0; arrayIdx < arrays.Length && currentCount < totalCount; arrayIdx++) + { + var array = arrays[arrayIdx]; + currentCount += chunkSize; + switch (currentCount < totalCount) + { + case true: + for (var valueIdx = 0; valueIdx < chunkSize; valueIdx++) + { + yield return array[valueIdx]; + } - // break; + 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]; - // } + 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; - // } - // } - //} + break; + } + } + } //[MethodImpl(MethodImplOptions.AggressiveInlining)] //public static T LimitTo(this T value, T maxValue, IComparer comparer) @@ -107,14 +107,26 @@ namespace Recyclable.Collections return -1; } - public static long LongIndexOf(this RecyclableArrayList arrays, int itemsCount, T itemToFind, IEqualityComparer comparer) + public static long LongIndexOf(this T[][] arrays, long itemsCount, T itemToFind, IEqualityComparer comparer) { - for (var itemIdx = 0; itemIdx < itemsCount; itemIdx++) + var arraysSpan = new Span(arrays); + var arraysCount = arraysSpan.Length; + for (int arrayIdx = 0, scannedCount = 0; arrayIdx < arraysCount; arrayIdx++) { - var item = arrays[itemIdx]; - if (comparer.Equals(itemToFind, item)) + var itemsSpan = new Span(arraysSpan[arrayIdx]); + var arrayItemsCount = itemsSpan.Length; + for (var itemIdx = 0; itemIdx < arrayItemsCount; itemIdx++) { - return itemIdx; + if (comparer.Equals(itemToFind, itemsSpan[itemIdx])) + { + return scannedCount; + } + + scannedCount++; + if (scannedCount == itemsCount) + { + return -1; + } } } diff --git a/Recyclable.Collections/RecyclableStack.cs b/Recyclable.Collections/RecyclableStack.cs index c8af95c..e103f4a 100644 --- a/Recyclable.Collections/RecyclableStack.cs +++ b/Recyclable.Collections/RecyclableStack.cs @@ -47,7 +47,7 @@ namespace Recyclable.Collections { var toRemove = List[LongCount - 1]; LongCount--; - if (List.Capacity - LongCount == _blockSize) + if (List.Capacity * _blockSize - LongCount == _blockSize) { List.RemoveBlock(List.BlockCount - 1); } diff --git a/Recyclable.CollectionsTests/RecyclableListTests.cs b/Recyclable.CollectionsTests/RecyclableListTests.cs index 43bef10..7cff822 100644 --- a/Recyclable.CollectionsTests/RecyclableListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableListTests.cs @@ -156,7 +156,7 @@ namespace Recyclable.CollectionsTests using var list = NewRecyclableList; // Act - _ = Assert.Throws(() => list.RemoveAt(0)); + _ = Assert.Throws(() => list.RemoveAt(0)); } [Fact]