First implementation

This commit is contained in:
mlemanczyk committed 2023-01-02 01:32:29 +01:00
1 parent 98c02ef0ed
commit 6a58917120
12 files changed
+662

No files matched your search

+31
View File
@@ -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
@@ -0,0 +1,10 @@
namespace Recyclable.Collections
{
internal class FrequencyTupleComparer<TKey, TValue> : IComparer<(TKey Key, TValue Value)>
{
private static readonly IComparer<TKey> _baseComparer = Comparer<TKey>.Default;
public int Compare((TKey Key, TValue Value) x, (TKey Key, TValue Value) y)
=> _baseComparer.Compare(x.Key, y.Key);
}
}
@@ -0,0 +1,7 @@
namespace Recyclable.Collections
{
public interface IRandomNumberGenerator
{
int NextInt32(int startIndex, int endIndex);
}
}
+123
View File
@@ -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<T>(this IList<T> values, int startIndex, int endIndex, IComparer<T> 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<T>(this IList<T> values, IComparer<T> comparer)
{
values.InsertionSort(0, values.Count - 1, comparer);
}
public static void InsertionSort<T>(this IList<T> values)
{
values.InsertionSort(Comparer<T>.Default);
}
public static void QuickSort<T>(this IList<T> values, int startIndex, int endIndex, IComparer<T> 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<T>(this IList<T> values, IComparer<T> comparer, IRandomNumberGenerator randomNumberGenerator)
{
values.QuickSort(0, values.Count - 1, comparer, randomNumberGenerator);
}
public static void QuickSort<T>(this IList<T> values)
{
values.QuickSort(Comparer<T>.Default, new SystemRandomNumberGenerator());
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T SampleMedian<T>(this IList<T> values, int startIndex, int endIndex, IComparer<T> 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<T>(this IList<T> values, int xIndex, int yIndex)
{
(values[yIndex], values[xIndex]) = (values[xIndex], values[yIndex]);
}
}
}
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageId>Recyclable.Collections</PackageId>
<Version>0.0.1</Version>
<Authors>Marek Lemańczyk</Authors>
<Company>N/A</Company>
<Product>Recyclable.Collections</Product>
<PackageTags>Recyclable ArrayPool Array Pool List Sortable Sort</PackageTags>
<Description>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&lt;T&gt;'s interface and is targeted as direct replacements of List&lt;T&gt;, SortableList&lt;T&gt;, PriorityQueue&lt;T&gt; and similar.
The project &amp; source code is free for educational, open-source &amp; personal use.</Description>
</PropertyGroup>
<ItemGroup>
<Compile Remove="RecyclableSortedList.cs" />
</ItemGroup>
</Project>
@@ -0,0 +1,7 @@
namespace Recyclable.Collections
{
public class RecyclableDefaults
{
public const int BlockSize = 10_240;
}
}
+220
View File
@@ -0,0 +1,220 @@
using System.Buffers;
using System.Collections;
namespace Recyclable.Collections
{
public delegate void ItemAdded<T>(long index, T item);
public class RecyclableList<T> : IDisposable, IList<T>
{
private static readonly IEqualityComparer<T> _comparer = EqualityComparer<T>.Default;
private readonly int _blockSize;
private readonly List<T[]> _arrays = new();
private bool _disposedValue;
private long _capacity;
private ItemAdded<T>? _itemAdded;
public event ItemAdded<T> ItemAdded { add => _itemAdded += value; remove => _itemAdded -= value; }
private static IEnumerable<T> EnumerateElements(List<T[]> 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<T>.Shared.Rent(minSize);
private static void ReturnArray(in T[] array) => ArrayPool<T>.Shared.Return(array);
public RecyclableList(int blockSize = RecyclableDefaults.BlockSize)
{
_blockSize = blockSize;
}
public RecyclableList(IEnumerable<T> 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<T> arrayMemory = array.AsSpan()[arrayIndex..];
for (var arrayIdx = 0; arrayIdx < _arrays.Count - 1; arrayIdx++)
{
T[] partialArray = _arrays[arrayIdx];
ReadOnlySpan<T> sourceMemory = partialArray.AsSpan();
sourceMemory = sourceMemory[.._blockSize];
sourceMemory.CopyTo(arrayMemory);
arrayMemory = arrayMemory[_blockSize..];
}
if (_arrays.Count > 0)
{
ReadOnlySpan<T> sourceMemory = _arrays[^1].AsSpan();
var maxCount = Count % _blockSize;
sourceMemory = maxCount > 0
? sourceMemory[..maxCount]
: sourceMemory[.._blockSize];
sourceMemory.CopyTo(arrayMemory);
}
}
public IEnumerator<T> GetEnumerator() => EnumerateElements(_arrays, _blockSize, LongCount).GetEnumerator();
public int IndexOf(T item)
{
for (var arrayIdx = 0; arrayIdx < _arrays.Count; arrayIdx++)
{
var array = _arrays[arrayIdx];
Span<T> 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);
}
}
}
@@ -0,0 +1,8 @@
namespace Recyclable.Collections
{
public static class RecyclableListExtensions
{
public static RecyclableList<T> ToRecyclableList<T>(this IList<T> values, int blockSize = RecyclableDefaults.BlockSize) => new(values, blockSize);
public static RecyclableList<T> ToRecyclableList<T>(this IEnumerable<T> values, int blockSize = RecyclableDefaults.BlockSize) => new(values, blockSize);
}
}
@@ -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);
}
}
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Compile Remove="RecyclableSortedListTests.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Recyclable.Collections\Recyclable.Collections.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,193 @@
using FluentAssertions;
using Recyclable.Collections;
namespace Recyclable.CollectionsTests
{
[TestClass()]
public class RecyclableListTests
{
private const int _totalObjectCount = 5000;
private static RecyclableList<int> NewRecyclableList => CreateReversedRecyclableList(_testData);
private static readonly IEnumerable<int> _testData = Enumerable.Range(1, _totalObjectCount);
private static RecyclableList<int> CreateReversedRecyclableList(IEnumerable<int> source) => new(source);
[TestMethod]
public void AddShouldAddItems()
{
// Prepare
using var sut = new RecyclableList<int>();
// 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<int>();
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<int>(testNumbers);
// Validate
_ = sut.LongCount.Should().Be(testNumbers.Length);
_ = sut.Should().BeEquivalentTo(testNumbers);
}
[TestMethod]
public void InitializeSortClearShouldSucceed()
{
var expectedList = _testData;
// Act
using RecyclableList<int> 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<int>();
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();
}
}
}
+1
View File
@@ -0,0 +1 @@
global using Microsoft.VisualStudio.TestTools.UnitTesting;