diff --git a/perf/ListPool.Benchmarks/ListPool.Benchmarks.csproj b/perf/ListPool.Benchmarks/ListPool.Benchmarks.csproj index edb391f..ec423fc 100644 --- a/perf/ListPool.Benchmarks/ListPool.Benchmarks.csproj +++ b/perf/ListPool.Benchmarks/ListPool.Benchmarks.csproj @@ -8,6 +8,7 @@ + diff --git a/perf/ListPool.Benchmarks/ListPoolAddBenchmarks.cs b/perf/ListPool.Benchmarks/ListPoolAddBenchmarks.cs index b01f346..144e3bb 100644 --- a/perf/ListPool.Benchmarks/ListPoolAddBenchmarks.cs +++ b/perf/ListPool.Benchmarks/ListPoolAddBenchmarks.cs @@ -11,53 +11,43 @@ namespace ListPool.Benchmarks [GcConcurrent] public class ListPoolAddBenchmarks { - private List _list; - private ListPool _listPool; - private ValueListPool _valueListPool; - - [Params(1000)] + [Params(100, 1000, 10000)] public int N { get; set; } - [IterationSetup] - public void IterationSetup() - { - _list = new List(N); - _listPool = new ListPool(N); - _valueListPool = new ValueListPool(N); - } - - [IterationCleanup] - public void IterationCleanup() - { - _listPool.Dispose(); - _valueListPool.Dispose(); - } - [Benchmark(Baseline = true)] - public void List() + public int List() { - for (int i = 0; i < N - 1; i++) + List list = new List(N); + for (int i = 0; i < N; i++) { - _list.Add(i); + list.Add(i); } + + return list.Count; } [Benchmark] - public void ListPool() + public int ListPool() { - for (int i = 0; i < N - 1; i++) + using ListPool list = new ListPool(N); + for (int i = 0; i < N; i++) { - _listPool.Add(i); + list.Add(i); } + + return list.Count; } [Benchmark] - public void ValueListPool() + public int ValueListPool() { - for (int i = 0; i < N - 1; i++) + using ValueListPool list = new ValueListPool(N); + for (int i = 0; i < N; i++) { - _valueListPool.Add(i); + list.Add(i); } + + return list.Count; } } } diff --git a/perf/ListPool.Benchmarks/Program.cs b/perf/ListPool.Benchmarks/Program.cs index 13503d0..98b5970 100644 --- a/perf/ListPool.Benchmarks/Program.cs +++ b/perf/ListPool.Benchmarks/Program.cs @@ -1,5 +1,4 @@ -using System; -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Running; namespace ListPool.Benchmarks { @@ -7,8 +6,10 @@ internal class Program { private static void Main(string[] args) { - BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); - Console.ReadLine(); + while (true) + { + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + } } } } diff --git a/perf/ListPool.Benchmarks/Utf8JsonDeserializeListOfIntBenchmarks.cs b/perf/ListPool.Benchmarks/Utf8JsonDeserializeListOfIntBenchmarks.cs index e73b1e2..860d513 100644 --- a/perf/ListPool.Benchmarks/Utf8JsonDeserializeListOfIntBenchmarks.cs +++ b/perf/ListPool.Benchmarks/Utf8JsonDeserializeListOfIntBenchmarks.cs @@ -14,7 +14,7 @@ public class Utf8JsonDeserializeListOfIntBenchmarks { private byte[] _serializedList; - [Params(100, 1000, 1000)] + [Params(100, 1_000, 10_000)] public int N { get; set; } [GlobalSetup] @@ -36,5 +36,18 @@ public int ListPool() using ListPool list = Utf8Json.JsonSerializer.Deserialize>(_serializedList); return list.Count; } + [Benchmark] + public int ListPool_Spreads() + { + using ListPool list = Spreads.Serialization.Utf8Json.JsonSerializer.Deserialize>(_serializedList); + return list.Count; + } + + [Benchmark] + public int List_Spreads() + { + List list = Spreads.Serialization.Utf8Json.JsonSerializer.Deserialize>(_serializedList); + return list.Count; + } } } diff --git a/perf/ListPool.Benchmarks/Utf8JsonSerializeListOfIntBenchmarks.cs b/perf/ListPool.Benchmarks/Utf8JsonSerializeListOfIntBenchmarks.cs new file mode 100644 index 0000000..0ec302e --- /dev/null +++ b/perf/ListPool.Benchmarks/Utf8JsonSerializeListOfIntBenchmarks.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using Utf8Json; + +namespace ListPool.Benchmarks +{ + [RPlotExporter] + [RankColumn] + [Orderer(SummaryOrderPolicy.FastestToSlowest)] + [MemoryDiagnoser] + [GcServer(true)] + [GcConcurrent] + public class Utf8JsonSerializeListOfIntBenchmarks + { + private List _list; + private ListPool _listPool; + + [Params(100, 1_000, 10_000)] + public int N { get; set; } + + [GlobalSetup] + public void GlobalSetup() + { + var items = Enumerable.Range(0, N).ToArray(); + _listPool = items.ToListPool(); + _list = items.ToList(); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + _listPool.Dispose(); + } + + [Benchmark(Baseline = true)] + public int List() + { + byte[] serializedItems = JsonSerializer.Serialize(_list); + return serializedItems.Length; + } + + [Benchmark] + public int ListPool() + { + byte[] serializedItems = JsonSerializer.Serialize(_listPool); + return serializedItems.Length; + } + + + [Benchmark] + public int ListPool_Spreads() + { + byte[] serializedItems = Spreads.Serialization.Utf8Json.JsonSerializer.Serialize(_listPool); + return serializedItems.Length; + } + } +} diff --git a/src/ListPool/IValueEnumerable.cs b/src/ListPool/IValueEnumerable.cs deleted file mode 100644 index 898e628..0000000 --- a/src/ListPool/IValueEnumerable.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace ListPool -{ - internal interface IValueEnumerable : IEnumerable - { - new ValueEnumerator GetEnumerator(); - } -} diff --git a/src/ListPool/ListPool.cs b/src/ListPool/ListPool.cs index 4a2b748..9e1d41f 100644 --- a/src/ListPool/ListPool.cs +++ b/src/ListPool/ListPool.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; using System.Threading; @@ -14,11 +13,10 @@ namespace ListPool /// With overhead being the class itself regardless the size of the underlying array. /// /// - public sealed class ListPool : IList, IList, IReadOnlyList, IDisposable, - IValueEnumerable - + [Serializable] + public sealed class ListPool : IList, IList, IReadOnlyList, IDisposable { - private const int MinimumCapacity = 128; + private const int MinimumCapacity = 64; private T[] _buffer; [NonSerialized] @@ -53,20 +51,37 @@ public ListPool(IEnumerable source) { if (source is ICollection collection) { - _buffer = ArrayPool.Shared.Rent(collection.Count); + T[] buffer = ArrayPool.Shared.Rent(collection.Count > MinimumCapacity ? collection.Count : MinimumCapacity); + + collection.CopyTo(buffer, 0); - collection.CopyTo(_buffer, 0); - _count = collection.Count; + _buffer = buffer; + Count = collection.Count; } else { _buffer = ArrayPool.Shared.Rent(MinimumCapacity); - + T[] buffer = _buffer; + Count = 0; + int count = 0; using IEnumerator enumerator = source.GetEnumerator(); while (enumerator.MoveNext()) { - Add(enumerator.Current); + if (count < buffer.Length) + { + buffer[count] = enumerator.Current; + count++; + } + else + { + Count = count; + count++; + AddWithResize(enumerator.Current); + buffer = _buffer; + } } + + Count = count; } } @@ -80,9 +95,10 @@ public ListPool(IEnumerable source) /// public void Dispose() { - _count = 0; - if (_buffer != null) - ArrayPool.Shared.Return(_buffer); + Count = 0; + T[] buffer = _buffer; + if (buffer != null) + ArrayPool.Shared.Return(buffer); } int ICollection.Count => Count; @@ -103,6 +119,7 @@ object ICollection.SyncRoot } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] int IList.Add(object item) { if (item is T itemAsTSource) @@ -140,29 +157,28 @@ int IList.IndexOf(object item) void IList.Remove(object item) { - if (item is null) + if (item is T itemAsTSource) { - return; + Remove(itemAsTSource); } - - if (!(item is T itemAsTSource)) + else if (item != null) { throw new ArgumentException($"Wrong value type. Expected {typeof(T)}, got: '{item}'.", nameof(item)); } - - Remove(itemAsTSource); } void IList.Insert(int index, object item) { - if (!(item is T itemAsTSource)) + if (item is T itemAsTSource) + { + Insert(index, itemAsTSource); + } + else { throw new ArgumentException($"Wrong value type. Expected {typeof(T)}, got: '{item}'.", nameof(item)); } - - Insert(index, itemAsTSource); } void ICollection.CopyTo(Array array, int arrayIndex) @@ -173,11 +189,10 @@ void ICollection.CopyTo(Array array, int arrayIndex) [MaybeNull] object IList.this[int index] { - [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - if (index >= _count) + if (index >= Count) throw new IndexOutOfRangeException(nameof(index)); return _buffer[index]; @@ -185,7 +200,7 @@ object IList.this[int index] set { - if (index >= _count) + if (index >= Count) throw new IndexOutOfRangeException(nameof(index)); if (value is T valueAsTSource) @@ -203,27 +218,34 @@ object IList.this[int index] /// /// Count of items added. /// - public int Count => _count; - - private int _count; + public int Count { get; private set; } public bool IsReadOnly => false; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(T item) { - if (_count >= _buffer.Length) GrowBufferDoubleSize(); + T[] buffer = _buffer; + int count = Count; - _buffer[_count++] = item; + if (count < buffer.Length) + { + buffer[count] = item; + Count = count + 1; + } + else + { + AddWithResize(item); + } } - public void Clear() => _count = 0; + public void Clear() => Count = 0; public bool Contains(T item) => IndexOf(item) > -1; - public int IndexOf(T item) => Array.IndexOf(_buffer, item, 0, _count); + public int IndexOf(T item) => Array.IndexOf(_buffer, item, 0, Count); public void CopyTo(T[] array, int arrayIndex) => - Array.Copy(_buffer, 0, array, arrayIndex, _count); + Array.Copy(_buffer, 0, array, arrayIndex, Count); public bool Remove(T item) { @@ -241,21 +263,42 @@ public bool Remove(T item) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Insert(int index, T item) { - if (index > _count) throw new IndexOutOfRangeException(nameof(index)); - if (index >= _buffer.Length) GrowBufferDoubleSize(); - if (index < _count) - Array.Copy(_buffer, index, _buffer, index + 1, _count - index); + int count = Count; + T[] buffer = _buffer; - _buffer[index] = item; - _count++; + if (buffer.Length == count) + { + count *= 2; + GrowBuffer(count); + buffer = _buffer; + } + + if (index < Count) + { + if (index < count) + Array.Copy(buffer, index, buffer, index + 1, count - index); + + buffer[index] = item; + Count++; + } + else if (index == Count) + { + buffer[index] = item; + Count++; + } + else if (index > count) throw new IndexOutOfRangeException(nameof(index)); } public void RemoveAt(int index) { - if (index >= _count) throw new IndexOutOfRangeException(nameof(index)); + int count = Count; + T[] buffer = _buffer; - _count--; - Array.Copy(_buffer, index + 1, _buffer, index, _count - index); + if (index >= count) throw new IndexOutOfRangeException(nameof(index)); + + count--; + Array.Copy(buffer, index + 1, buffer, index, count - index); + Count = count; } [MaybeNull] @@ -264,7 +307,7 @@ public T this[int index] [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - if (index >= _count) + if (index >= Count) throw new IndexOutOfRangeException(nameof(index)); return _buffer[index]; @@ -272,93 +315,177 @@ public T this[int index] set { - if (index >= _count) + if (index >= Count) throw new IndexOutOfRangeException(nameof(index)); _buffer[index] = value; } } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ValueEnumerator GetEnumerator() => - new ValueEnumerator(_buffer, _count); - public void AddRange(Span items) { - bool isCapacityEnough = _buffer.Length - items.Length - _count > 0; + int count = Count; + T[] buffer = _buffer; + + bool isCapacityEnough = buffer.Length - items.Length - count > 0; if (!isCapacityEnough) - GrowBuffer(_buffer.Length + items.Length); + { + GrowBuffer(buffer.Length + items.Length); + buffer = _buffer; + } - items.CopyTo(_buffer.AsSpan().Slice(Count)); - _count += items.Length; + items.CopyTo(buffer.AsSpan().Slice(count)); + Count += items.Length; } public void AddRange(ReadOnlySpan items) { - bool isCapacityEnough = _buffer.Length - items.Length - _count > 0; + int count = Count; + T[] buffer = _buffer; + + bool isCapacityEnough = buffer.Length - items.Length - count > 0; if (!isCapacityEnough) - GrowBuffer(_buffer.Length + items.Length); + { + GrowBuffer(buffer.Length + items.Length); + buffer = _buffer; + } - items.CopyTo(_buffer.AsSpan().Slice(Count)); - _count += items.Length; + items.CopyTo(buffer.AsSpan().Slice(count)); + Count += items.Length; } public void AddRange(T[] array) => AddRange(array.AsSpan()); public void AddRange(IEnumerable items) { + int count = Count; + T[] buffer = _buffer; + if (items is ICollection collection) { - bool isCapacityEnough = _buffer.Length - collection.Count - _count > 0; + bool isCapacityEnough = buffer.Length - collection.Count - count > 0; if (!isCapacityEnough) - GrowBuffer(_buffer.Length + collection.Count); + { + GrowBuffer(buffer.Length + collection.Count); + buffer = _buffer; + } - collection.CopyTo(_buffer, _count); - _count += collection.Count; + collection.CopyTo(buffer, count); + Count += collection.Count; } else { foreach (T item in items) { - if (Count >= _buffer.Length) GrowBufferDoubleSize(); - _buffer[_count++] = item; + if (count < buffer.Length) + { + buffer[count] = item; + count++; + } + else + { + Count = count; + AddWithResize(item); + count++; + buffer = _buffer; + } } + + Count = count; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span AsSpan() => _buffer.AsSpan(0, _count); + public Span AsSpan() => _buffer.AsSpan(0, Count); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Memory AsMemory() => _buffer.AsMemory(0, _count); + public Memory AsMemory() => _buffer.AsMemory(0, Count); [MethodImpl(MethodImplOptions.NoInlining)] - public void GrowBufferDoubleSize() + private void AddWithResize(T item) { - int newLength = _buffer.Length * 2; - var newBuffer = ArrayPool.Shared.Rent(newLength); - var oldBuffer = _buffer; + ArrayPool arrayPool = ArrayPool.Shared; + T[] oldBuffer = _buffer; + T[] newBuffer = arrayPool.Rent(oldBuffer.Length * 2); + int count = oldBuffer.Length; - Array.Copy(oldBuffer, 0, newBuffer, 0, _buffer.Length); + Array.Copy(oldBuffer, 0, newBuffer, 0, count); + newBuffer[count] = item; _buffer = newBuffer; - ArrayPool.Shared.Return(oldBuffer); + Count = count + 1; + arrayPool.Return(oldBuffer); } [MethodImpl(MethodImplOptions.NoInlining)] - public void GrowBuffer(int capacity) + private void GrowBuffer(int capacity) { - var newBuffer = ArrayPool.Shared.Rent(capacity); - var oldBuffer = _buffer; + ArrayPool arrayPool = ArrayPool.Shared; + T[] newBuffer = arrayPool.Rent(capacity); + T[] oldBuffer = _buffer; - Array.Copy(oldBuffer, 0, newBuffer, 0, _buffer.Length); + Array.Copy(oldBuffer, 0, newBuffer, 0, oldBuffer.Length); _buffer = newBuffer; - ArrayPool.Shared.Return(oldBuffer); + arrayPool.Return(oldBuffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_buffer, Count); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_buffer, Count); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => new Enumerator(_buffer, Count); + + public struct Enumerator : IEnumerator + { + private readonly T[] _source; + private readonly int _itemsCount; + private int _index; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator(T[] source, int itemsCount) + { + _source = source; + _itemsCount = itemsCount; + _index = -1; + } + + [MaybeNull] + public readonly ref T Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref _source[_index]; + } + + [MaybeNull] + readonly T IEnumerator.Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _source[_index]; + } + + [MaybeNull] + readonly object? IEnumerator.Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _source[_index]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() => unchecked(++_index < _itemsCount); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _index = -1; + } + + public readonly void Dispose() + { + } } } } diff --git a/src/ListPool/ListPool.csproj b/src/ListPool/ListPool.csproj index f43aa26..5fb9337 100644 --- a/src/ListPool/ListPool.csproj +++ b/src/ListPool/ListPool.csproj @@ -9,16 +9,14 @@ ListPool ListPool ListPool and ValueListPool are an optimized allocation free implementations of IList using ArrayPool. - ASP.NET;List;System.Buffers;ArrayPool;ListPool;Performance + ASP.NET;List;System.Buffers;ArrayPool;ListPool;Performance;Span https://github.com/faustodavid/ListPool true LICENSE - 2.2.0 + 2.2.1 Changelog: - * Improve performance of ValueListPool by removing buffer checks.Now is required to avoid parametless constructor for ValueListPool. - * Add support for span and memory - * Add method AddRange + * Improve overall performance of ListPool and ValueListPool ListPool is the general use of the implementation. ValueListPool is the zero heap allocations implementation. Note, because it is a struct it is passed by value, not by reference. diff --git a/src/ListPool/ValueEnumerator.cs b/src/ListPool/ValueEnumerator.cs deleted file mode 100644 index 2650b6b..0000000 --- a/src/ListPool/ValueEnumerator.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace ListPool -{ - public struct ValueEnumerator : IEnumerator - { - private readonly T[] _source; - private readonly int _itemsCount; - private int _index; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ValueEnumerator(T[] source, int itemsCount) - { - _source = source; - _itemsCount = itemsCount; - _index = -1; - } - - [MaybeNull] - public readonly ref T Current - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => ref _source[_index]; - } - - [MaybeNull] - readonly T IEnumerator.Current - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get { return _source[_index]; } - } - - [MaybeNull] - readonly object? IEnumerator.Current - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get { return _source[_index]; } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() => ++_index < _itemsCount; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Reset() - { - _index = -1; - } - - public readonly void Dispose() - { - } - } -} diff --git a/src/ListPool/ValueListPool.cs b/src/ListPool/ValueListPool.cs index 76ce3e0..85c06f5 100644 --- a/src/ListPool/ValueListPool.cs +++ b/src/ListPool/ValueListPool.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; using System.Threading; @@ -15,215 +14,244 @@ namespace ListPool /// Otherwise it wont work. /// /// - public struct ValueListPool : IList, IList, IReadOnlyList, IDisposable, - IValueEnumerable + public struct ValueListPool : IList, IList, IReadOnlyList, IDisposable - { - /// - /// Capacity of the underlying array. - /// - public readonly int Capacity => _buffer.Length; - - /// - /// Count of items added. - /// - public int Count { get; private set; } - - public readonly bool IsReadOnly => false; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Span AsSpan() => _buffer.AsSpan(0, Count); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Memory AsMemory() => _buffer.AsMemory(0, Count); - - int ICollection.Count => Count; - readonly bool IList.IsFixedSize => false; - bool ICollection.IsSynchronized => false; - readonly bool IList.IsReadOnly => false; + { + private const int MinimumCapacity = 64; private T[] _buffer; - private const int MinimumCapacity = 128; [NonSerialized] private object? _syncRoot; - object ICollection.SyncRoot - { - get - { - if (_syncRoot is null) - { - _ = Interlocked.CompareExchange(ref _syncRoot, new object(), null); - } - - return _syncRoot; - } - } - /// - /// Construct ValueListPool with the indicated capacity. + /// Construct ListPool with the indicated capacity. /// /// Required initial capacity + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ValueListPool(int capacity) { - _syncRoot = null; _buffer = ArrayPool.Shared.Rent(capacity < MinimumCapacity ? MinimumCapacity : capacity); Count = 0; + _syncRoot = null; } /// - /// Construct ValueListPool from the given source. + /// Construct ListPool from the given source. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ValueListPool(IEnumerable source) { _syncRoot = null; if (source is ICollection collection) { - _buffer = ArrayPool.Shared.Rent(collection.Count); + T[] buffer = ArrayPool.Shared.Rent(collection.Count > MinimumCapacity ? collection.Count : MinimumCapacity); - collection.CopyTo(_buffer, 0); + collection.CopyTo(buffer, 0); + + _buffer = buffer; Count = collection.Count; } else { _buffer = ArrayPool.Shared.Rent(MinimumCapacity); + T[] buffer = _buffer; Count = 0; - + int count = 0; using IEnumerator enumerator = source.GetEnumerator(); while (enumerator.MoveNext()) { - Add(enumerator.Current); + if (count < buffer.Length) + { + buffer[count] = enumerator.Current; + count++; + } + else + { + Count = count; + count++; + AddWithResize(enumerator.Current); + buffer = _buffer; + } } + + Count = count; } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(T item) - { - if (Count >= _buffer.Length) GrowBufferDoubleSize(); - - _buffer[Count++] = item; - } + /// + /// Capacity of the underlying array. + /// + public int Capacity => _buffer.Length; - int IList.Add(object item) + /// + /// Returns underlying array to the pool + /// + public void Dispose() { - if (item is T itemAsTSource) - { - Add(itemAsTSource); - } - else - { - throw new ArgumentException($"Wrong type. Expected {typeof(T)}, actual: '{item}'.", nameof(item)); - } - - return Count - 1; + Count = 0; + T[] buffer = _buffer; + if (buffer != null) + ArrayPool.Shared.Return(buffer); } - public void AddRange(Span items) - { - bool isCapacityEnough = _buffer.Length - items.Length - Count > 0; - if (!isCapacityEnough) - GrowBuffer(_buffer.Length + items.Length); - - items.CopyTo(_buffer.AsSpan().Slice(Count)); - Count += items.Length; - } + readonly int ICollection.Count => Count; + readonly bool IList.IsFixedSize => false; + readonly bool ICollection.IsSynchronized => false; + readonly bool IList.IsReadOnly => false; - public void AddRange(ReadOnlySpan items) + object ICollection.SyncRoot { - bool isCapacityEnough = _buffer.Length - items.Length - Count > 0; - if (!isCapacityEnough) - GrowBuffer(_buffer.Length + items.Length); + get + { + if (_syncRoot is null) + { + _ = Interlocked.CompareExchange(ref _syncRoot, new object(), null); + } - items.CopyTo(_buffer.AsSpan().Slice(Count)); - Count += items.Length; + return _syncRoot; + } } - public void AddRange(T[] array) => AddRange(array.AsSpan()); - - public void AddRange(IEnumerable items) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + int IList.Add(object item) { - if (items is ICollection collection) + if (item is T itemAsTSource) { - bool isCapacityEnough = _buffer.Length - collection.Count - Count > 0; - if (!isCapacityEnough) - GrowBuffer(_buffer.Length + collection.Count); + T[] buffer = _buffer; + int count = Count; - collection.CopyTo(_buffer, Count); - Count += collection.Count; + if (count < buffer.Length) + { + buffer[count] = itemAsTSource; + Count = count + 1; + } + else + { + AddWithResize(itemAsTSource); + } } else { - foreach (T item in items) - { - if (Count >= _buffer.Length) GrowBufferDoubleSize(); - _buffer[Count++] = item; - } + throw new ArgumentException($"Wrong value type. Expected {typeof(T)}, got: '{item}'.", + nameof(item)); } - } - public void Clear() => Count = 0; - public readonly bool Contains(T item) => IndexOf(item) > -1; + return Count - 1; + } - bool IList.Contains(object item) + readonly bool IList.Contains(object item) { if (item is T itemAsTSource) { return Contains(itemAsTSource); } - throw new ArgumentException($"Wrong type. Expected {typeof(T)}, actual: '{item}'.", nameof(item)); + throw new ArgumentException($"Wrong value type. Expected {typeof(T)}, got: '{item}'.", nameof(item)); } - int IList.IndexOf(object item) + readonly int IList.IndexOf(object item) { if (item is T itemAsTSource) { return IndexOf(itemAsTSource); } - throw new ArgumentException($"Wrong type. Expected {typeof(T)}, actual: '{item}'.", nameof(item)); + throw new ArgumentException($"Wrong value type. Expected {typeof(T)}, got: '{item}'.", nameof(item)); } void IList.Remove(object item) { - if (item is null) + if (item is T itemAsTSource) { - return; + Remove(itemAsTSource); } - - if (!(item is T itemAsTSource)) + else if (item != null) { throw new ArgumentException($"Wrong value type. Expected {typeof(T)}, got: '{item}'.", nameof(item)); } - - Remove(itemAsTSource); } void IList.Insert(int index, object item) { - if (!(item is T itemAsTSource)) + if (item is T itemAsTSource) + { + Insert(index, itemAsTSource); + } + else { throw new ArgumentException($"Wrong value type. Expected {typeof(T)}, got: '{item}'.", nameof(item)); } + } - Insert(index, itemAsTSource); + readonly void ICollection.CopyTo(Array array, int arrayIndex) + { + Array.Copy(_buffer, 0, array, arrayIndex, Count); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly int IndexOf(T item) => Array.IndexOf(_buffer, item, 0, Count); + [MaybeNull] + readonly object IList.this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (index >= Count) + throw new IndexOutOfRangeException(nameof(index)); - public readonly void CopyTo(T[] array, int arrayIndex) => - Array.Copy(_buffer, 0, array, arrayIndex, Count); + return _buffer[index]; + } + + set + { + if (index >= Count) + throw new IndexOutOfRangeException(nameof(index)); + + if (value is T valueAsTSource) + { + _buffer[index] = valueAsTSource; + } + else + { + throw new ArgumentException($"Wrong value type. Expected {typeof(T)}, got: '{value}'.", + nameof(value)); + } + } + } - void ICollection.CopyTo(Array array, int arrayIndex) + /// + /// Count of items added. + /// + public int Count { get; private set; } + + public bool IsReadOnly => false; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(T item) { - Array.Copy(_buffer, 0, array, arrayIndex, Count); + T[] buffer = _buffer; + int count = Count; + + if (count < buffer.Length) + { + buffer[count] = item; + Count = count + 1; + } + else + { + AddWithResize(item); + } } + public void Clear() => Count = 0; + public readonly bool Contains(T item) => IndexOf(item) > -1; + + public readonly int IndexOf(T item) => Array.IndexOf(_buffer, item, 0, Count); + + public readonly void CopyTo(T[] array, int arrayIndex) => + Array.Copy(_buffer, 0, array, arrayIndex, Count); + public bool Remove(T item) { if (item is null) return false; @@ -240,25 +268,46 @@ public bool Remove(T item) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Insert(int index, T item) { - if (index > Count) throw new IndexOutOfRangeException(nameof(index)); - if (index >= _buffer.Length) GrowBufferDoubleSize(); + int count = Count; + T[] buffer = _buffer; + + if (buffer.Length == count) + { + count *= 2; + GrowBuffer(count); + buffer = _buffer; + } + if (index < Count) - Array.Copy(_buffer, index, _buffer, index + 1, Count - index); + { + if (index < count) + Array.Copy(buffer, index, buffer, index + 1, count - index); - _buffer[index] = item; - Count++; + buffer[index] = item; + Count++; + } + else if (index == Count) + { + buffer[index] = item; + Count++; + } + else if (index > count) throw new IndexOutOfRangeException(nameof(index)); } public void RemoveAt(int index) { - if (index >= Count) throw new IndexOutOfRangeException(nameof(index)); + int count = Count; + T[] buffer = _buffer; + + if (index >= count) throw new IndexOutOfRangeException(nameof(index)); - Count--; - Array.Copy(_buffer, index + 1, _buffer, index, Count - index); + count--; + Array.Copy(buffer, index + 1, buffer, index, count - index); + Count = count; } [MaybeNull] - readonly object IList.this[int index] + public readonly T this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get @@ -274,104 +323,174 @@ readonly object IList.this[int index] if (index >= Count) throw new IndexOutOfRangeException(nameof(index)); - if (value is T valueAsTSource) - { - _buffer[index] = valueAsTSource; - } - else - { - throw new ArgumentException($"Wrong value type. Expected {typeof(T)}, got: '{value}'.", - nameof(value)); - } + _buffer[index] = value; } } - [MaybeNull] - readonly T IList.this[int index] + public void AddRange(Span items) { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - if (index >= Count) - throw new IndexOutOfRangeException(nameof(index)); - - return _buffer[index]; - } + int count = Count; + T[] buffer = _buffer; - set + bool isCapacityEnough = buffer.Length - items.Length - count > 0; + if (!isCapacityEnough) { - if (index >= Count) - throw new IndexOutOfRangeException(nameof(index)); - - _buffer[index] = value; + GrowBuffer(buffer.Length + items.Length); + buffer = _buffer; } + + items.CopyTo(buffer.AsSpan().Slice(count)); + Count += items.Length; } - [MaybeNull] - readonly T IReadOnlyList.this[int index] + public void AddRange(ReadOnlySpan items) { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - if (index >= Count) - throw new IndexOutOfRangeException(nameof(index)); + int count = Count; + T[] buffer = _buffer; - return _buffer[index]; + bool isCapacityEnough = buffer.Length - items.Length - count > 0; + if (!isCapacityEnough) + { + GrowBuffer(buffer.Length + items.Length); + buffer = _buffer; } + + items.CopyTo(buffer.AsSpan().Slice(count)); + Count += items.Length; } - [MaybeNull] - public readonly ref T this[int index] + public void AddRange(T[] array) => AddRange(array.AsSpan()); + + public void AddRange(IEnumerable items) { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get + int count = Count; + T[] buffer = _buffer; + + if (items is ICollection collection) { - if (index >= Count) - throw new IndexOutOfRangeException(nameof(index)); + bool isCapacityEnough = buffer.Length - collection.Count - count > 0; + if (!isCapacityEnough) + { + GrowBuffer(buffer.Length + collection.Count); + buffer = _buffer; + } + + collection.CopyTo(buffer, count); + Count += collection.Count; + } + else + { + foreach (T item in items) + { + if (count < buffer.Length) + { + buffer[count] = item; + count++; + } + else + { + Count = count; + AddWithResize(item); + count++; + buffer = _buffer; + } + } - return ref _buffer[index]; + Count = count; } } - [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly ValueEnumerator GetEnumerator() => - new ValueEnumerator(_buffer, Count); - - readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public readonly Span AsSpan() => _buffer.AsSpan(0, Count); - readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Memory AsMemory() => _buffer.AsMemory(0, Count); [MethodImpl(MethodImplOptions.NoInlining)] - public void GrowBufferDoubleSize() + private void AddWithResize(T item) { - int newLength = _buffer.Length * 2; - var newBuffer = ArrayPool.Shared.Rent(newLength); - var oldBuffer = _buffer; + ArrayPool arrayPool = ArrayPool.Shared; + T[] oldBuffer = _buffer; + T[] newBuffer = arrayPool.Rent(oldBuffer.Length * 2); + int count = oldBuffer.Length; - Array.Copy(oldBuffer, 0, newBuffer, 0, _buffer.Length); + Array.Copy(oldBuffer, 0, newBuffer, 0, count); + newBuffer[count] = item; _buffer = newBuffer; - ArrayPool.Shared.Return(oldBuffer); + Count = count + 1; + arrayPool.Return(oldBuffer); } [MethodImpl(MethodImplOptions.NoInlining)] - public void GrowBuffer(int capacity) + private void GrowBuffer(int capacity) { - var newBuffer = ArrayPool.Shared.Rent(capacity); - var oldBuffer = _buffer; + ArrayPool arrayPool = ArrayPool.Shared; + T[] newBuffer = arrayPool.Rent(capacity); + T[] oldBuffer = _buffer; - Array.Copy(oldBuffer, 0, newBuffer, 0, _buffer.Length); + Array.Copy(oldBuffer, 0, newBuffer, 0, oldBuffer.Length); _buffer = newBuffer; - ArrayPool.Shared.Return(oldBuffer); + arrayPool.Return(oldBuffer); } - public void Dispose() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_buffer, Count); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_buffer, Count); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Enumerator GetEnumerator() => new Enumerator(_buffer, Count); + + public struct Enumerator : IEnumerator { - Count = 0; - if (_buffer != null) - ArrayPool.Shared.Return(_buffer); + private readonly T[] _source; + private readonly int _itemsCount; + private int _index; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator(T[] source, int itemsCount) + { + _source = source; + _itemsCount = itemsCount; + _index = -1; + } + + [MaybeNull] + public readonly ref T Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref _source[_index]; + } + + [MaybeNull] + readonly T IEnumerator.Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _source[_index]; + } + + [MaybeNull] + readonly object? IEnumerator.Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _source[_index]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() => ++_index < _itemsCount; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _index = -1; + } + + public readonly void Dispose() + { + } } } } diff --git a/tests/ListPool.UnitTests/ListPool.UnitTests.csproj b/tests/ListPool.UnitTests/ListPool.UnitTests.csproj index 779f693..a3131b2 100644 --- a/tests/ListPool.UnitTests/ListPool.UnitTests.csproj +++ b/tests/ListPool.UnitTests/ListPool.UnitTests.csproj @@ -13,6 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/ListPool.UnitTests/ListPool/ListPoolTests.cs b/tests/ListPool.UnitTests/ListPool/ListPoolTests.cs index a7b4b95..54ca3aa 100644 --- a/tests/ListPool.UnitTests/ListPool/ListPoolTests.cs +++ b/tests/ListPool.UnitTests/ListPool/ListPoolTests.cs @@ -8,6 +8,7 @@ namespace ListPool.UnitTests.ListPool { public class ListPoolTests : ListPoolTestsBase { + [Fact] public void Add_item_without_indicate_capacity_of_list() { int expectedItem = s_fixture.Create(); @@ -16,6 +17,15 @@ public void Add_item_without_indicate_capacity_of_list() Assert.Equal(expectedItem, sut[0]); } + [Fact] + public void Create_list_by_passing_another_without_items_set_minimum_capacity() + { + List emptyList = new List(); + + using ListPool sut = new ListPool(emptyList); + + Assert.Equal(64, sut.Capacity); + } public override void Add_items_when_capacity_is_full_then_buffer_autogrow() { @@ -31,7 +41,7 @@ public override void Add_items_when_capacity_is_full_then_buffer_autogrow() Assert.True(expectedItems.All(expectedItem => sut.Contains(expectedItem))); } - + [Fact] public void Contains_empty_ListPool_without_indicating_capacity_returns_false() { int randomItem = s_fixture.Create(); @@ -111,7 +121,7 @@ public override void Create_list_and_add_values_after_clear() Assert.Empty(sut); } - + [Fact] public void Create_without_parameters_should_add_and_get_items() { const int expectedItemsCount = 3; @@ -127,7 +137,7 @@ public void Create_without_parameters_should_add_and_get_items() Assert.Equal(expectedItemsCount, sut.Count); } - + [Fact] public void Enumerate_when_capacity_is_not_set_dont_throw_exception() { using var sut = new ListPool(); @@ -155,7 +165,7 @@ public override void Get_item_with_index_bellow_zero_throws_IndexOutOfRangeExcep Assert.Throws(() => sut[index]); } - + [Fact] public void IndexOf_empty_ListPool_without_indicating_capacity_returns_negative_one() { int randomItem = s_fixture.Create(); @@ -246,7 +256,7 @@ public override void Insert_items_when_capacity_is_full_then_buffer_autogrow() Assert.True(expectedItems.All(expectedItem => sut.Contains(expectedItem))); } - + [Fact] public void Insert_without_indicating_capacity_of_list() { const int index = 0; diff --git a/tests/ListPool.UnitTests/ListPool/Serializer/ListPoolSpreadsUtf8JsonTests.cs b/tests/ListPool.UnitTests/ListPool/Serializer/ListPoolSpreadsUtf8JsonTests.cs new file mode 100644 index 0000000..33dc21c --- /dev/null +++ b/tests/ListPool.UnitTests/ListPool/Serializer/ListPoolSpreadsUtf8JsonTests.cs @@ -0,0 +1,61 @@ +using System.Linq; +using AutoFixture; +using Spreads.Serialization.Utf8Json; +using Xunit; + +namespace ListPool.UnitTests.ListPool.Serializer +{ + public class ListPoolSpreadsUtf8JsonTests : ListPoolSerializerTestsBase + { + public override void Serialize_and_deserialize_ListPool_with_value_types() + { + using ListPool expectedItems = new ListPool + { + s_fixture.Create(), s_fixture.Create(), s_fixture.Create() + }; + string serializedItems = JsonSerializer.ToJsonString(expectedItems); + + using ListPool actualItems = JsonSerializer.Deserialize>(serializedItems); + + Assert.Equal(expectedItems.Count, actualItems.Count); + Assert.All(expectedItems, expectedItem => actualItems.Contains(expectedItem)); + } + + public override void Serialize_and_deserialize_ListPool_with_objects() + { + using ListPool expectedItems = new ListPool + { + s_fixture.Create(), s_fixture.Create(), s_fixture.Create() + }; + string serializedItems = JsonSerializer.ToJsonString(expectedItems); + + using ListPool actualItems = + JsonSerializer.Deserialize>(serializedItems); + + Assert.Equal(expectedItems.Count, actualItems.Count); + Assert.All(expectedItems, + expectedItem => actualItems.Any(actualItem => actualItem.Property == expectedItem.Property)); + } + + public override void Serialize_and_deserialize_objects_containing_ListPool() + { + using ListPool expectedItems = new ListPool + { + s_fixture.Create(), s_fixture.Create(), s_fixture.Create() + }; + var expectedObject = new CustomObjectWithListPool + { + Property = s_fixture.Create(), List = expectedItems + }; + string serializedItems = JsonSerializer.ToJsonString(expectedObject); + + using CustomObjectWithListPool actualObject = + JsonSerializer.Deserialize(serializedItems); + + Assert.Equal(expectedObject.Property, actualObject.Property); + Assert.Equal(expectedItems.Count, actualObject.List.Count); + Assert.All(expectedItems, + expectedItem => actualObject.List.Any(actualItem => actualItem == expectedItem)); + } + } +} diff --git a/tests/ListPool.UnitTests/EnumeratorTests.cs b/tests/ListPool.UnitTests/ListPoolEnumeratorTests.cs similarity index 64% rename from tests/ListPool.UnitTests/EnumeratorTests.cs rename to tests/ListPool.UnitTests/ListPoolEnumeratorTests.cs index 2ad38d8..a95f541 100644 --- a/tests/ListPool.UnitTests/EnumeratorTests.cs +++ b/tests/ListPool.UnitTests/ListPoolEnumeratorTests.cs @@ -1,20 +1,38 @@ using System.Collections; +using System.Collections.Generic; using System.Linq; using AutoFixture; using Xunit; namespace ListPool.UnitTests { - public class EnumeratorTests + public class ListPoolEnumeratorTests { private static readonly Fixture s_fixture = new Fixture(); + [Fact] + public void GetEnumerator_Enumerate_All_Items() + { + int[] expectedItems = s_fixture.CreateMany(10).ToArray(); + using ListPool listPool = new ListPool(expectedItems); + using ListPool.Enumerator sut = listPool.GetEnumerator(); + List actualItems = new List(expectedItems.Length); + + while (sut.MoveNext()) + { + actualItems.Add(sut.Current); + } + + Assert.Equal(expectedItems.Length, actualItems.Count); + Assert.Contains(expectedItems, expectedItem => actualItems.Contains(expectedItem)); + } + [Fact] public void Current_is_updated_in_each_iteration() { string[] items = s_fixture.CreateMany(10).ToArray(); IEnumerator expectedEnumerator = items.GetEnumerator(); - var sut = new ValueEnumerator(items, items.Length); + var sut = new ListPool.Enumerator(items, items.Length); while (expectedEnumerator.MoveNext()) { @@ -28,7 +46,7 @@ public void Current_is_updated_in_each_iteration_using_IEnumerator() { string[] items = s_fixture.CreateMany(10).ToArray(); IEnumerator expectedEnumerator = items.GetEnumerator(); - IEnumerator sut = new ValueEnumerator(items, items.Length); + IEnumerator sut = new ListPool.Enumerator(items, items.Length); while (expectedEnumerator.MoveNext()) { @@ -42,7 +60,7 @@ public void Reset_allows_enumerator_to_be_enumerate_again() { string[] items = s_fixture.CreateMany(10).ToArray(); IEnumerator expectedEnumerator = items.GetEnumerator(); - var sut = new ValueEnumerator(items, items.Length); + ListPool.Enumerator sut = new ListPool.Enumerator(items, items.Length); while (expectedEnumerator.MoveNext()) { diff --git a/tests/ListPool.UnitTests/ValueListPool/ValueListPoolTests.cs b/tests/ListPool.UnitTests/ValueListPool/ValueListPoolTests.cs index 61c9645..e3adfb7 100644 --- a/tests/ListPool.UnitTests/ValueListPool/ValueListPoolTests.cs +++ b/tests/ListPool.UnitTests/ValueListPool/ValueListPoolTests.cs @@ -22,6 +22,15 @@ public override void Add_items_when_capacity_is_full_then_buffer_autogrow() Assert.True(expectedItems.All(expectedItem => sut.Contains(expectedItem))); } + [Fact] + public void Create_list_by_passing_another_without_items_set_minimum_capacity() + { + List emptyList = new List(); + + using ValueListPool sut = new ValueListPool(emptyList); + + Assert.Equal(64, sut.Capacity); + } public override void Contains_return_true_when_item_exists() { diff --git a/tests/ListPool.UnitTests/ValueListPoolEnumeratorTests.cs b/tests/ListPool.UnitTests/ValueListPoolEnumeratorTests.cs new file mode 100644 index 0000000..b67ebd0 --- /dev/null +++ b/tests/ListPool.UnitTests/ValueListPoolEnumeratorTests.cs @@ -0,0 +1,81 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using AutoFixture; +using Xunit; + +namespace ListPool.UnitTests +{ + public class ValueListPoolEnumeratorTests + { + private static readonly Fixture s_fixture = new Fixture(); + + [Fact] + public void GetEnumerator_Enumerate_All_Items() + { + int[] expectedItems = s_fixture.CreateMany(10).ToArray(); + using ValueListPool listPool = new ValueListPool(expectedItems); + using ValueListPool.Enumerator sut = listPool.GetEnumerator(); + List actualItems = new List(expectedItems.Length); + + while (sut.MoveNext()) + { + actualItems.Add(sut.Current); + } + + Assert.Equal(expectedItems.Length, actualItems.Count); + Assert.Contains(expectedItems, expectedItem => actualItems.Contains(expectedItem)); + } + + [Fact] + public void Current_is_updated_in_each_iteration() + { + string[] items = s_fixture.CreateMany(10).ToArray(); + IEnumerator expectedEnumerator = items.GetEnumerator(); + var sut = new ValueListPool.Enumerator(items, items.Length); + + while (expectedEnumerator.MoveNext()) + { + Assert.True(sut.MoveNext()); + Assert.Equal(expectedEnumerator.Current, sut.Current); + } + } + + [Fact] + public void Current_is_updated_in_each_iteration_using_IEnumerator() + { + string[] items = s_fixture.CreateMany(10).ToArray(); + IEnumerator expectedEnumerator = items.GetEnumerator(); + IEnumerator sut = new ValueListPool.Enumerator(items, items.Length); + + while (expectedEnumerator.MoveNext()) + { + Assert.True(sut.MoveNext()); + Assert.Equal(expectedEnumerator.Current, sut.Current); + } + } + + [Fact] + public void Reset_allows_enumerator_to_be_enumerate_again() + { + string[] items = s_fixture.CreateMany(10).ToArray(); + IEnumerator expectedEnumerator = items.GetEnumerator(); + var sut = new ValueListPool.Enumerator(items, items.Length); + + while (expectedEnumerator.MoveNext()) + { + Assert.True(sut.MoveNext()); + Assert.Equal(expectedEnumerator.Current, sut.Current); + } + + Assert.False(sut.MoveNext()); + sut.Reset(); + expectedEnumerator.Reset(); + while (expectedEnumerator.MoveNext()) + { + Assert.True(sut.MoveNext()); + Assert.Equal(expectedEnumerator.Current, sut.Current); + } + } + } +}