From a534cb985016635d180dbae473374a80806601a2 Mon Sep 17 00:00:00 2001 From: Rima Falko Date: Tue, 3 Jun 2025 05:02:33 +0200 Subject: [PATCH 01/10] optimize first operators for frozen-sequence --- .../Sequences/FrozenSequence.Operator.First.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs index 74cb6a5..267c3a2 100644 --- a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs +++ b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs @@ -20,8 +20,14 @@ public T First(Func predicate) { ArgumentNullException.ThrowIfNull(predicate); - foreach (ref readonly var item in this) + scoped ref var itemsReference = ref MemoryMarshal.GetArrayDataReference(_items); + + var itemsCount = _itemsCount; + + for (var itemIndex = 0; itemIndex < itemsCount; itemIndex++) { + var item = Unsafe.Add(ref itemsReference, itemIndex); + if (predicate(item)) return item; } From 85803d3d44b1d19869530cecebe0321e6f559f79 Mon Sep 17 00:00:00 2001 From: Rima Falko Date: Tue, 3 Jun 2025 05:06:53 +0200 Subject: [PATCH 02/10] opt --- .../Falko.Common.Sequences/Asserts/SequenceExceptions.cs | 1 - .../Sequences/FrozenSequence.Operator.First.cs | 8 +------- .../Falko.Common.Sequences/Sequences/FrozenSequence.cs | 1 + 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Sources/Falko.Common.Sequences/Asserts/SequenceExceptions.cs b/Sources/Falko.Common.Sequences/Asserts/SequenceExceptions.cs index 0f6dadb..9309908 100644 --- a/Sources/Falko.Common.Sequences/Asserts/SequenceExceptions.cs +++ b/Sources/Falko.Common.Sequences/Asserts/SequenceExceptions.cs @@ -67,7 +67,6 @@ public static void ThrowIfNotSingle(int count) } [DoesNotReturn] - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ThrowNotMatchAny() { throw new InvalidOperationException("The source sequence does not match any of the specified conditions."); diff --git a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs index 267c3a2..91807a0 100644 --- a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs +++ b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs @@ -20,14 +20,8 @@ public T First(Func predicate) { ArgumentNullException.ThrowIfNull(predicate); - scoped ref var itemsReference = ref MemoryMarshal.GetArrayDataReference(_items); - - var itemsCount = _itemsCount; - - for (var itemIndex = 0; itemIndex < itemsCount; itemIndex++) + foreach (var item in this) { - var item = Unsafe.Add(ref itemsReference, itemIndex); - if (predicate(item)) return item; } diff --git a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.cs b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.cs index ed36223..bb2f230 100644 --- a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.cs +++ b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.cs @@ -50,6 +50,7 @@ public T this[int index] /// Use the method to iterate in asynchronous code. /// /// An enumerator that can be used to iterate through the sequence. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ValueEnumerator GetEnumerator() => new(_items, _itemsCount); IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_items, _itemsCount); From 64db952fe1b1cf430375943c5043529c1c048296 Mon Sep 17 00:00:00 2001 From: Rima Falko Date: Tue, 3 Jun 2025 05:10:05 +0200 Subject: [PATCH 03/10] optimize the first operator to use readonly reference in the frozen-sequence --- .../Sequences/FrozenSequence.Operator.First.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs index 91807a0..74cb6a5 100644 --- a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs +++ b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs @@ -20,7 +20,7 @@ public T First(Func predicate) { ArgumentNullException.ThrowIfNull(predicate); - foreach (var item in this) + foreach (ref readonly var item in this) { if (predicate(item)) return item; } From 37c686e23b9485bfae0fdaf6117a4c93c55f154f Mon Sep 17 00:00:00 2001 From: Rima Falko Date: Tue, 3 Jun 2025 05:22:00 +0200 Subject: [PATCH 04/10] optimize value-enumerator to improve move-next method performance --- Falko.Common.slnx | 2 +- .../Sequences/FrozenSequence.ValueEnumerator.cs | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/Falko.Common.slnx b/Falko.Common.slnx index 09dcf30..347def6 100644 --- a/Falko.Common.slnx +++ b/Falko.Common.slnx @@ -23,4 +23,4 @@ - + \ No newline at end of file diff --git a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.ValueEnumerator.cs b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.ValueEnumerator.cs index 7ce5afc..83f6371 100644 --- a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.ValueEnumerator.cs +++ b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.ValueEnumerator.cs @@ -11,7 +11,7 @@ public ref struct ValueEnumerator private readonly int _valuesCount; - private int _currentIndex; + private int _currentIndex = -1; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal ValueEnumerator(T[] values, int valuesCount) @@ -29,19 +29,13 @@ public ref readonly T Current [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { - var currentIndex = _currentIndex; - - if (currentIndex == _valuesCount) return false; - - _currentIndex = currentIndex + 1; - - return true; + return ++_currentIndex < _valuesCount; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { - _currentIndex = 0; + _currentIndex = -1; } } } From 2ae7b10a32be8e0fd3878255f7dd403a9cde595e Mon Sep 17 00:00:00 2001 From: Rima Falko Date: Tue, 3 Jun 2025 05:25:53 +0200 Subject: [PATCH 05/10] optimize move-next method for improved performance in value-enumerator --- .../Sequences/FrozenSequence.ValueEnumerator.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.ValueEnumerator.cs b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.ValueEnumerator.cs index 83f6371..28954b1 100644 --- a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.ValueEnumerator.cs +++ b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.ValueEnumerator.cs @@ -11,11 +11,12 @@ public ref struct ValueEnumerator private readonly int _valuesCount; - private int _currentIndex = -1; + private int _currentIndex; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal ValueEnumerator(T[] values, int valuesCount) { + _currentIndex = -1; _valuesReference = ref MemoryMarshal.GetArrayDataReference(values); _valuesCount = valuesCount; } @@ -29,7 +30,15 @@ public ref readonly T Current [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { - return ++_currentIndex < _valuesCount; + var index = _currentIndex + 1; + + if (index < _valuesCount) + { + _currentIndex = index; + return true; + } + + return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] From e473567b3076e34d1be59703c82368d8a83c3543 Mon Sep 17 00:00:00 2001 From: Rima Falko Date: Tue, 3 Jun 2025 05:28:21 +0200 Subject: [PATCH 06/10] optimize first operator in frozen-sequence by removing unnecessary readonly reference --- .../Sequences/FrozenSequence.Operator.First.cs | 2 +- Sources/Falko.Common.Sequences/Sequences/FrozenSequence.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs index 74cb6a5..91807a0 100644 --- a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs +++ b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs @@ -20,7 +20,7 @@ public T First(Func predicate) { ArgumentNullException.ThrowIfNull(predicate); - foreach (ref readonly var item in this) + foreach (var item in this) { if (predicate(item)) return item; } diff --git a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.cs b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.cs index bb2f230..ed36223 100644 --- a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.cs +++ b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.cs @@ -50,7 +50,6 @@ public T this[int index] /// Use the method to iterate in asynchronous code. /// /// An enumerator that can be used to iterate through the sequence. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public ValueEnumerator GetEnumerator() => new(_items, _itemsCount); IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_items, _itemsCount); From 79886885babd121dafbaab04b4da30a17ce44227 Mon Sep 17 00:00:00 2001 From: Rima Falko Date: Tue, 3 Jun 2025 05:37:09 +0200 Subject: [PATCH 07/10] optimize the first operator in a frozen-sequence to use readonly reference and improve move-next performance --- .../Sequences/FrozenSequence.Operator.First.cs | 2 +- .../Sequences/FrozenSequence.ValueEnumerator.cs | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs index 91807a0..8224ac1 100644 --- a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs +++ b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs @@ -20,7 +20,7 @@ public T First(Func predicate) { ArgumentNullException.ThrowIfNull(predicate); - foreach (var item in this) + foreach (ref readonly var item in AsSpan()) { if (predicate(item)) return item; } diff --git a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.ValueEnumerator.cs b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.ValueEnumerator.cs index 28954b1..c39d887 100644 --- a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.ValueEnumerator.cs +++ b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.ValueEnumerator.cs @@ -30,15 +30,12 @@ public ref readonly T Current [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { - var index = _currentIndex + 1; + var nextIndex = _currentIndex + 1; - if (index < _valuesCount) - { - _currentIndex = index; - return true; - } + if (nextIndex >= _valuesCount) return false; - return false; + _currentIndex = nextIndex; + return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] From e9219d77e2711e10903f912b517deeca2d878bd4 Mon Sep 17 00:00:00 2001 From: Rima Falko Date: Tue, 3 Jun 2025 06:02:30 +0200 Subject: [PATCH 08/10] optimize the first operator in a frozen-sequence to improve performance by reducing overhead in item access --- .../Sequences/FrozenSequence.Operator.First.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs index 8224ac1..c1a8c63 100644 --- a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs +++ b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs @@ -18,15 +18,23 @@ public T First() public T First(Func predicate) { + var itemsCount = _itemsCount; + + if (itemsCount is 0) SequenceExceptions.ThrowIfEmpty(itemsCount); + ArgumentNullException.ThrowIfNull(predicate); - foreach (ref readonly var item in AsSpan()) + scoped ref var itemsReference = ref MemoryMarshal.GetArrayDataReference(_items); + + for (var itemIndex = 0; itemIndex < itemsCount; itemIndex++) { + var item = Unsafe.Add(ref itemsReference, itemIndex); + if (predicate(item)) return item; } SequenceExceptions.ThrowNotMatchAny(); - return default!; // This line is unreachable + return default; // This line is unreachable } [MethodImpl(MethodImplOptions.AggressiveInlining)] From fd79c8a12d6eb9525642132b46b5c131d92d817a Mon Sep 17 00:00:00 2001 From: Rima Falko Date: Tue, 3 Jun 2025 06:08:01 +0200 Subject: [PATCH 09/10] benchmark frozen-sequence first operator by running multiple iterations for performance evaluation --- Benchmarks/Benchmarks/FirstOperatorBenchmark.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Benchmarks/Benchmarks/FirstOperatorBenchmark.cs b/Benchmarks/Benchmarks/FirstOperatorBenchmark.cs index 6d282e3..12dee8b 100644 --- a/Benchmarks/Benchmarks/FirstOperatorBenchmark.cs +++ b/Benchmarks/Benchmarks/FirstOperatorBenchmark.cs @@ -21,12 +21,18 @@ public void Setup() [Benchmark(Baseline = true)] public void FrozenSequenceFirst() { - _ = _frozenSequence!.First(number => number is 50); + for (var i = 0; i < 3; i++) + { + _ = _frozenSequence!.First(number => number is 50); + } } [Benchmark] public void ListFirst() { - _ = _list!.First(number => number is 50); + for (var i = 0; i < 3; i++) + { + _ = _list!.First(number => number is 50); + } } } From c47fb3c60e010e51c9353017625be4719fa86b01 Mon Sep 17 00:00:00 2001 From: Rima Falko Date: Tue, 3 Jun 2025 06:15:45 +0200 Subject: [PATCH 10/10] add the last operator benchmark for performance comparison between frozen-sequence and list --- .../Benchmarks/LastOperatorBenchmark.cs | 38 +++++++++++++++++++ Benchmarks/Benchmarks/Program.cs | 5 +-- .../FrozenSequence.Operator.First.cs | 14 ++++--- .../Sequences/FrozenSequence.Operator.Last.cs | 12 ++---- 4 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 Benchmarks/Benchmarks/LastOperatorBenchmark.cs diff --git a/Benchmarks/Benchmarks/LastOperatorBenchmark.cs b/Benchmarks/Benchmarks/LastOperatorBenchmark.cs new file mode 100644 index 0000000..fac46de --- /dev/null +++ b/Benchmarks/Benchmarks/LastOperatorBenchmark.cs @@ -0,0 +1,38 @@ +using BenchmarkDotNet.Attributes; +using Falko.Common.Extensions; +using Falko.Common.Sequences; + +namespace Benchmarks; + +public class LastOperatorBenchmark +{ + private List? _list; + + private FrozenSequence? _frozenSequence; + + [GlobalSetup] + public void Setup() + { + _frozenSequence = Enumerable.Range(0, 100).ToFrozenSequence(); + + _list = Enumerable.Range(0, 100).ToList(); + } + + [Benchmark(Baseline = true)] + public void FrozenSequenceFirst() + { + for (var i = 0; i < 3; i++) + { + _ = _frozenSequence!.Last(number => number is 50); + } + } + + [Benchmark] + public void ListFirst() + { + for (var i = 0; i < 3; i++) + { + _ = _list!.Last(number => number is 50); + } + } +} diff --git a/Benchmarks/Benchmarks/Program.cs b/Benchmarks/Benchmarks/Program.cs index 3201438..c05779b 100644 --- a/Benchmarks/Benchmarks/Program.cs +++ b/Benchmarks/Benchmarks/Program.cs @@ -1,8 +1,5 @@ using BenchmarkDotNet.Running; using Benchmarks; -var b = new FirstOperatorBenchmark(); -b.Setup(); -b.FrozenSequenceFirst(); -b.ListFirst(); BenchmarkRunner.Run(); +BenchmarkRunner.Run(); diff --git a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs index c1a8c63..5ecea87 100644 --- a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs +++ b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.First.cs @@ -18,12 +18,10 @@ public T First() public T First(Func predicate) { - var itemsCount = _itemsCount; - - if (itemsCount is 0) SequenceExceptions.ThrowIfEmpty(itemsCount); - ArgumentNullException.ThrowIfNull(predicate); + var itemsCount = _itemsCount; + scoped ref var itemsReference = ref MemoryMarshal.GetArrayDataReference(_items); for (var itemIndex = 0; itemIndex < itemsCount; itemIndex++) @@ -49,8 +47,14 @@ public T First(Func predicate) { ArgumentNullException.ThrowIfNull(predicate); - foreach (ref readonly var item in this) + var itemsCount = _itemsCount; + + scoped ref var itemsReference = ref MemoryMarshal.GetArrayDataReference(_items); + + for (var itemIndex = 0; itemIndex < itemsCount; itemIndex++) { + var item = Unsafe.Add(ref itemsReference, itemIndex); + if (predicate(item)) return item; } diff --git a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.Last.cs b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.Last.cs index 1da92e9..d628b83 100644 --- a/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.Last.cs +++ b/Sources/Falko.Common.Sequences/Sequences/FrozenSequence.Operator.Last.cs @@ -19,12 +19,10 @@ public T Last() public T Last(Func predicate) { - var itemsCount = _itemsCount; - - SequenceExceptions.ThrowIfEmpty(itemsCount); - ArgumentNullException.ThrowIfNull(predicate); + var itemsCount = _itemsCount; + scoped ref var itemsReference = ref MemoryMarshal.GetArrayDataReference(_items); for (var itemIndex = itemsCount - 1; itemIndex >= 0; itemIndex--) @@ -50,12 +48,10 @@ public T Last(Func predicate) public T? LastOrDefault(Func predicate) { - var itemsCount = _itemsCount; - - if (itemsCount is 0) return default; - ArgumentNullException.ThrowIfNull(predicate); + var itemsCount = _itemsCount; + scoped ref var itemsReference = ref MemoryMarshal.GetArrayDataReference(_items); for (var itemIndex = itemsCount - 1; itemIndex >= 0; itemIndex--)