diff --git a/README.md b/README.md index 4ca2a1b..81c658a 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,9 @@ 1. ✅ `RecyclableSortedList` 1. ✅ `RecyclableStack` 1. ✅ `RecyclableUnorderedList` - 1. 👉 Add support for `ReadOnlySpan` - 1. 🅿️ Implement list versioning to allow data change identification + 1. ✅ Add support for `ReadOnlySpan` 1. ✅ Release 0.0.3-alpha - 1. 🅿️ Implement `List` interfaces + 1. 👉 Implement `List` interfaces 1. 🅿️ `ICollection` 1. 🅿️ `IEnumerable` 1. 🅿️ `IEnumerable` @@ -87,6 +86,7 @@ 1. 🅿️ `IReadOnlyList` 1. 🅿️ `ICollection` 1. 🅿️ `IList` + 1. 🅿️ Implement list versioning to allow data change identification 1. 🅿️ Make sure that `NeedsClearing` is used & items are cleared in 1. 🅿️ `Clear` 1. 🅿️ `Dispose` @@ -111,6 +111,9 @@ 1. 🅿️ Make type castings `checked` 1. 🅿️ Port `RecyclableLongList` optimizations to `RecyclableList` 1. 🅿️ Release 0.0.3 +1. 🅿️ Implement `ILongList` interface + 1. 🅿️ `RecyclableList` + 1. 🅿️ `RecyclableLongList` 1. 🅿️ Implement `RecyclableQeueue` 1. 🅿️ Port `RecyclableLongList` optimizations to `RecyclableQueue` 1. 🅿️ Release 0.0.4 @@ -126,9 +129,11 @@ 1. 🅿️ Optimize `OneSizeArrayPool` 1. 🅿️ Review locks 1. 🅿️ Measure multi-threading performance + 1. 🅿️ Implement memory bucket disposal in high RAM pressure scenario 1. 🅿️ Review `RecyclableArrayPool` 1. 🅿️ Review locks 1. 🅿️ Measure multi-threading performance + 1. 🅿️ Implement array disposal in high RAM pressure scenario 1. 🅿️ Optimize `MemoryBucket` 1. 🅿️ Convert to `struct`, if possible 1. 🅿️ Find out if there are better replacements @@ -137,8 +142,11 @@ 1. 🅿️ `IndexOfSynchronizationContext` 1. 🅿️ `IndexOfSynchronizationContextPool` 1. 🅿️ `ManualResetEventSlimmer` + 1. 🅿️ Multi-threading benchmarks 1. 🅿️ `ManualResetEventSlimmerPool` + 1. 🅿️ Multi-threading benchmarks 1. 🅿️ `SpinLockSlimmer` + 1. 🅿️ Multi-threading benchmarks 1. 🅿️ Release 0.0.9-beta 1. 🅿️ Cleanup 1. 🅿️ Replace `LastBlockWithData` property with `_lastBlockWithData` field @@ -149,7 +157,18 @@ 1. 🅿️ `ListExtensions` 1. 🅿️ `MathUtils` 1. 🅿️ `SystemRandomNumberGenerator` +1. 🅿️ Review and remove warnings & hints + 1. 🅿️ Warnings + 1. 🅿️ Hints +1. Documentation + 1. 🅿️ Document differences in behavior + 1. 🅿️ Document other specifics 1. 🅿️ Release 1.0.0 +1. 🅿️ Optimize + 1. 🅿️ `RecyclableLongList.Resize` + 1. 🅿️ Check if we can benefit from Sse2 in `.IndexOf`/`.Contains` methods as given in [MS blog](https://devblogs.microsoft.com/dotnet/hardware-intrinsics-in-net-core/). +1. 🅿️ Add support for `ICollection` interface in `.AddRange` & `constructor` +1. 🅿️ Release 2.0.0 # Characteristics of the classes @@ -174,7 +193,7 @@ This is the direct equivalent of `List` class, except that the arrays are tak * 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, unless the no. of items exceeds the no. of allocated blocks. In that case, a new array of blocks is created to accommodate . 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. -* ⚠️ `IndexOf` will automatically switch to parallel search mode. Collections smaller than (currently) 850_000 items are guaranteed to find the 1st occurrence of the `item` parameter. Collections bigger than (currently) 850_000 items will be scanned using parallelization. If either case it'll return the index of the first matching item. This note also applies to `Contains`, because it internally uses `IndexOf`. The value of 850_000 may become configurable in the future, unless it proves to result in much worse performance. +* ⚠️ `IndexOf` will automatically switch to parallel search mode. Collections bigger than (currently) 850_000 items will be scanned using parallelization. In either case it'll return the index of the first matching item. This note also applies to `Contains`, because it internally uses `IndexOf`. The value of 850_000 may become configurable in the future, unless it proves to result in much worse performance. The parallelization utilizes default `Task` scheduling, resulting in non-deterministic order in which search tasks are executed. In most cases it resulted in significant performance improvements. Below you can find the benchmarks for various item counts. diff --git a/Recyclable.Collections.Benchmarks/Benchmarks/RecyclableCollections.Benchmarks.cs b/Recyclable.Collections.Benchmarks/Benchmarks/RecyclableCollections.Benchmarks.cs index c8560c2..901ac8b 100644 --- a/Recyclable.Collections.Benchmarks/Benchmarks/RecyclableCollections.Benchmarks.cs +++ b/Recyclable.Collections.Benchmarks/Benchmarks/RecyclableCollections.Benchmarks.cs @@ -68,6 +68,6 @@ namespace Recyclable.Collections.Benchmarks protected override Action? GetTestMethod(RecyclableCollectionsBenchmarkSource benchmarkType) => (Action?) GetType().GetMethod($"{benchmarkType}_{TestCase}", BindingFlags.Instance | BindingFlags.Public)?.CreateDelegate(typeof(Action), this) - ?? throw CreateMethodNotFoundException($"{benchmarkType}_{TestCase}", GetType().FullName); + ?? (Action?)GetType().GetMethod($"{benchmarkType}_AddRangeWhenSourceIs{benchmarkType}", BindingFlags.Instance | BindingFlags.Public)?.CreateDelegate(typeof(Action), this); } } diff --git a/Recyclable.Collections.Benchmarks/POC/PocBenchmarkBase.cs b/Recyclable.Collections.Benchmarks/POC/PocBenchmarkBase.cs index a033a39..b3ba2ad 100644 --- a/Recyclable.Collections.Benchmarks/POC/PocBenchmarkBase.cs +++ b/Recyclable.Collections.Benchmarks/POC/PocBenchmarkBase.cs @@ -45,7 +45,7 @@ namespace Recyclable.Collections.Benchmarks.POC // [Params(RecyclableDefaults.MaxPooledBlockSize)] // [Params(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 30, 32, 40, 50, 60, 80, 90, 100, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 10_000_000, 33_554_432, 50_000_000, 100_000_000)] [Params(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 30, 32, 40, 50, 60, 80, 90, 100, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072)] - //[Params(RecyclableDefaults.MinItemsCountForParallelization, 1_048_576, 2_097_152, 4_194_304, 8_388_608, 16_777_216, 33_554_432, RecyclableDefaults.MaxPooledBlockSize)] + // [Params(RecyclableDefaults.MinItemsCountForParallelization, 1_048_576, 2_097_152, 4_194_304, 8_388_608, 16_777_216, 33_554_432, RecyclableDefaults.MaxPooledBlockSize)] // [Params(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 30, 32, 40, 50, 60, 80, 90, 100, 128)] // [Params(1, 10, 1024, 10240, RecyclableDefaults.MinPooledArrayLength, RecyclableDefaults.MinItemsCountForParallelization)] // [Params(RecyclableDefaults.MinItemsCountForParallelization * 3)] diff --git a/Recyclable.Collections.Benchmarks/Program.cs b/Recyclable.Collections.Benchmarks/Program.cs index 26a8f3e..2608901 100644 --- a/Recyclable.Collections.Benchmarks/Program.cs +++ b/Recyclable.Collections.Benchmarks/Program.cs @@ -6,7 +6,7 @@ using Recyclable.Collections.Benchmarks.POC; public static class Program { static IConfig BenchmarkConfig { get; } = ManualConfig.Create(DefaultConfig.Instance) - .WithOptions(ConfigOptions.DisableOptimizationsValidator | ConfigOptions.JoinSummary | ConfigOptions.KeepBenchmarkFiles); + .WithOptions(ConfigOptions.DisableOptimizationsValidator | ConfigOptions.JoinSummary ); static void RunAssemblyBenchmarks() { diff --git a/Recyclable.Collections/RecyclableList.cs b/Recyclable.Collections/RecyclableList.cs index 19937d3..bbc85bf 100644 --- a/Recyclable.Collections/RecyclableList.cs +++ b/Recyclable.Collections/RecyclableList.cs @@ -12,6 +12,8 @@ namespace Recyclable.Collections protected T[] _memoryBlock; + public static explicit operator ReadOnlySpan(RecyclableList source) => new(source._memoryBlock, 0, source.Count); + private static void ThrowArgumentOutOfRangeException(in string message) { throw new ArgumentOutOfRangeException(message, (Exception?)null); @@ -92,6 +94,20 @@ namespace Recyclable.Collections AddRange(source); } +#pragma warning disable CS8618 // _memory will be initialized when the 1st item is added + public RecyclableList(RecyclableLongList source) +#pragma warning restore CS8618 + { + AddRange(source); + } + +#pragma warning disable CS8618 // _memory will be initialized when the 1st item is added + public RecyclableList(ReadOnlySpan source) +#pragma warning restore CS8618 + { + AddRange(source); + } + #pragma warning disable CS8618 // _memory will be initialized when the 1st item is added public RecyclableList(in T[] source) #pragma warning restore CS8618 @@ -188,6 +204,26 @@ namespace Recyclable.Collections _count = targetCapacity; } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public void AddRange(ReadOnlySpan itemsSpan) + { + if (itemsSpan.Length == 0) + { + return; + } + + int sourceItemsCount = itemsSpan.Length; + int targetCapacity = _count + sourceItemsCount; + if (_capacity < targetCapacity) + { + _ = EnsureCapacity(this, targetCapacity); + } + + var targetSpan = new Span(_memoryBlock, _count, sourceItemsCount); + itemsSpan.CopyTo(targetSpan); + _count = targetCapacity; + } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] public void AddRange(List items) { diff --git a/Recyclable.Collections/RecyclableLongList.cs b/Recyclable.Collections/RecyclableLongList.cs index e1ac32c..9394aa9 100644 --- a/Recyclable.Collections/RecyclableLongList.cs +++ b/Recyclable.Collections/RecyclableLongList.cs @@ -417,6 +417,186 @@ namespace Recyclable.Collections } } +#pragma warning disable CS8618 // It's set by SetupBlockArrayPooling + public RecyclableLongList(in T[] source, int minBlockSize = RecyclableDefaults.BlockSize, long? expectedItemsCount = default, ArrayPool? memoryBlocksPool = default, ArrayPool? blockArrayPool = default) +#pragma warning restore CS8618 + { + _memoryBlocksPool = memoryBlocksPool ?? _defaultMemoryBlocksPool; + if (expectedItemsCount > 0) + { + minBlockSize = checked((int)BitOperations.RoundUpToPowerOf2((uint)minBlockSize)); + SetupBlockArrayPooling(this, minBlockSize, blockArrayPool); + _capacity = Resize(this, minBlockSize, _blockSizePow2BitShift, expectedItemsCount.Value); + if (minBlockSize != _memoryBlocks![0].Length && _capacity > 0) + { + SetupBlockArrayPooling(this, _memoryBlocks[0].Length, blockArrayPool); + _blockSizeMinus1 = _blockSize - 1; + } + else + { + _blockSizeMinus1 = minBlockSize - 1; + } + } + else + { + SetupBlockArrayPooling(this, checked((int)BitOperations.RoundUpToPowerOf2((uint)minBlockSize)), blockArrayPool); + _blockSizeMinus1 = _blockSize - 1; + _memoryBlocks = _emptyMemoryBlocksArray; + } + + AddRange(source); + } + +#pragma warning disable CS8618 // It's set by SetupBlockArrayPooling + public RecyclableLongList(ReadOnlySpan source, int minBlockSize = RecyclableDefaults.BlockSize, long? expectedItemsCount = default, ArrayPool? memoryBlocksPool = default, ArrayPool? blockArrayPool = default) +#pragma warning restore CS8618 + { + _memoryBlocksPool = memoryBlocksPool ?? _defaultMemoryBlocksPool; + if (expectedItemsCount > 0) + { + minBlockSize = checked((int)BitOperations.RoundUpToPowerOf2((uint)minBlockSize)); + SetupBlockArrayPooling(this, minBlockSize, blockArrayPool); + _capacity = Resize(this, minBlockSize, _blockSizePow2BitShift, expectedItemsCount.Value); + if (minBlockSize != _memoryBlocks![0].Length && _capacity > 0) + { + SetupBlockArrayPooling(this, _memoryBlocks[0].Length, blockArrayPool); + _blockSizeMinus1 = _blockSize - 1; + } + else + { + _blockSizeMinus1 = minBlockSize - 1; + } + } + else + { + SetupBlockArrayPooling(this, checked((int)BitOperations.RoundUpToPowerOf2((uint)minBlockSize)), blockArrayPool); + _blockSizeMinus1 = _blockSize - 1; + _memoryBlocks = _emptyMemoryBlocksArray; + } + + AddRange(source); + } + +#pragma warning disable CS8618 // It's set by SetupBlockArrayPooling + public RecyclableLongList(RecyclableList source, int minBlockSize = RecyclableDefaults.BlockSize, long? expectedItemsCount = default, ArrayPool? memoryBlocksPool = default, ArrayPool? blockArrayPool = default) +#pragma warning restore CS8618 + { + _memoryBlocksPool = memoryBlocksPool ?? _defaultMemoryBlocksPool; + if (expectedItemsCount > 0) + { + minBlockSize = checked((int)BitOperations.RoundUpToPowerOf2((uint)minBlockSize)); + SetupBlockArrayPooling(this, minBlockSize, blockArrayPool); + _capacity = Resize(this, minBlockSize, _blockSizePow2BitShift, expectedItemsCount.Value); + if (minBlockSize != _memoryBlocks![0].Length && _capacity > 0) + { + SetupBlockArrayPooling(this, _memoryBlocks[0].Length, blockArrayPool); + _blockSizeMinus1 = _blockSize - 1; + } + else + { + _blockSizeMinus1 = minBlockSize - 1; + } + } + else + { + SetupBlockArrayPooling(this, checked((int)BitOperations.RoundUpToPowerOf2((uint)minBlockSize)), blockArrayPool); + _blockSizeMinus1 = _blockSize - 1; + _memoryBlocks = _emptyMemoryBlocksArray; + } + + AddRange(source); + } + +#pragma warning disable CS8618 // It's set by SetupBlockArrayPooling + public RecyclableLongList(RecyclableLongList source, int minBlockSize = RecyclableDefaults.BlockSize, long? expectedItemsCount = default, ArrayPool? memoryBlocksPool = default, ArrayPool? blockArrayPool = default) +#pragma warning restore CS8618 + { + _memoryBlocksPool = memoryBlocksPool ?? _defaultMemoryBlocksPool; + if (expectedItemsCount > 0) + { + minBlockSize = checked((int)BitOperations.RoundUpToPowerOf2((uint)minBlockSize)); + SetupBlockArrayPooling(this, minBlockSize, blockArrayPool); + _capacity = Resize(this, minBlockSize, _blockSizePow2BitShift, expectedItemsCount.Value); + if (minBlockSize != _memoryBlocks![0].Length && _capacity > 0) + { + SetupBlockArrayPooling(this, _memoryBlocks[0].Length, blockArrayPool); + _blockSizeMinus1 = _blockSize - 1; + } + else + { + _blockSizeMinus1 = minBlockSize - 1; + } + } + else + { + SetupBlockArrayPooling(this, checked((int)BitOperations.RoundUpToPowerOf2((uint)minBlockSize)), blockArrayPool); + _blockSizeMinus1 = _blockSize - 1; + _memoryBlocks = _emptyMemoryBlocksArray; + } + + AddRange(source); + } + +#pragma warning disable CS8618 // It's set by SetupBlockArrayPooling + public RecyclableLongList(List source, int minBlockSize = RecyclableDefaults.BlockSize, long? expectedItemsCount = default, ArrayPool? memoryBlocksPool = default, ArrayPool? blockArrayPool = default) +#pragma warning restore CS8618 + { + _memoryBlocksPool = memoryBlocksPool ?? _defaultMemoryBlocksPool; + if (expectedItemsCount > 0) + { + minBlockSize = checked((int)BitOperations.RoundUpToPowerOf2((uint)minBlockSize)); + SetupBlockArrayPooling(this, minBlockSize, blockArrayPool); + _capacity = Resize(this, minBlockSize, _blockSizePow2BitShift, expectedItemsCount.Value); + if (minBlockSize != _memoryBlocks![0].Length && _capacity > 0) + { + SetupBlockArrayPooling(this, _memoryBlocks[0].Length, blockArrayPool); + _blockSizeMinus1 = _blockSize - 1; + } + else + { + _blockSizeMinus1 = minBlockSize - 1; + } + } + else + { + SetupBlockArrayPooling(this, checked((int)BitOperations.RoundUpToPowerOf2((uint)minBlockSize)), blockArrayPool); + _blockSizeMinus1 = _blockSize - 1; + _memoryBlocks = _emptyMemoryBlocksArray; + } + + AddRange(source); + } + +#pragma warning disable CS8618 // It's set by SetupBlockArrayPooling + public RecyclableLongList(IList source, int minBlockSize = RecyclableDefaults.BlockSize, long? expectedItemsCount = default, ArrayPool? memoryBlocksPool = default, ArrayPool? blockArrayPool = default) +#pragma warning restore CS8618 + { + _memoryBlocksPool = memoryBlocksPool ?? _defaultMemoryBlocksPool; + if (expectedItemsCount > 0) + { + minBlockSize = checked((int)BitOperations.RoundUpToPowerOf2((uint)minBlockSize)); + SetupBlockArrayPooling(this, minBlockSize, blockArrayPool); + _capacity = Resize(this, minBlockSize, _blockSizePow2BitShift, expectedItemsCount.Value); + if (minBlockSize != _memoryBlocks![0].Length && _capacity > 0) + { + SetupBlockArrayPooling(this, _memoryBlocks[0].Length, blockArrayPool); + _blockSizeMinus1 = _blockSize - 1; + } + else + { + _blockSizeMinus1 = minBlockSize - 1; + } + } + else + { + SetupBlockArrayPooling(this, checked((int)BitOperations.RoundUpToPowerOf2((uint)minBlockSize)), blockArrayPool); + _blockSizeMinus1 = _blockSize - 1; + _memoryBlocks = _emptyMemoryBlocksArray; + } + + AddRange(source); + } + #pragma warning disable CS8618 // It's set by SetupBlockArrayPooling public RecyclableLongList(IEnumerable source, int minBlockSize = RecyclableDefaults.BlockSize, long? expectedItemsCount = default, ArrayPool? memoryBlocksPool = default, ArrayPool? blockArrayPool = default) #pragma warning restore CS8618 @@ -576,6 +756,53 @@ namespace Recyclable.Collections _lastBlockWithData = targetBlockIndex - (itemsSpan.Length > 0 ? 0 : 1); } + public void AddRange(ReadOnlySpan itemsSpan) + { + if (itemsSpan.Length == 0) + { + return; + } + + long targetCapacity = _longCount + itemsSpan.Length; + if (_capacity < targetCapacity) + { + _ = EnsureCapacity(this, targetCapacity); + } + + int blockSize = _blockSize, + targetBlockIndex = _nextItemBlockIndex, + memoryBlockCount = _reservedBlockCount; + + Span memoryBlocksSpan = new(_memoryBlocks, 0, memoryBlockCount); + Span targetBlockArraySpan = new(memoryBlocksSpan[targetBlockIndex], _nextItemIndex, blockSize - _nextItemIndex); + while (itemsSpan.Length >= targetBlockArraySpan.Length) + { + itemsSpan[..targetBlockArraySpan.Length].CopyTo(targetBlockArraySpan); + itemsSpan = itemsSpan[targetBlockArraySpan.Length..]; + if (itemsSpan.Length == 0) + { + break; + } + + targetBlockIndex++; + targetBlockArraySpan = new(memoryBlocksSpan[targetBlockIndex], 0, blockSize); + } + + if (itemsSpan.Length > 0) + { + itemsSpan.CopyTo(targetBlockArraySpan); + _nextItemBlockIndex = targetBlockIndex; + } + else + { + _nextItemBlockIndex = targetBlockIndex + 1; + } + + _nextItemIndex = itemsSpan.Length; + _longCount = targetCapacity; + _lastBlockWithData = targetBlockIndex; + } + public void AddRange(RecyclableLongList items) { if (items.LongCount == 0)