From acfb7e9558d9f49bb3bfa6ac911e54f0bd2415d5 Mon Sep 17 00:00:00 2001 From: tahakocal Date: Tue, 22 Sep 2026 13:24:10 +0300 Subject: [PATCH 1/8] Compare a vector at a time when taking the minimum of a float span Min over a span of float or double walks it one element at a time, while the integer overloads reach the vectorized MemoryExtensions.Min. Compare a vector at a time instead, reducing the lanes once the loop ends. The sequential walk returns the first NaN it meets, so the vectorized loop abandons the block and hands the whole span back to that walk as soon as a vector contains one, rather than trying to locate it. It also keeps the first of two equal values, which matters only for zero, since negative and positive zero compare equal while the reduction may keep either: when the result is zero, the first zero in the span is returned. Measured on arm64 with Vector128: 3.9x for float and 1.8x for double at 1K elements, 2.9x and 1.5x at a million. --- .../System.Linq/src/System/Linq/Min.cs | 109 +++++++++++++++--- 1 file changed, 94 insertions(+), 15 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Min.cs b/src/libraries/System.Linq/src/System/Linq/Min.cs index 7cdc7abcda1664..51ef3d8857ade9 100644 --- a/src/libraries/System.Linq/src/System/Linq/Min.cs +++ b/src/libraries/System.Linq/src/System/Linq/Min.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Numerics; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; namespace System.Linq { @@ -87,6 +89,97 @@ private static T MinIntegerEnumerator(IEnumerable source) where T : struct public static double? Min(this IEnumerable source) => MinFloat(source); + private static T MinFloat(ReadOnlySpan span) where T : struct, IFloatingPointIeee754 + { + T value; + int i = 1; + + // A vector's worth at a time, bailing out to the sequential walk as soon as a NaN + // appears, since the first NaN is the result and the walk already reports it. + if (Vector128.IsHardwareAccelerated && Vector128.IsSupported && span.Length >= Vector128.Count * 2) + { + ref T first = ref MemoryMarshal.GetReference(span); + Vector128 best = Vector128.LoadUnsafe(ref first, 0); + + if (~Vector128.Equals(best, best) == Vector128.Zero) + { + int lastVector = span.Length - Vector128.Count; + int index = Vector128.Count; + bool sawNaN = false; + + while (index <= lastVector) + { + Vector128 current = Vector128.LoadUnsafe(ref first, (uint)index); + if (~Vector128.Equals(current, current) != Vector128.Zero) + { + sawNaN = true; + break; + } + + best = Vector128.Min(best, current); + index += Vector128.Count; + } + + if (!sawNaN) + { + value = best.GetElement(0); + for (int lane = 1; lane < Vector128.Count; lane++) + { + T candidate = best.GetElement(lane); + if (candidate < value) + { + value = candidate; + } + } + + for (i = index; (uint)i < (uint)span.Length; i++) + { + T current = span[i]; + if (current < value) + { + value = current; + } + else if (T.IsNaN(current)) + { + return current; + } + } + + // Negative and positive zero compare equal, so the reduction may have kept + // either one, while the sequential walk keeps the first of two equal values. + if (value == T.Zero) + { + foreach (T element in span) + { + if (element == T.Zero) + { + return element; + } + } + } + + return value; + } + } + } + + value = span[0]; + for (; (uint)i < (uint)span.Length; i++) + { + T current = span[i]; + if (current < value) + { + value = current; + } + else if (T.IsNaN(current)) + { + return current; + } + } + + return value; + } + private static T MinFloat(this IEnumerable source) where T : struct, IFloatingPointIeee754 { T value; @@ -103,21 +196,7 @@ private static T MinFloat(this IEnumerable source) where T : struct, IFloa ThrowHelper.ThrowNoElementsException(); } - value = span[0]; - for (int i = 1; (uint)i < (uint)span.Length; i++) - { - T current = span[i]; - if (current < value) - { - value = current; - } - else if (T.IsNaN(current)) - { - return current; - } - } - - return value; + return MinFloat(span); } using IEnumerator e = source.GetEnumerator(); From b4b160c968ff51cd1b17c1b0486694e467a036ef Mon Sep 17 00:00:00 2001 From: tahakocal Date: Tue, 22 Sep 2026 13:26:07 +0300 Subject: [PATCH 2/8] Compare a vector at a time when taking the maximum of a float span Max over a span of float or double walks it one element at a time. Compare a vector at a time instead, once the leading NaNs have been skipped and a first candidate is in hand. NaN lanes are replaced with negative infinity before the comparison, because the sequential walk ignores a NaN that is not the leading one while Vector128.Max would propagate it. That substitution costs two operations per vector, which only pays off when a vector holds at least four elements, so double keeps the sequential walk: measured 0.93x to 1.01x for double against 1.97x to 3.05x for float. The walk also keeps the first of two equal values, which matters only for zero, since negative and positive zero compare equal while the reduction may keep either: when the result is zero, the first zero in the span is returned. --- .../System.Linq/src/System/Linq/Max.cs | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Max.cs b/src/libraries/System.Linq/src/System/Linq/Max.cs index 3a78d56dc35e6e..939623e3d8f094 100644 --- a/src/libraries/System.Linq/src/System/Linq/Max.cs +++ b/src/libraries/System.Linq/src/System/Linq/Max.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Numerics; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; namespace System.Linq { @@ -129,7 +131,38 @@ private static T MaxFloat(this IEnumerable source) where T : struct, IFloa return span[^1]; } - for (value = span[i]; (uint)i < (uint)span.Length; i++) + value = span[i]; + + // Only worth vectorizing when a vector holds at least four elements: the NaN lanes + // have to be replaced before the comparison, and with two elements per vector that + // costs as much as it saves. + if (Vector128.IsHardwareAccelerated && Vector128.IsSupported && + Vector128.Count >= 4 && span.Length - i >= Vector128.Count * 2) + { + ref T first = ref MemoryMarshal.GetReference(span); + Vector128 negativeInfinity = Vector128.Create(T.NegativeInfinity); + Vector128 best = Vector128.Create(value); + int lastVector = span.Length - Vector128.Count; + + while (i <= lastVector) + { + // A NaN is never the maximum here, and Vector128.Max would propagate it. + Vector128 current = Vector128.LoadUnsafe(ref first, (uint)i); + best = Vector128.Max(best, Vector128.ConditionalSelect(Vector128.Equals(current, current), current, negativeInfinity)); + i += Vector128.Count; + } + + for (int lane = 0; lane < Vector128.Count; lane++) + { + T candidate = best.GetElement(lane); + if (candidate > value) + { + value = candidate; + } + } + } + + for (; (uint)i < (uint)span.Length; i++) { if (span[i] > value) { @@ -137,6 +170,19 @@ private static T MaxFloat(this IEnumerable source) where T : struct, IFloa } } + // Negative and positive zero compare equal, so the reduction may have kept either + // one, while the sequential walk keeps the first of two equal values. + if (value == T.Zero) + { + foreach (T element in span) + { + if (element == T.Zero) + { + return element; + } + } + } + return value; } From 71d6ab98e7d80bcaa25fac45aa36c12697631acd Mon Sep 17 00:00:00 2001 From: tahakocal Date: Tue, 22 Sep 2026 13:29:13 +0300 Subject: [PATCH 3/8] Compare vectors instead of calling the comparer for float spans in Min and Max Min and Max over a span dispatch every integer type to a vectorized routine, but float and double fall through to the shared comparer walk, which calls Comparer.Default.Compare once per element. Give them a vectorized path that keeps the order Comparer.Default defines: NaN sorts below every value, so Min returns the first NaN in the span and Max ignores NaN unless every element is one; negative and positive zero compare equal, so when the result is zero the first zero in the span is returned rather than whichever one the reduction happened to keep. Max replaces NaN lanes with negative infinity, since Vector128.Max would otherwise propagate them. Measured on arm64 with Vector128: 3.0x to 4.5x for float and 3.0x to 5.4x for double, the latter gaining more because its comparer walk is the slower one. --- .../src/System/MemoryExtensions.MinMax.cs | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.MinMax.cs b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.MinMax.cs index 04344cfc23f08b..b09e17048fcafb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.MinMax.cs +++ b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.MinMax.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Runtime.Intrinsics; namespace System @@ -38,6 +39,8 @@ public static partial class MemoryExtensions if (typeof(T) == typeof(nint)) return MinMaxInteger>(span); if (typeof(T) == typeof(Int128)) return MinMaxInteger>(span); if (typeof(T) == typeof(UInt128)) return MinMaxInteger>(span); + if (typeof(T) == typeof(float)) return MinFloat(span); + if (typeof(T) == typeof(double)) return MinFloat(span); return MinMax(span, Comparer.Default); } @@ -91,6 +94,8 @@ public static partial class MemoryExtensions if (typeof(T) == typeof(nint)) return MinMaxInteger>(span); if (typeof(T) == typeof(Int128)) return MinMaxInteger>(span); if (typeof(T) == typeof(UInt128)) return MinMaxInteger>(span); + if (typeof(T) == typeof(float)) return MaxFloat(span); + if (typeof(T) == typeof(double)) return MaxFloat(span); return MinMax(span, Comparer.Default); } @@ -221,6 +226,169 @@ private interface IMinMaxCalc where T : struct, IBinaryInteger public static Vector512 Compare(Vector512 left, Vector512 right) => Vector512.Max(left, right); } + private static TOuter MinFloat(this ReadOnlySpan span) + where TInner : struct, IFloatingPointIeee754 => + Unsafe.BitCast(MinFloat(Unsafe.BitCast, ReadOnlySpan>(span))); + + private static TOuter MaxFloat(this ReadOnlySpan span) + where TInner : struct, IFloatingPointIeee754 => + Unsafe.BitCast(MaxFloat(Unsafe.BitCast, ReadOnlySpan>(span))); + + /// + /// Comparer{T}.Default orders NaN below every value and treats negative and positive zero + /// as equal, keeping whichever of the two the span holds first. Both are preserved here. + /// + private static T MinFloat(this ReadOnlySpan span) where T : struct, IFloatingPointIeee754 + { + if (span.IsEmpty) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_NoElements); + } + + T value; + int i = 1; + + if (Vector128.IsHardwareAccelerated && Vector128.IsSupported && span.Length >= Vector128.Count * 2) + { + ref T first = ref MemoryMarshal.GetReference(span); + int lastVector = span.Length - Vector128.Count; + + Vector128 best = Vector128.LoadUnsafe(ref first, 0); + Vector128 nanFound = ~Vector128.Equals(best, best); + i = Vector128.Count; + + while (i <= lastVector) + { + Vector128 current = Vector128.LoadUnsafe(ref first, (uint)i); + nanFound |= ~Vector128.Equals(current, current); + best = Vector128.Min(best, current); + i += Vector128.Count; + } + + if (nanFound != Vector128.Zero) + { + foreach (T element in span) + { + if (T.IsNaN(element)) + { + return element; + } + } + } + + value = best.GetElement(0); + for (int lane = 1; lane < Vector128.Count; lane++) + { + T candidate = best.GetElement(lane); + if (candidate < value) + { + value = candidate; + } + } + } + else + { + value = span[0]; + } + + for (; i < span.Length; i++) + { + T current = span[i]; + if (T.IsNaN(current)) + { + return current; + } + + if (current < value) + { + value = current; + } + } + + if (value == T.Zero) + { + foreach (T element in span) + { + if (element == T.Zero) + { + return element; + } + } + } + + return value; + } + + /// + private static T MaxFloat(this ReadOnlySpan span) where T : struct, IFloatingPointIeee754 + { + if (span.IsEmpty) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_NoElements); + } + + int i = 0; + while (i < span.Length && T.IsNaN(span[i])) + { + i++; + } + + if (i == span.Length) + { + return span[0]; + } + + T value = span[i]; + i++; + + if (Vector128.IsHardwareAccelerated && Vector128.IsSupported && span.Length - i >= Vector128.Count * 2) + { + ref T first = ref MemoryMarshal.GetReference(span); + Vector128 negativeInfinity = Vector128.Create(T.NegativeInfinity); + Vector128 best = Vector128.Create(value); + int lastVector = span.Length - Vector128.Count; + + while (i <= lastVector) + { + // A NaN is never the maximum here, and Vector128.Max would propagate it. + Vector128 current = Vector128.LoadUnsafe(ref first, (uint)i); + best = Vector128.Max(best, Vector128.ConditionalSelect(Vector128.Equals(current, current), current, negativeInfinity)); + i += Vector128.Count; + } + + for (int lane = 0; lane < Vector128.Count; lane++) + { + T candidate = best.GetElement(lane); + if (candidate > value) + { + value = candidate; + } + } + } + + for (; i < span.Length; i++) + { + T current = span[i]; + if (current > value) + { + value = current; + } + } + + if (value == T.Zero) + { + foreach (T element in span) + { + if (element == T.Zero) + { + return element; + } + } + } + + return value; + } + private static TOuter MinMaxInteger(this ReadOnlySpan span) where TInner : struct, IBinaryInteger where TMinMax : IMinMaxCalc => From a4dc15f989dcac73ffeaae3c080a792820854b3c Mon Sep 17 00:00:00 2001 From: tahakocal Date: Tue, 22 Sep 2026 15:19:19 +0300 Subject: [PATCH 4/8] Load the vectors through the span instead of an unsafe reference --- .../src/System/MemoryExtensions.MinMax.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.MinMax.cs b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.MinMax.cs index b09e17048fcafb..24559b94fd81d1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.MinMax.cs +++ b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.MinMax.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Runtime.Intrinsics; namespace System @@ -250,16 +249,15 @@ private static T MinFloat(this ReadOnlySpan span) where T : struct, IFloat if (Vector128.IsHardwareAccelerated && Vector128.IsSupported && span.Length >= Vector128.Count * 2) { - ref T first = ref MemoryMarshal.GetReference(span); int lastVector = span.Length - Vector128.Count; - Vector128 best = Vector128.LoadUnsafe(ref first, 0); + Vector128 best = Vector128.Create(span); Vector128 nanFound = ~Vector128.Equals(best, best); i = Vector128.Count; while (i <= lastVector) { - Vector128 current = Vector128.LoadUnsafe(ref first, (uint)i); + Vector128 current = Vector128.Create(span.Slice(i)); nanFound |= ~Vector128.Equals(current, current); best = Vector128.Min(best, current); i += Vector128.Count; @@ -343,7 +341,6 @@ private static T MaxFloat(this ReadOnlySpan span) where T : struct, IFloat if (Vector128.IsHardwareAccelerated && Vector128.IsSupported && span.Length - i >= Vector128.Count * 2) { - ref T first = ref MemoryMarshal.GetReference(span); Vector128 negativeInfinity = Vector128.Create(T.NegativeInfinity); Vector128 best = Vector128.Create(value); int lastVector = span.Length - Vector128.Count; @@ -351,7 +348,7 @@ private static T MaxFloat(this ReadOnlySpan span) where T : struct, IFloat while (i <= lastVector) { // A NaN is never the maximum here, and Vector128.Max would propagate it. - Vector128 current = Vector128.LoadUnsafe(ref first, (uint)i); + Vector128 current = Vector128.Create(span.Slice(i)); best = Vector128.Max(best, Vector128.ConditionalSelect(Vector128.Equals(current, current), current, negativeInfinity)); i += Vector128.Count; } From 62d6417e9d45ac44d043348b9d7610f10b0a15bd Mon Sep 17 00:00:00 2001 From: tahakocal Date: Tue, 22 Sep 2026 15:19:29 +0300 Subject: [PATCH 5/8] Load the vectors through the span instead of an unsafe reference --- src/libraries/System.Linq/src/System/Linq/Min.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Min.cs b/src/libraries/System.Linq/src/System/Linq/Min.cs index 51ef3d8857ade9..cb885996d01511 100644 --- a/src/libraries/System.Linq/src/System/Linq/Min.cs +++ b/src/libraries/System.Linq/src/System/Linq/Min.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Numerics; -using System.Runtime.InteropServices; using System.Runtime.Intrinsics; namespace System.Linq @@ -98,8 +97,7 @@ private static T MinFloat(ReadOnlySpan span) where T : struct, IFloatingPo // appears, since the first NaN is the result and the walk already reports it. if (Vector128.IsHardwareAccelerated && Vector128.IsSupported && span.Length >= Vector128.Count * 2) { - ref T first = ref MemoryMarshal.GetReference(span); - Vector128 best = Vector128.LoadUnsafe(ref first, 0); + Vector128 best = Vector128.Create(span); if (~Vector128.Equals(best, best) == Vector128.Zero) { @@ -109,7 +107,7 @@ private static T MinFloat(ReadOnlySpan span) where T : struct, IFloatingPo while (index <= lastVector) { - Vector128 current = Vector128.LoadUnsafe(ref first, (uint)index); + Vector128 current = Vector128.Create(span.Slice(index)); if (~Vector128.Equals(current, current) != Vector128.Zero) { sawNaN = true; From 73a88ba7fb71549134846e89f49dbf6eab0c23d3 Mon Sep 17 00:00:00 2001 From: tahakocal Date: Tue, 22 Sep 2026 15:19:36 +0300 Subject: [PATCH 6/8] Load the vectors through the span instead of an unsafe reference --- src/libraries/System.Linq/src/System/Linq/Max.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Max.cs b/src/libraries/System.Linq/src/System/Linq/Max.cs index 939623e3d8f094..a29b532a9e3cc9 100644 --- a/src/libraries/System.Linq/src/System/Linq/Max.cs +++ b/src/libraries/System.Linq/src/System/Linq/Max.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Numerics; -using System.Runtime.InteropServices; using System.Runtime.Intrinsics; namespace System.Linq @@ -139,7 +138,6 @@ private static T MaxFloat(this IEnumerable source) where T : struct, IFloa if (Vector128.IsHardwareAccelerated && Vector128.IsSupported && Vector128.Count >= 4 && span.Length - i >= Vector128.Count * 2) { - ref T first = ref MemoryMarshal.GetReference(span); Vector128 negativeInfinity = Vector128.Create(T.NegativeInfinity); Vector128 best = Vector128.Create(value); int lastVector = span.Length - Vector128.Count; @@ -147,7 +145,7 @@ private static T MaxFloat(this IEnumerable source) where T : struct, IFloa while (i <= lastVector) { // A NaN is never the maximum here, and Vector128.Max would propagate it. - Vector128 current = Vector128.LoadUnsafe(ref first, (uint)i); + Vector128 current = Vector128.Create(span.Slice(i)); best = Vector128.Max(best, Vector128.ConditionalSelect(Vector128.Equals(current, current), current, negativeInfinity)); i += Vector128.Count; } From 58612ce88532482272d3e73e7810ca7ac4a9c673 Mon Sep 17 00:00:00 2001 From: tahakocal Date: Tue, 22 Sep 2026 16:38:51 +0300 Subject: [PATCH 7/8] Walk the span forward so the vector loads need no bounds check --- .../System.Linq/src/System/Linq/Max.cs | 10 +++++---- .../System.Linq/src/System/Linq/Min.cs | 11 +++++----- .../src/System/MemoryExtensions.MinMax.cs | 22 ++++++++++--------- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Max.cs b/src/libraries/System.Linq/src/System/Linq/Max.cs index a29b532a9e3cc9..ec7c1735d604ce 100644 --- a/src/libraries/System.Linq/src/System/Linq/Max.cs +++ b/src/libraries/System.Linq/src/System/Linq/Max.cs @@ -140,16 +140,18 @@ private static T MaxFloat(this IEnumerable source) where T : struct, IFloa { Vector128 negativeInfinity = Vector128.Create(T.NegativeInfinity); Vector128 best = Vector128.Create(value); - int lastVector = span.Length - Vector128.Count; + ReadOnlySpan remaining = span.Slice(i); - while (i <= lastVector) + while (remaining.Length >= Vector128.Count) { // A NaN is never the maximum here, and Vector128.Max would propagate it. - Vector128 current = Vector128.Create(span.Slice(i)); + Vector128 current = Vector128.Create(remaining); best = Vector128.Max(best, Vector128.ConditionalSelect(Vector128.Equals(current, current), current, negativeInfinity)); - i += Vector128.Count; + remaining = remaining.Slice(Vector128.Count); } + i = span.Length - remaining.Length; + for (int lane = 0; lane < Vector128.Count; lane++) { T candidate = best.GetElement(lane); diff --git a/src/libraries/System.Linq/src/System/Linq/Min.cs b/src/libraries/System.Linq/src/System/Linq/Min.cs index cb885996d01511..5ccb16fc06860e 100644 --- a/src/libraries/System.Linq/src/System/Linq/Min.cs +++ b/src/libraries/System.Linq/src/System/Linq/Min.cs @@ -101,13 +101,12 @@ private static T MinFloat(ReadOnlySpan span) where T : struct, IFloatingPo if (~Vector128.Equals(best, best) == Vector128.Zero) { - int lastVector = span.Length - Vector128.Count; - int index = Vector128.Count; + ReadOnlySpan remaining = span.Slice(Vector128.Count); bool sawNaN = false; - while (index <= lastVector) + while (remaining.Length >= Vector128.Count) { - Vector128 current = Vector128.Create(span.Slice(index)); + Vector128 current = Vector128.Create(remaining); if (~Vector128.Equals(current, current) != Vector128.Zero) { sawNaN = true; @@ -115,9 +114,11 @@ private static T MinFloat(ReadOnlySpan span) where T : struct, IFloatingPo } best = Vector128.Min(best, current); - index += Vector128.Count; + remaining = remaining.Slice(Vector128.Count); } + int index = span.Length - remaining.Length; + if (!sawNaN) { value = best.GetElement(0); diff --git a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.MinMax.cs b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.MinMax.cs index 24559b94fd81d1..6e2b48cea96eb1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.MinMax.cs +++ b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.MinMax.cs @@ -249,20 +249,20 @@ private static T MinFloat(this ReadOnlySpan span) where T : struct, IFloat if (Vector128.IsHardwareAccelerated && Vector128.IsSupported && span.Length >= Vector128.Count * 2) { - int lastVector = span.Length - Vector128.Count; - Vector128 best = Vector128.Create(span); Vector128 nanFound = ~Vector128.Equals(best, best); - i = Vector128.Count; + ReadOnlySpan remaining = span.Slice(Vector128.Count); - while (i <= lastVector) + while (remaining.Length >= Vector128.Count) { - Vector128 current = Vector128.Create(span.Slice(i)); + Vector128 current = Vector128.Create(remaining); nanFound |= ~Vector128.Equals(current, current); best = Vector128.Min(best, current); - i += Vector128.Count; + remaining = remaining.Slice(Vector128.Count); } + i = span.Length - remaining.Length; + if (nanFound != Vector128.Zero) { foreach (T element in span) @@ -343,16 +343,18 @@ private static T MaxFloat(this ReadOnlySpan span) where T : struct, IFloat { Vector128 negativeInfinity = Vector128.Create(T.NegativeInfinity); Vector128 best = Vector128.Create(value); - int lastVector = span.Length - Vector128.Count; + ReadOnlySpan remaining = span.Slice(i); - while (i <= lastVector) + while (remaining.Length >= Vector128.Count) { // A NaN is never the maximum here, and Vector128.Max would propagate it. - Vector128 current = Vector128.Create(span.Slice(i)); + Vector128 current = Vector128.Create(remaining); best = Vector128.Max(best, Vector128.ConditionalSelect(Vector128.Equals(current, current), current, negativeInfinity)); - i += Vector128.Count; + remaining = remaining.Slice(Vector128.Count); } + i = span.Length - remaining.Length; + for (int lane = 0; lane < Vector128.Count; lane++) { T candidate = best.GetElement(lane); From b9e1987b702b64d607e20394467e7aa065b89f3c Mon Sep 17 00:00:00 2001 From: tahakocal Date: Tue, 22 Sep 2026 16:50:33 +0300 Subject: [PATCH 8/8] Accumulate the NaN lanes instead of testing them every iteration --- src/libraries/System.Linq/src/System/Linq/Min.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Min.cs b/src/libraries/System.Linq/src/System/Linq/Min.cs index 5ccb16fc06860e..3a349dae6e5e4d 100644 --- a/src/libraries/System.Linq/src/System/Linq/Min.cs +++ b/src/libraries/System.Linq/src/System/Linq/Min.cs @@ -102,24 +102,20 @@ private static T MinFloat(ReadOnlySpan span) where T : struct, IFloatingPo if (~Vector128.Equals(best, best) == Vector128.Zero) { ReadOnlySpan remaining = span.Slice(Vector128.Count); - bool sawNaN = false; + Vector128 nanFound = Vector128.Zero; + // Accumulating the NaN lanes and testing once keeps the loop branchless. while (remaining.Length >= Vector128.Count) { Vector128 current = Vector128.Create(remaining); - if (~Vector128.Equals(current, current) != Vector128.Zero) - { - sawNaN = true; - break; - } - + nanFound |= ~Vector128.Equals(current, current); best = Vector128.Min(best, current); remaining = remaining.Slice(Vector128.Count); } int index = span.Length - remaining.Length; - if (!sawNaN) + if (nanFound == Vector128.Zero) { value = best.GetElement(0); for (int lane = 1; lane < Vector128.Count; lane++)