From 51d3613ffd93363364546048f5076dc0f722b354 Mon Sep 17 00:00:00 2001 From: mlemanczyk Date: Sat, 17 Jun 2023 16:08:06 +0200 Subject: [PATCH] Versioning implemented in RecyclableList --- README.md | 7 +- .../Pools/RecyclableCollectionVersionPool.cs | 20 ++ .../RecyclableCollectionVersion.cs | 41 +++ .../RecyclableList.Enumerator.cs | 79 ++++++ Recyclable.Collections/RecyclableList.cs | 237 +++++++++++++----- 5 files changed, 316 insertions(+), 68 deletions(-) create mode 100644 Recyclable.Collections/Pools/RecyclableCollectionVersionPool.cs create mode 100644 Recyclable.Collections/RecyclableCollectionVersion.cs create mode 100644 Recyclable.Collections/RecyclableList.Enumerator.cs diff --git a/README.md b/README.md index 0dd7340..15d2869 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,10 @@ 1. 🅿️ Review locks 1. 🅿️ Measure multi-threading performance 1. 🅿️ Implement memory bucket disposal in high RAM pressure scenario +1. 🅿️ Optimize `RecyclableCollectionVersionObjectPool` + 1. 🅿️ Implement custom `ObjectPool` + 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 @@ -166,9 +170,10 @@ 1. 🅿️ `RecyclableLongListExtensions.CopyTo` 1. 🅿️ Cleanup 1. 🅿️ Replace `LastBlockWithData` property with `_lastBlockWithData` field - 1. ✅ Cleanup `RecyclableLongListExtensions` + 1. ✅ `RecyclableLongListExtensions` 1. ✅ `ListExtensions` 1. 🅿️ `MathUtils` + 1. 🅿️ Resolve TODOs 1. 🅿️ Optimize 1. 🅿️ `ListExtensions` 1. 🅿️ `MathUtils` diff --git a/Recyclable.Collections/Pools/RecyclableCollectionVersionPool.cs b/Recyclable.Collections/Pools/RecyclableCollectionVersionPool.cs new file mode 100644 index 0000000..cba1b81 --- /dev/null +++ b/Recyclable.Collections/Pools/RecyclableCollectionVersionPool.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.ObjectPool; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Recyclable.Collections.Pools +{ + public class RecyclableCollectionVersionPool + { + private static readonly RecyclableCollectionVersionPool _shared = new(); + public static RecyclableCollectionVersionPool Shared => _shared; + + private ObjectPool _pool = new DefaultObjectPool(new DefaultPooledObjectPolicy()); + + public RecyclableCollectionVersion Get() => _pool.Get().Reset(); + public void Return(RecyclableCollectionVersion version) => _pool.Return(version); + } +} diff --git a/Recyclable.Collections/RecyclableCollectionVersion.cs b/Recyclable.Collections/RecyclableCollectionVersion.cs new file mode 100644 index 0000000..4d8781c --- /dev/null +++ b/Recyclable.Collections/RecyclableCollectionVersion.cs @@ -0,0 +1,41 @@ +using System.Runtime.CompilerServices; + +namespace Recyclable.Collections +{ + public sealed class RecyclableCollectionVersion + { + private ulong _enumeratorCount; + public ulong Version; + public bool IsVersioned; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Inc() => _ = unchecked(Version++); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddEnumerator() + { + if (unchecked(_enumeratorCount++) == 0) + { + IsVersioned = true; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveEnumerator() + { + if (unchecked(--_enumeratorCount) == 0) + { + IsVersioned = false; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public RecyclableCollectionVersion Reset() + { + IsVersioned = false; + Version = 0; + _enumeratorCount = 0; + return this; + } + } +} diff --git a/Recyclable.Collections/RecyclableList.Enumerator.cs b/Recyclable.Collections/RecyclableList.Enumerator.cs new file mode 100644 index 0000000..110d0c0 --- /dev/null +++ b/Recyclable.Collections/RecyclableList.Enumerator.cs @@ -0,0 +1,79 @@ +using System.Collections; + +namespace Recyclable.Collections +{ + public class RecyclableListEnumerator : IEnumerator + { + private static void ThrowListModifiedException() => throw new InvalidOperationException("Can't move to the next item, because the collection was modified. You must restart the enumeration by calling Reset(), if you want to enumerate the collection again"); + + private int _currentIndex = RecyclableDefaults.ItemNotFoundIndex; + private RecyclableList? _list; + private RecyclableCollectionVersion? _listVersion; + private ulong _version; + + private T? _current; + + public RecyclableListEnumerator(RecyclableList list) + { + _list = list; + _listVersion = list._version; + _listVersion!.AddEnumerator(); + _version = _listVersion!.Version; + } + + public T Current => _current ?? throw new InvalidOperationException("The enumerator wasn't initialized. You need to call MoveNext() before getting the current iterator value"); + object? IEnumerator.Current => _current; + + public void Dispose() + { + if (_listVersion != null) + { + _listVersion.RemoveEnumerator(); + _listVersion = null; + } + + if (_list != null) + { + _list = null; + } + + GC.SuppressFinalize(this); + } + + public bool MoveNext() + { + if (_list != null) + { + if (_list._version!.Version != _version) + { + ThrowListModifiedException(); + return false; + } + else if (_currentIndex < _list.Count - 1) + { + _currentIndex++; + _current = _list[_currentIndex]; + return true; + } + else + { + return false; + } + } + else + { + return false; + } + } + + public void Reset() + { + _currentIndex = RecyclableDefaults.ItemNotFoundIndex; + _current = default; + if (_list != null) + { + _version = _list._version!.Version; + } + } + } +} diff --git a/Recyclable.Collections/RecyclableList.cs b/Recyclable.Collections/RecyclableList.cs index 4f0366f..0aa3426 100644 --- a/Recyclable.Collections/RecyclableList.cs +++ b/Recyclable.Collections/RecyclableList.cs @@ -1,4 +1,5 @@ -using System.Buffers; +using Recyclable.Collections.Pools; +using System.Buffers; using System.Collections; using System.Runtime.CompilerServices; @@ -9,10 +10,65 @@ namespace Recyclable.Collections private static readonly ArrayPool _arrayPool = ArrayPool.Create(); private static readonly bool _defaultIsNull = default(T) == null; protected static readonly bool NeedsClearing = !typeof(T).IsValueType; - public static explicit operator ReadOnlySpan(RecyclableList source) => new(source._memoryBlock, 0, source.Count); - protected T[] _memoryBlock; - + private static void ThrowArgumentOutOfRangeException(in string argumentName, in string message) => throw new ArgumentOutOfRangeException(argumentName, message); + public static explicit operator ReadOnlySpan(RecyclableList source) => new(source._memoryBlock, 0, source.Count); + + protected T[] _memoryBlock; + internal RecyclableCollectionVersion? _version; + + public RecyclableCollectionVersion? Version => _version; + + protected IEnumerator AddRangeEnumerated(IEnumerable source, int growByCount) + { + int targetItemIdx = _count; + Span memorySpan; + + int i; + var enumerator = source.GetEnumerator(); + + int capacity = _capacity; + memorySpan = new(_memoryBlock); + if (enumerator.MoveNext()) + { + int available = capacity - targetItemIdx; + do + { + if (targetItemIdx + growByCount > capacity) + { + capacity = EnsureCapacity(this, capacity + growByCount); + memorySpan = new(_memoryBlock); + available = capacity - targetItemIdx; + } + + for (i = 0; i < available; i++) + { + memorySpan[targetItemIdx++] = enumerator.Current; + if (!enumerator.MoveNext()) + { + break; + } + } + } + while (i >= available); + } + + _count = targetItemIdx; + return enumerator; + } + + protected void AddRangeWithKnownCount(IEnumerable source, int targetItemIdx, int requiredAdditionalCapacity) + { + _ = EnsureCapacity(this, targetItemIdx + requiredAdditionalCapacity); + + Span memorySpan = new(_memoryBlock); + foreach (var item in source) + { + memorySpan[targetItemIdx++] = item; + } + + _count = targetItemIdx; + } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected static T[] Resize(in T[]? source, int newSize) @@ -137,7 +193,15 @@ namespace Recyclable.Collections get => _memoryBlock[index]; [MethodImpl(MethodImplOptions.AggressiveInlining)] - set => _memoryBlock[index] = value; + set + { + _memoryBlock[index] = value; + + if (_version?.IsVersioned ?? false) + { + _version.Inc(); + } + } } protected int _capacity; @@ -151,6 +215,11 @@ namespace Recyclable.Collections _memoryBlock = Resize(_memoryBlock, value); _capacity = _memoryBlock.Length; } + + if (_version?.IsVersioned ?? false) + { + _version.Inc(); + } } } @@ -161,10 +230,19 @@ namespace Recyclable.Collections get => _count; [MethodImpl(MethodImplOptions.AggressiveInlining)] - set => _count = value; + // TODO: Clear items if we're decreasing the size of the list, if T is reference type + set + { + _count = value; + + if (_version?.IsVersioned ?? false) + { + _version.Inc(); + } + } } - public bool IsReadOnly { get; } = false; + public bool IsReadOnly => false; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(T item) @@ -176,6 +254,11 @@ namespace Recyclable.Collections } _memoryBlock[_count++] = item; + + if (_version?.IsVersioned ?? false) + { + _version.Inc(); + } } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] @@ -197,6 +280,11 @@ namespace Recyclable.Collections Span itemsSpan = new(items); itemsSpan.CopyTo(targetSpan); _count = targetCapacity; + + if (_version?.IsVersioned ?? false) + { + _version.Inc(); + } } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] @@ -217,6 +305,11 @@ namespace Recyclable.Collections var targetSpan = new Span(_memoryBlock, _count, sourceItemsCount); itemsSpan.CopyTo(targetSpan); _count = targetCapacity; + + if (_version?.IsVersioned ?? false) + { + _version.Inc(); + } } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] @@ -236,6 +329,11 @@ namespace Recyclable.Collections items.CopyTo(_memoryBlock, _count); _count = targetCapacity; + + if (_version?.IsVersioned ?? false) + { + _version.Inc(); + } } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] @@ -255,6 +353,11 @@ namespace Recyclable.Collections items.CopyTo(_memoryBlock, _count); _count = targetCapacity; + + if (_version?.IsVersioned ?? false) + { + _version.Inc(); + } } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] @@ -276,6 +379,11 @@ namespace Recyclable.Collections Span itemsSpan = new(items._memoryBlock, 0, sourceItemsCount); itemsSpan.CopyTo(targetSpan); _count = targetCapacity; + + if (_version?.IsVersioned ?? false) + { + _version.Inc(); + } } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] @@ -329,6 +437,11 @@ namespace Recyclable.Collections } _count = targetCapacity; + + if (_version?.IsVersioned ?? false) + { + _version.Inc(); + } } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] @@ -337,72 +450,37 @@ namespace Recyclable.Collections if (source is T[] sourceArray) { AddRange(sourceArray); - return; } - - if (source is List sourceList) + else if (source is List sourceList) { AddRange(sourceList); - return; } - - if (source is RecyclableList sourceRecyclableList) + else if (source is RecyclableList sourceRecyclableList) { AddRange(sourceRecyclableList); - return; } - - if (source is IList sourceIList) + else if (source is IList sourceIList) { AddRange(sourceIList); - return; } - - int targetItemIdx = _count; - Span memorySpan; - if (source.TryGetNonEnumeratedCount(out var requiredAdditionalCapacity)) + else if (source.TryGetNonEnumeratedCount(out var requiredAdditionalCapacity)) { - _ = EnsureCapacity(this, targetItemIdx + requiredAdditionalCapacity); - memorySpan = new(_memoryBlock); - foreach (var item in source) + AddRangeWithKnownCount(source, _count, requiredAdditionalCapacity); + + if (_version?.IsVersioned ?? false) { - memorySpan[targetItemIdx++] = item; + _version.Inc(); } - - _count = targetItemIdx; - return; } - - int i; - using var enumerator = source.GetEnumerator(); - - int capacity = _capacity; - memorySpan = new(_memoryBlock); - if (enumerator.MoveNext()) + else { - int available = capacity - targetItemIdx; - do + AddRangeEnumerated(source, growByCount); + + if (_version?.IsVersioned ?? false) { - if (targetItemIdx + growByCount > capacity) - { - capacity = EnsureCapacity(this, capacity + growByCount); - memorySpan = new(_memoryBlock); - available = capacity - targetItemIdx; - } - - for (i = 0; i < available; i++) - { - memorySpan[targetItemIdx++] = enumerator.Current; - if (!enumerator.MoveNext()) - { - break; - } - } + _version.Inc(); } - while (i >= available); } - - _count = targetItemIdx; } public T[] AsArray => _memoryBlock; @@ -416,6 +494,11 @@ namespace Recyclable.Collections } _count = 0; + + if (_version?.IsVersioned ?? false) + { + _version.Inc(); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -424,16 +507,6 @@ namespace Recyclable.Collections [MethodImpl(MethodImplOptions.NoInlining)] public void CopyTo(T[] array, int index) => Array.Copy(_memoryBlock, 0, array, index, _count); - protected static IEnumerable Enumerate(RecyclableList list) - { - int count = list._count; - var memory = list._memoryBlock; - for (var i = 0; i < count; i++) - { - yield return memory[i]; - } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public int IndexOf(T itemToFind) => _count > 0 ? Array.IndexOf(_memoryBlock, itemToFind, 0, _count) : RecyclableDefaults.ItemNotFoundIndex; @@ -456,6 +529,11 @@ namespace Recyclable.Collections _memoryBlock[index] = item; _count++; + + if (_version?.IsVersioned ?? false) + { + _version.Inc(); + } } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] @@ -477,6 +555,11 @@ namespace Recyclable.Collections #pragma warning restore CS8601 } + if (_version?.IsVersioned ?? false) + { + _version.Inc(); + } + return true; } @@ -503,13 +586,26 @@ namespace Recyclable.Collections _memoryBlock[_count] = default; #pragma warning restore CS8601 } + + if (_version?.IsVersioned ?? false) + { + _version.Inc(); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IEnumerator GetEnumerator() => Enumerate(this).GetEnumerator(); + public IEnumerator GetEnumerator() + { + _version ??= RecyclableCollectionVersionPool.Shared.Get(); + return new RecyclableListEnumerator(this); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - IEnumerator IEnumerable.GetEnumerator() => Enumerate(this).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() + { + _version ??= RecyclableCollectionVersionPool.Shared.Get(); + return new RecyclableListEnumerator(this); + } public void Dispose() { @@ -522,6 +618,13 @@ namespace Recyclable.Collections _arrayPool.Return(_memoryBlock, NeedsClearing); } + if (_version != null) + { + var toReturn = _version; + _version = null; + RecyclableCollectionVersionPool.Shared.Return(toReturn); + } + GC.SuppressFinalize(this); } }