From 6a58917120f2be59f8346519397efa3ba9c9ed7a Mon Sep 17 00:00:00 2001 From: mlemanczyk Date: Mon, 2 Jan 2023 01:32:29 +0100 Subject: [PATCH] First implementation --- Recyclable.Collections.sln | 31 +++ .../FrequencyTupleComparer.cs | 10 + .../IRandomNumberGenerator.cs | 7 + Recyclable.Collections/ListExtensions.cs | 123 ++++++++++ .../Recyclable.Collections.csproj | 23 ++ Recyclable.Collections/RecyclableDefaults.cs | 7 + Recyclable.Collections/RecyclableList.cs | 220 ++++++++++++++++++ .../RecyclableListExtensions.cs | 8 + .../SystemRandomNumberGenerator.cs | 9 + .../Recyclable.CollectionsTests.csproj | 30 +++ .../RecyclableListTests.cs | 193 +++++++++++++++ Recyclable.CollectionsTests/Usings.cs | 1 + 12 files changed, 662 insertions(+) create mode 100644 Recyclable.Collections.sln create mode 100644 Recyclable.Collections/FrequencyTupleComparer.cs create mode 100644 Recyclable.Collections/IRandomNumberGenerator.cs create mode 100644 Recyclable.Collections/ListExtensions.cs create mode 100644 Recyclable.Collections/Recyclable.Collections.csproj create mode 100644 Recyclable.Collections/RecyclableDefaults.cs create mode 100644 Recyclable.Collections/RecyclableList.cs create mode 100644 Recyclable.Collections/RecyclableListExtensions.cs create mode 100644 Recyclable.Collections/SystemRandomNumberGenerator.cs create mode 100644 Recyclable.CollectionsTests/Recyclable.CollectionsTests.csproj create mode 100644 Recyclable.CollectionsTests/RecyclableListTests.cs create mode 100644 Recyclable.CollectionsTests/Usings.cs diff --git a/Recyclable.Collections.sln b/Recyclable.Collections.sln new file mode 100644 index 0000000..dd3c5b6 --- /dev/null +++ b/Recyclable.Collections.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33205.214 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Recyclable.Collections", "Recyclable.Collections\Recyclable.Collections.csproj", "{7EC0F96D-FB4C-418A-9172-612CC5BCE705}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Recyclable.CollectionsTests", "Recyclable.CollectionsTests\Recyclable.CollectionsTests.csproj", "{2B7223FA-2D5B-40BF-9BD9-9D6B05DC18E2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7EC0F96D-FB4C-418A-9172-612CC5BCE705}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EC0F96D-FB4C-418A-9172-612CC5BCE705}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EC0F96D-FB4C-418A-9172-612CC5BCE705}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EC0F96D-FB4C-418A-9172-612CC5BCE705}.Release|Any CPU.Build.0 = Release|Any CPU + {2B7223FA-2D5B-40BF-9BD9-9D6B05DC18E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B7223FA-2D5B-40BF-9BD9-9D6B05DC18E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B7223FA-2D5B-40BF-9BD9-9D6B05DC18E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B7223FA-2D5B-40BF-9BD9-9D6B05DC18E2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2C612762-D1AF-4E88-9427-B4750FBECB1A} + EndGlobalSection +EndGlobal diff --git a/Recyclable.Collections/FrequencyTupleComparer.cs b/Recyclable.Collections/FrequencyTupleComparer.cs new file mode 100644 index 0000000..b378e6f --- /dev/null +++ b/Recyclable.Collections/FrequencyTupleComparer.cs @@ -0,0 +1,10 @@ +namespace Recyclable.Collections +{ + internal class FrequencyTupleComparer : IComparer<(TKey Key, TValue Value)> + { + private static readonly IComparer _baseComparer = Comparer.Default; + + public int Compare((TKey Key, TValue Value) x, (TKey Key, TValue Value) y) + => _baseComparer.Compare(x.Key, y.Key); + } +} diff --git a/Recyclable.Collections/IRandomNumberGenerator.cs b/Recyclable.Collections/IRandomNumberGenerator.cs new file mode 100644 index 0000000..9199fe4 --- /dev/null +++ b/Recyclable.Collections/IRandomNumberGenerator.cs @@ -0,0 +1,7 @@ +namespace Recyclable.Collections +{ + public interface IRandomNumberGenerator + { + int NextInt32(int startIndex, int endIndex); + } +} \ No newline at end of file diff --git a/Recyclable.Collections/ListExtensions.cs b/Recyclable.Collections/ListExtensions.cs new file mode 100644 index 0000000..ad01588 --- /dev/null +++ b/Recyclable.Collections/ListExtensions.cs @@ -0,0 +1,123 @@ +using System.Runtime.CompilerServices; + +namespace Recyclable.Collections +{ + // The source code was taken from https://codereview.stackexchange.com/questions/205407/quicksort-without-recursion. + // It was only beautified for .Net 6.0+. + public static class ListExtensions + { + public static void InsertionSort(this IList values, int startIndex, int endIndex, IComparer comparer) + { + var left = startIndex; + while (left < endIndex) + { + int right = left; + T? temp = values[++left]; + while (right >= startIndex && 0 < comparer.Compare(values[right], temp)) + { + values[right + 1] = values[right--]; + } + + values[right + 1] = temp; + } + } + public static void InsertionSort(this IList values, IComparer comparer) + { + values.InsertionSort(0, values.Count - 1, comparer); + } + public static void InsertionSort(this IList values) + { + values.InsertionSort(Comparer.Default); + } + public static void QuickSort(this IList values, int startIndex, int endIndex, IComparer comparer, IRandomNumberGenerator randomNumberGenerator) + { + var range = (startIndex, endIndex); + var stack = new Stack<(int, int)>(); + + do + { + startIndex = range.startIndex; + endIndex = range.endIndex; + + if (endIndex - startIndex + 1 < 31) + { + values.InsertionSort(startIndex, endIndex, comparer); + + continue; + } + + var pivot = values.SampleMedian(startIndex, endIndex, comparer, randomNumberGenerator); + var left = startIndex; + var right = endIndex; + + while (left <= right) + { + while (0 > comparer.Compare(values[left], pivot)) + { + left++; + } + + while (0 > comparer.Compare(pivot, values[right])) + { + right--; + } + + if (left <= right) + { + values.Swap(left++, right--); + } + } + + if (startIndex < right) + { + stack.Push((startIndex, right)); + } + + if (left < endIndex) + { + stack.Push((left, endIndex)); + } + } + while (stack.TryPop(out range)); + } + public static void QuickSort(this IList values, IComparer comparer, IRandomNumberGenerator randomNumberGenerator) + { + values.QuickSort(0, values.Count - 1, comparer, randomNumberGenerator); + } + public static void QuickSort(this IList values) + { + values.QuickSort(Comparer.Default, new SystemRandomNumberGenerator()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T SampleMedian(this IList values, int startIndex, int endIndex, IComparer comparer, IRandomNumberGenerator randomNumberGenerator) + { + var left = randomNumberGenerator.NextInt32(startIndex, endIndex); + var middle = randomNumberGenerator.NextInt32(startIndex, endIndex); + var right = randomNumberGenerator.NextInt32(startIndex, endIndex); + + if (0 > comparer.Compare(values[right], values[left])) + { + values.Swap(right, left); + } + + if (0 > comparer.Compare(values[middle], values[left])) + { + values.Swap(middle, left); + } + + if (0 > comparer.Compare(values[right], values[middle])) + { + values.Swap(right, middle); + } + + return values[middle]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Swap(this IList values, int xIndex, int yIndex) + { + (values[yIndex], values[xIndex]) = (values[xIndex], values[yIndex]); + } + } +} diff --git a/Recyclable.Collections/Recyclable.Collections.csproj b/Recyclable.Collections/Recyclable.Collections.csproj new file mode 100644 index 0000000..d48316f --- /dev/null +++ b/Recyclable.Collections/Recyclable.Collections.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + net6.0;net7.0 + enable + enable + Recyclable.Collections + 0.0.1 + Marek Lemańczyk + N/A + Recyclable.Collections + Recyclable ArrayPool Array Pool List Sortable Sort + Recyclable.Collections project is an open source framework for operating dynamic lists at performance close to raw arrays. It aims at providing high performance with minimal memory footprint. It implements IList<T>'s interface and is targeted as direct replacements of List<T>, SortableList<T>, PriorityQueue<T> and similar. + + The project & source code is free for educational, open-source & personal use. + + + + + + + diff --git a/Recyclable.Collections/RecyclableDefaults.cs b/Recyclable.Collections/RecyclableDefaults.cs new file mode 100644 index 0000000..e726503 --- /dev/null +++ b/Recyclable.Collections/RecyclableDefaults.cs @@ -0,0 +1,7 @@ +namespace Recyclable.Collections +{ + public class RecyclableDefaults + { + public const int BlockSize = 10_240; + } +} diff --git a/Recyclable.Collections/RecyclableList.cs b/Recyclable.Collections/RecyclableList.cs new file mode 100644 index 0000000..76d836b --- /dev/null +++ b/Recyclable.Collections/RecyclableList.cs @@ -0,0 +1,220 @@ +using System.Buffers; +using System.Collections; + +namespace Recyclable.Collections +{ + public delegate void ItemAdded(long index, T item); + + public class RecyclableList : IDisposable, IList + { + private static readonly IEqualityComparer _comparer = EqualityComparer.Default; + + private readonly int _blockSize; + private readonly List _arrays = new(); + private bool _disposedValue; + private long _capacity; + + private ItemAdded? _itemAdded; + public event ItemAdded ItemAdded { add => _itemAdded += value; remove => _itemAdded -= value; } + + private static IEnumerable EnumerateElements(List 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]; + } + + 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; + } + } + } + + private static T[] RentArray(int minSize) => ArrayPool.Shared.Rent(minSize); + private static void ReturnArray(in T[] array) => ArrayPool.Shared.Return(array); + + public RecyclableList(int blockSize = RecyclableDefaults.BlockSize) + { + _blockSize = blockSize; + } + + public RecyclableList(IEnumerable source, int blockSize = RecyclableDefaults.BlockSize) + { + _blockSize = blockSize; + foreach (var item in source) + { + Add(item); + } + } + + public T this[long index] + { + get => _arrays[(int)(index / _blockSize)][index % _blockSize]; + + set + { + _arrays[(int)(index / _blockSize)][index % _blockSize] = value; + _itemAdded?.Invoke(index, value); + } + } + + public T this[int index] + { + get => this[(long)index]; + set => this[(long)index] = value; + } + + public int Count => LongCount <= int.MaxValue ? (int)LongCount : int.MaxValue; + public long LongCount { get; protected set; } + public bool IsReadOnly { get; } = false; + + public void Add(T item) + { + if (LongCount == _capacity) + { + var newArray = RentArray(_blockSize); + _arrays.Add(newArray); + _capacity += _blockSize; + } + + var newIndex = LongCount; + this[newIndex] = item; + // Don't merge these 2 lines, otherwise we'll have increased count + // before the item is actually added. + LongCount++; + _itemAdded?.Invoke(newIndex, item); + } + + public void Clear() + { + try + { + for (var arrayIdx = 0; arrayIdx < _arrays.Count; arrayIdx++) + { + try + { + ReturnArray(_arrays[arrayIdx]); + } + catch (Exception) + { + // We want to try returning as many arrays, as possible, before + // the list is cleared. + } + } + } + finally + { + // Make sure the list is cleared so that we no longer use any of the arrays + LongCount = 0; + _arrays.Clear(); + } + } + + public bool Contains(T item) => _arrays.Any(x => x.Contains(item)); + + public void CopyTo(T[] array, int arrayIndex) + { + Span arrayMemory = array.AsSpan()[arrayIndex..]; + for (var arrayIdx = 0; arrayIdx < _arrays.Count - 1; arrayIdx++) + { + T[] partialArray = _arrays[arrayIdx]; + ReadOnlySpan sourceMemory = partialArray.AsSpan(); + sourceMemory = sourceMemory[.._blockSize]; + sourceMemory.CopyTo(arrayMemory); + arrayMemory = arrayMemory[_blockSize..]; + } + + if (_arrays.Count > 0) + { + ReadOnlySpan sourceMemory = _arrays[^1].AsSpan(); + var maxCount = Count % _blockSize; + sourceMemory = maxCount > 0 + ? sourceMemory[..maxCount] + : sourceMemory[.._blockSize]; + + sourceMemory.CopyTo(arrayMemory); + } + } + + public IEnumerator GetEnumerator() => EnumerateElements(_arrays, _blockSize, LongCount).GetEnumerator(); + + public int IndexOf(T item) + { + for (var arrayIdx = 0; arrayIdx < _arrays.Count; arrayIdx++) + { + var array = _arrays[arrayIdx]; + Span arrayMemory = ((T[]?)array).AsSpan(); + for (var memoryIdx = 0; memoryIdx < arrayMemory.Length; memoryIdx++) + { + if (_comparer.Equals(arrayMemory[memoryIdx], item)) + { + return (arrayIdx * _blockSize) + memoryIdx; + } + } + } + + return -1; + } + + public void Insert(int index, T item) + { + throw new NotSupportedException(); + } + + public bool Remove(T item) + { + throw new NotSupportedException(); + } + + public void RemoveAt(int index) + { + throw new NotSupportedException(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + Clear(); + } + + _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/RecyclableListExtensions.cs b/Recyclable.Collections/RecyclableListExtensions.cs new file mode 100644 index 0000000..9433010 --- /dev/null +++ b/Recyclable.Collections/RecyclableListExtensions.cs @@ -0,0 +1,8 @@ +namespace Recyclable.Collections +{ + public static class RecyclableListExtensions + { + public static RecyclableList ToRecyclableList(this IList values, int blockSize = RecyclableDefaults.BlockSize) => new(values, blockSize); + public static RecyclableList ToRecyclableList(this IEnumerable values, int blockSize = RecyclableDefaults.BlockSize) => new(values, blockSize); + } +} diff --git a/Recyclable.Collections/SystemRandomNumberGenerator.cs b/Recyclable.Collections/SystemRandomNumberGenerator.cs new file mode 100644 index 0000000..c5c5358 --- /dev/null +++ b/Recyclable.Collections/SystemRandomNumberGenerator.cs @@ -0,0 +1,9 @@ +namespace Recyclable.Collections +{ + public class SystemRandomNumberGenerator : IRandomNumberGenerator + { + private static readonly Random _random = new(); + + public int NextInt32(int startIndex, int endIndex) => _random.Next(startIndex, endIndex); + } +} \ No newline at end of file diff --git a/Recyclable.CollectionsTests/Recyclable.CollectionsTests.csproj b/Recyclable.CollectionsTests/Recyclable.CollectionsTests.csproj new file mode 100644 index 0000000..df55634 --- /dev/null +++ b/Recyclable.CollectionsTests/Recyclable.CollectionsTests.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Recyclable.CollectionsTests/RecyclableListTests.cs b/Recyclable.CollectionsTests/RecyclableListTests.cs new file mode 100644 index 0000000..1746bfe --- /dev/null +++ b/Recyclable.CollectionsTests/RecyclableListTests.cs @@ -0,0 +1,193 @@ +using FluentAssertions; +using Recyclable.Collections; + +namespace Recyclable.CollectionsTests +{ + [TestClass()] + public class RecyclableListTests + { + private const int _totalObjectCount = 5000; + private static RecyclableList NewRecyclableList => CreateReversedRecyclableList(_testData); + private static readonly IEnumerable _testData = Enumerable.Range(1, _totalObjectCount); + private static RecyclableList CreateReversedRecyclableList(IEnumerable source) => new(source); + + [TestMethod] + public void AddShouldAddItems() + { + // Prepare + using var sut = new RecyclableList(); + + // Act + const int itemsCount = 5; + foreach (var index in Enumerable.Range(1, itemsCount)) + { + sut.Add(index); + } + + // Validate + _ = sut.Count.Should().Be(itemsCount, "we added so many items"); + for (var index = 0; index < itemsCount; index++) + { + var actual = sut[index]; + _ = actual.Should().Be(index + 1); + } + } + + [TestMethod] + public void AddShouldAcceptDuplicates() + { + // Prepare + int[] testNumbers = new[] { 1, 2, 2, 3 }; + + // Act + using var sut = new RecyclableList(); + foreach (var index in testNumbers) + { + sut.Add(index); + } + + // Validate + _ = sut.LongCount.Should().Be(testNumbers.Length); + _ = sut.Should().BeEquivalentTo(testNumbers); + } + + [TestMethod] + public void ConstructorShouldAcceptDuplicates() + { + // Prepare + int[] testNumbers = new[] { 1, 2, 2, 3 }; + + // Act + using var sut = new RecyclableList(testNumbers); + + // Validate + _ = sut.LongCount.Should().Be(testNumbers.Length); + _ = sut.Should().BeEquivalentTo(testNumbers); + } + + [TestMethod] + public void InitializeSortClearShouldSucceed() + { + var expectedList = _testData; + + // Act + using RecyclableList actualList = NewRecyclableList; + actualList.QuickSort(); + + // Validate + _ = actualList.LongCount.Should().Be(_totalObjectCount); + _ = actualList.Count.Should().Be(_totalObjectCount); + _ = actualList.Should().Equal(expectedList); + _ = expectedList.Should().AllSatisfy(expected => actualList.Contains(expected)); + + // Act + actualList.Clear(); + + // Validate + _ = actualList.LongCount.Should().Be(0); + _ = actualList.Count.Should().Be(0); + } + + [TestMethod] + public void CopyToShouldCopyAllItems() + { + // Prepare + using var sut = NewRecyclableList; + + // Act + var actual = new int[sut.Count]; + sut.CopyTo(actual, 0); + _ = actual.Should().BeEquivalentTo(_testData); + } + + [TestMethod] + public void GetEnumeratorShouldYieldAllItems() + { + // Prepare + using var sut = NewRecyclableList; + + // Act + var actual = new List(); + var enumerator = sut.GetEnumerator(); + while (enumerator.MoveNext()) + { + actual.Add(enumerator.Current); + } + + // Validate + _ = actual.Should().BeEquivalentTo(_testData); + } + + [TestMethod] + public void IndexOfShouldFindTheIndexes() + { + // Prepare + using var sut = NewRecyclableList; + + // Act & Validate + foreach(var index in _testData) + { + var actual = sut.IndexOf(index); + _ = actual.Should().Be(index - 1); + } + } + + [TestMethod] + [ExpectedException(typeof(NotSupportedException))] + public void InsertShouldRaiseNotSupportedException() + { + // Prepare + using var sut = NewRecyclableList; + + // Act + sut.Insert(0, -1); + } + + [TestMethod] + [ExpectedException(typeof(NotSupportedException))] + public void RemoveShouldRaiseNotSupportedException() + { + // Prepare + using var sut = NewRecyclableList; + + // Act + bool removed = sut.Remove(_testData.First()); + _ = removed.Should().BeFalse(); + } + + [TestMethod] + [ExpectedException(typeof(NotSupportedException))] + public void RemoveAtShouldRaiseNotSupportedException() + { + // Prepare + using var sut = NewRecyclableList; + + // Act + sut.RemoveAt(0); + } + + [TestMethod] + public void DisposeShouldSucceed() + { + // Prepare + using var sut = NewRecyclableList; + + // Act + sut.Dispose(); + + // Validate + _ = sut.LongCount.Should().Be(0L); + } + + [TestMethod] + public void ConsecutiveDisposeShouldSucceed() + { + // Prepare + using var sut = NewRecyclableList; + sut.Dispose(); + + // Act + sut.Dispose(); + } + } +} \ No newline at end of file diff --git a/Recyclable.CollectionsTests/Usings.cs b/Recyclable.CollectionsTests/Usings.cs new file mode 100644 index 0000000..ab67c7e --- /dev/null +++ b/Recyclable.CollectionsTests/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file