diff --git a/Recyclable.Collections/RecyclableList.Helpers.cs b/Recyclable.Collections/RecyclableList.Helpers.cs index d5926cd..02cfbf0 100644 --- a/Recyclable.Collections/RecyclableList.Helpers.cs +++ b/Recyclable.Collections/RecyclableList.Helpers.cs @@ -52,29 +52,49 @@ namespace Recyclable.Collections [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public static int EnsureCapacity(RecyclableList list, int requestedCapacity) { - int newCapacity; - switch (list._capacity > 0) + // int newCapacity; + // switch (list._capacity > 0) + // { + // case true: + int newCapacity = list._capacity; + while (newCapacity < requestedCapacity) { - case true: - newCapacity = list._capacity; - while (newCapacity < requestedCapacity) - { - newCapacity <<= 1; - } - - break; - - case false: - newCapacity = requestedCapacity; - break; + newCapacity <<= 1; } - list._memoryBlock = Resize(list._memoryBlock, newCapacity); - newCapacity = list._memoryBlock.Length; - list._capacity = newCapacity; - return newCapacity; + ResizeAndCopy(list, newCapacity); + // break; + + // case false: + // newCapacity = requestedCapacity; + // list._memoryBlock = Resize(list, newCapacity); + // break; + // } + + // newCapacity = list._memoryBlock.Length; + // list._capacity = newCapacity; + return list._capacity; } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static void EnsureExactCapacity(RecyclableList list, int requestedCapacity) + { + // int newCapacity; + // switch (list._capacity > 0) + // { + // case true: + ResizeAndCopy(list, requestedCapacity); + // break; + + // case false: + // newCapacity = requestedCapacity; + // list._memoryBlock = Resize(list, newCapacity); + // break; + // } + + // var newCapacity = list._memoryBlock.Length; + // list._capacity = list._memoryBlock.Length; + } [Obsolete("This method WILL be removed in the near future. Please use the existing enumerators instead of it.")] [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -237,31 +257,89 @@ namespace Recyclable.Collections return values[middle]; } - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static T[] Resize(in T[]? source, int newSize) + // [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + // public static T[] Resize(int newSize) + // // ArrayPool arrayPool = _arrayPool; + // => newSize >= RecyclableDefaults.MinPooledArrayLength + // ? _arrayPool.Rent(newSize) + // : new T[newSize]; + + internal static readonly ArrayPool _arrayPool = ArrayPool.Create(); + + private static readonly bool NeedsClearing = !typeof(T).IsValueType; + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static void ResizeAndCopy(RecyclableList sourceList, int newSize) { - ArrayPool arrayPool = _arrayPool; + // ArrayPool arrayPool = _arrayPool; T[] newMemoryBlock = newSize >= RecyclableDefaults.MinPooledArrayLength - ? arrayPool.Rent(newSize) + ? _arrayPool.Rent(newSize) : new T[newSize]; - if (source?.Length > 0) - { - Array.Copy(source!, 0, newMemoryBlock, 0, source!.Length); - if (source!.Length >= RecyclableDefaults.MinPooledArrayLength) - { - // TODO: Measure gain vs relying on arrayPool to clear - //if (NeedsClearing) - //{ - // Array.Clear(source); - //} + // & WAS SLOWER WITHOUT + T[] oldMemoryBlock = sourceList._memoryBlock; - // If anything, it has been already cleared above, so we don't need to repeat it. - arrayPool.Return(source, NeedsClearing); - } + // & WAS SLOWER AS ARRAY + // new Span(oldMemoryBlock).CopyTo(newMemoryBlock); + Array.Copy(oldMemoryBlock, newMemoryBlock, oldMemoryBlock.Length); + + if (oldMemoryBlock.Length >= RecyclableDefaults.MinPooledArrayLength) + { + // TODO: Measure gain vs relying on arrayPool to clear + //if (NeedsClearing) + //{ + // Array.Clear(source); + //} + + // If anything, it has been already cleared above, so we don't need to repeat it. + _arrayPool.Return(oldMemoryBlock, NeedsClearing); + //_arrayPool.Return(oldMemoryBlock, NeedsClearing); + } + + sourceList._memoryBlock = newMemoryBlock; + sourceList._capacity = newMemoryBlock.Length; } - return newMemoryBlock; + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static void ResizeAndCopy(RecyclableList sourceList) + { + // ArrayPool arrayPool = _arrayPool; + // int newSize = _capacity << 1; + if ((sourceList._capacity <<= 1) < RecyclableDefaults.MinPooledArrayLength) + { + Array.Resize(ref sourceList._memoryBlock, sourceList._capacity); + // _capacity <<= 1; + return; + } + + T[] newMemoryBlock = _arrayPool.Rent(sourceList._capacity); + //T[] newMemoryBlock = _arrayPool.Rent(sourceList._capacity); + // T[] newMemoryBlock = newSize >= RecyclableDefaults.MinPooledArrayLength + // ? RecyclableListHelpers._arrayPool.Rent(newSize) + // : new T[newSize]; + + // & WAS SLOWER WITHOUT + T[] oldMemoryBlock = sourceList._memoryBlock; + + // & WAS SLOWER AS ARRAY + new Span(oldMemoryBlock).CopyTo(newMemoryBlock); + // Array.Copy(oldMemoryBlock, newMemoryBlock, oldMemoryBlock.Length); + + if (oldMemoryBlock.Length >= RecyclableDefaults.MinPooledArrayLength) + { + // TODO: Measure gain vs relying on arrayPool to clear + //if (NeedsClearing) + //{ + // Array.Clear(source); + //} + + // If anything, it has been already cleared above, so we don't need to repeat it. + _arrayPool.Return(oldMemoryBlock, NeedsClearing); + //_arrayPool.Return(oldMemoryBlock, NeedsClearing); + } + + sourceList._memoryBlock = newMemoryBlock; + sourceList._capacity = newMemoryBlock.Length; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Recyclable.Collections/RecyclableList.cs b/Recyclable.Collections/RecyclableList.cs index e75b646..b3c9bc8 100644 --- a/Recyclable.Collections/RecyclableList.cs +++ b/Recyclable.Collections/RecyclableList.cs @@ -1,5 +1,4 @@ -using System.Buffers; -using System.Collections; +using System.Collections; using System.Runtime.CompilerServices; namespace Recyclable.Collections @@ -7,7 +6,7 @@ namespace Recyclable.Collections [Serializable] public sealed partial class RecyclableList : IList, IReadOnlyList, IDisposable { - private static readonly ArrayPool _arrayPool = ArrayPool.Create(); + // private static readonly T[] _emptyMemoryBlock = new T[0]; private static readonly bool NeedsClearing = !typeof(T).IsValueType; public static explicit operator ReadOnlySpan(RecyclableList source) => new(source._memoryBlock, 0, source.Count); @@ -74,23 +73,34 @@ namespace Recyclable.Collections public RecyclableList() #pragma warning restore CS8618 { + _capacity = 4; + _memoryBlock = new T[4]; } #pragma warning disable CS8618 // _memory will be initialized when the 1st item is added public RecyclableList(int initialCapacity) #pragma warning restore CS8618 { - if (initialCapacity > 0) + if (initialCapacity >= 4) { - _memoryBlock = RecyclableListHelpers.Resize(_memoryBlock, initialCapacity); + _memoryBlock = initialCapacity >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableListHelpers._arrayPool.Rent(initialCapacity) + : new T[initialCapacity]; _capacity = _memoryBlock.Length; } + else + { + _memoryBlock = new T[4]; + _capacity = 4; + } } #pragma warning disable CS8618 // _memory will be initialized when the 1st item is added public RecyclableList(RecyclableList source) #pragma warning restore CS8618 { + _capacity = 4; + _memoryBlock = new T[4]; AddRange(source); } @@ -98,6 +108,8 @@ namespace Recyclable.Collections public RecyclableList(RecyclableLongList source) #pragma warning restore CS8618 { + _capacity = 4; + _memoryBlock = new T[4]; AddRange(source); } @@ -105,6 +117,8 @@ namespace Recyclable.Collections public RecyclableList(ReadOnlySpan source) #pragma warning restore CS8618 { + _capacity = 4; + _memoryBlock = new T[4]; AddRange(source); } @@ -112,6 +126,8 @@ namespace Recyclable.Collections public RecyclableList(in T[] source) #pragma warning restore CS8618 { + _capacity = 4; + _memoryBlock = new T[4]; AddRange(source); } @@ -119,6 +135,8 @@ namespace Recyclable.Collections public RecyclableList(List source) #pragma warning restore CS8618 { + _capacity = 4; + _memoryBlock = new T[4]; AddRange(source); } @@ -126,13 +144,26 @@ namespace Recyclable.Collections public RecyclableList(IList source) #pragma warning restore CS8618 { + _capacity = 4; + _memoryBlock = new T[4]; AddRange(source); } public RecyclableList(IEnumerable source, int initialCapacity = RecyclableDefaults.Capacity) { - _memoryBlock = RecyclableListHelpers.Resize(_memoryBlock, initialCapacity); - _capacity = _memoryBlock.Length; + if (initialCapacity >= 4) + { + _memoryBlock = initialCapacity >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableListHelpers._arrayPool.Rent(initialCapacity) + : new T[initialCapacity]; + _capacity = _memoryBlock.Length; + } + else + { + _capacity = 4; + _memoryBlock = new T[4]; + } + AddRange(source); } @@ -157,8 +188,8 @@ namespace Recyclable.Collections { if (_capacity != value) { - _memoryBlock = RecyclableListHelpers.Resize(_memoryBlock, value); - _capacity = _memoryBlock.Length; + RecyclableListHelpers.ResizeAndCopy(this, value); + // _capacity = _memoryBlock.Length; _version++; } } @@ -169,15 +200,76 @@ namespace Recyclable.Collections public bool IsReadOnly => false; + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private void ResizeAndCopy() + { + // TODO: Measure performance + // ArrayPool arrayPool = _arrayPool; + // & WAS SLOWER + // int newSize = _capacity << 1; + // & WAS SLOWER + // if ((_capacity <<= 1) < RecyclableDefaults.MinPooledArrayLength) + // { + // Array.Resize(ref _memoryBlock, _capacity); + // // _capacity <<= 1; + // return; + // } + + // TODO: Measure performance after RecyclableArrayPool is reworked + // T[] newMemoryBlock = RecyclableListHelpers._arrayPool.Rent(_capacity <<= 1); + T[] newMemoryBlock = (_capacity <<= 1) >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableListHelpers._arrayPool.Rent(_capacity) + : new T[_capacity]; + + // & WAS SLOWER WITHOUT + T[] oldMemoryBlock = _memoryBlock; + new Span(oldMemoryBlock).CopyTo(newMemoryBlock); + // & WAS SLOWER + // Array.Copy(oldMemoryBlock, newMemoryBlock, oldMemoryBlock.Length); + + if (oldMemoryBlock.Length >= RecyclableDefaults.MinPooledArrayLength) + { + // TODO: Measure gain vs relying on arrayPool to clear + //if (NeedsClearing) + //{ + // Array.Clear(source); + //} + + // // If anything, it has been already cleared above, so we don't need to repeat it. + RecyclableListHelpers._arrayPool.Return(oldMemoryBlock, NeedsClearing); + } + + _memoryBlock = newMemoryBlock; + _capacity = newMemoryBlock.Length; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(T item) { - int requiredCapacity = _count + 1; - if (_capacity < requiredCapacity) + if (_count == _capacity) { - _ = RecyclableListHelpers.EnsureCapacity(this, requiredCapacity); + ResizeAndCopy(); + // & WAS SLOWER + // new Span(_memoryBlock)[_count++] = item; + // } + // else + // if (_capacity == _count) + // { + // break; + + // case false: + // newCapacity = requestedCapacity; + // list._memoryBlock = Resize(list, newCapacity); + // break; + // } + + // var newCapacity = list._memoryBlock.Length; + // _capacity = _memoryBlock.Length; } + // & WAS SLOWER + // new Span(_memoryBlock)[_count++] = item; + // new Span(_memoryBlock, _count++, 1)[0] = item; _memoryBlock[_count++] = item; _version++; } @@ -321,6 +413,7 @@ namespace Recyclable.Collections int blockIndex, sourceBlockSize = items.BlockSize, + // TODO: Convert to bit-shift lastBlockIndex = (int)(sourceItemsCount / sourceBlockSize) - 1; for (blockIndex = 0; blockIndex <= lastBlockIndex; blockIndex++) @@ -502,7 +595,7 @@ namespace Recyclable.Collections { _capacity = 0; // If anything, it has been already cleared by .Clear(), Remove() or RemoveAt() methods, as the list was modified / disposed. - _arrayPool.Return(_memoryBlock, false); + RecyclableListHelpers._arrayPool.Return(_memoryBlock, false); } GC.SuppressFinalize(this); diff --git a/Recyclable.CollectionsTests/RecyclableListTests.cs b/Recyclable.CollectionsTests/RecyclableListTests.cs index 327529e..28a5432 100644 --- a/Recyclable.CollectionsTests/RecyclableListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableListTests.cs @@ -86,7 +86,7 @@ namespace Recyclable.CollectionsTests } // Validate - _ = list.Capacity.Should().Be(0); + _ = list.Capacity.Should().Be(4); _ = list.Count.Should().Be(0); _ = list.Should().BeEmpty(); }