Add MemoryExtensions.Min/Max#128306
Conversation
|
Tagging subscribers to this area: @dotnet/area-system-memory |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds Min and Max extension methods on ReadOnlySpan<T> (with optional IComparer<T>), mirroring LINQ's Enumerable.Min/Max semantics for spans. Includes ref assembly updates and tests.
Changes:
- New
Min/MaxReadOnlySpan extension methods with empty-span and null-element handling. - Reference assembly exposes the four new API overloads.
- New test file covering empty, all-null, mixed-null, default comparer, and custom comparer scenarios.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs | Implements Min/Max with a generic comparer fast path and null-skipping inner core. |
| src/libraries/System.Memory/ref/System.Memory.cs | Adds public surface for the four new overloads. |
| src/libraries/System.Memory/tests/System.Memory.Tests.csproj | Includes the new test file. |
| src/libraries/System.Memory/tests/ReadOnlySpan/MinMax.cs | Tests for empty/null/normal/custom-comparer behaviors. |
| /// <param name="comparer">The <see cref="IComparer{T}"/> to compare values.</param> | ||
| /// <returns>The minimum value in the span.</returns> | ||
| /// <exception cref="InvalidOperationException"><paramref name="span"/> is empty and <typeparamref name="T"/> is a non-nullable value type.</exception> | ||
| public static T? Min<T>(this ReadOnlySpan<T> span, IComparer<T> comparer) |
| T next = span[i]; | ||
| if (next is not null && comparer.Compare(next, value) < 0) |
| static T? MinCore<TComparer>(ReadOnlySpan<T> span, TComparer comparer) | ||
| where TComparer : IComparer<T> |
| public static void MinMax_CustomComparer_IsUsed() | ||
| { | ||
| ReadOnlySpan<int> ints = new int[] { 4, -1, 7, 2 }; | ||
| IComparer<int> reverse = Comparer<int>.Create((left, right) => right.CompareTo(left)); |
| public static T? Max<T>(this System.ReadOnlySpan<T> span, System.Collections.Generic.IComparer<T> comparer) { throw null; } | ||
| public static T? Min<T>(this System.ReadOnlySpan<T> span) { throw null; } | ||
| public static T? Min<T>(this System.ReadOnlySpan<T> span, System.Collections.Generic.IComparer<T> comparer) { throw null; } | ||
| [System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute(-1)] |
There was a problem hiding this comment.
This remark makes sense.
However this is not part of the approved API.
@tannergooding @bartonjs How should we handle such an update?
There was a problem hiding this comment.
The remark is largely incorrect. The other APIs are there for historical reasons and are not necessary for modern .NET due to the 1st class spans feature.
We intentionally did not expose the span APIs here accordingly.
| /// <typeparam name="T">The type of the elements in the span.</typeparam> | ||
| /// <param name="span">The span of values to determine the minimum value of.</param> | ||
| /// <returns>The minimum value in the span.</returns> | ||
| /// <exception cref="InvalidOperationException"><paramref name="span"/> is empty and <typeparamref name="T"/> is a non-nullable value type.</exception> |
|
Enumerable's Min/Max already have vectorized logic implementing this. We should move that logic into these new methods, then switch LINQ to call them instead |
| T? value = span[0]; | ||
| int i = 1; | ||
|
|
||
| while (value is null) | ||
| { | ||
| if ((uint)i >= (uint)span.Length) | ||
| { | ||
| return value; | ||
| } | ||
| value = span[i++]; | ||
| } | ||
|
|
||
| for (; (uint)i < (uint)span.Length; i++) | ||
| { | ||
| T next = span[i]; | ||
| if (next is not null && comparer.Compare(next, value) < 0) | ||
| { | ||
| value = next; | ||
| } | ||
| } |
| /// <typeparam name="T">The type of the elements in the span.</typeparam> | ||
| /// <param name="span">The span of values to determine the minimum value of.</param> | ||
| /// <returns>The minimum value in the span.</returns> | ||
| /// <exception cref="InvalidOperationException"><paramref name="span"/> is empty and <typeparamref name="T"/> is a non-nullable value type.</exception> |
| public static T? Max<T>(this System.ReadOnlySpan<T> span, System.Collections.Generic.IComparer<T> comparer) { throw null; } | ||
| public static T? Min<T>(this System.ReadOnlySpan<T> span) { throw null; } | ||
| public static T? Min<T>(this System.ReadOnlySpan<T> span, System.Collections.Generic.IComparer<T> comparer) { throw null; } | ||
| [System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute(-1)] |
| TestHelpers.AssertThrows<InvalidOperationException, int>(span, (_span) => _span.Min()); | ||
| TestHelpers.AssertThrows<InvalidOperationException, int>(span, (_span) => _span.Max()); |
| public static T? Min<T>(this ReadOnlySpan<T> span) => | ||
| Min(span, Comparer<T>.Default); | ||
|
|
| for (; (uint)i < (uint)span.Length; i++) | ||
| { | ||
| T next = span[i]; | ||
| if (next is not null && comparer.Compare(next, value) < 0) | ||
| { | ||
| value = next; | ||
| } | ||
| } |
| /// <remarks> | ||
| /// <para>If type <typeparamref name="T" /> implements <see cref="System.IComparable{T}" />, the <see cref="Min{T}(ReadOnlySpan{T})" /> method uses that implementation to compare values. Otherwise, if type <typeparamref name="T" /> implements <see cref="System.IComparable" />, that implementation is used to compare values.</para> |
| Min(span, Comparer<T>.Default); | ||
|
|
||
| /// <summary> | ||
| /// Returns the minimum value in the span. | ||
| /// </summary> | ||
| /// <typeparam name="T">The type of the elements in the span.</typeparam> | ||
| /// <param name="span">The span of values to determine the minimum value of.</param> | ||
| /// <param name="comparer">The <see cref="IComparer{T}"/> to compare values.</param> | ||
| /// <returns>The minimum value in the span.</returns> | ||
| /// <exception cref="ArgumentNullException"><paramref name="comparer"/> is <see langword="null"/>.</exception> | ||
| /// <exception cref="InvalidOperationException"><paramref name="span"/> is empty and <typeparamref name="T"/> is a non-nullable value type.</exception> | ||
| /// <remarks> | ||
| /// <para>If <typeparamref name="T" /> is a reference type and the span sequence is empty, this method returns <see langword="null" />.</para> | ||
| /// <para>Null values are ignored when determining the minimum value. If the span contains at least one non-null value, the minimum of those values is returned. If the span does not contain any non-null values, <see langword="null" /> is returned.</para> | ||
| /// </remarks> | ||
| public static T? Min<T>(this ReadOnlySpan<T> span, IComparer<T> comparer) | ||
| { | ||
| if (comparer is null) | ||
| ThrowHelper.ThrowArgumentNullException(ExceptionArgument.comparer); |
| [Fact] | ||
| public static void MinMax_DefaultComparer_ProducesExpectedValues() |
| { | ||
| ReadOnlySpan<int> span = ReadOnlySpan<int>.Empty; | ||
|
|
||
| TestHelpers.AssertThrows<InvalidOperationException, int>(span, (_span) => _span.Min()); |
| public static T? Min<T>(this ReadOnlySpan<T> span) => | ||
| Min(span, Comparer<T>.Default); |
| /// </remarks> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static T? Min<T>(this ReadOnlySpan<T> span) => | ||
| Min(span, Comparer<T>.Default); |
There was a problem hiding this comment.
Much like with LINQ, this should be passing comparer: null and then we should be treating a null comparer as selecting the default, not throwing.
| /// <para>If <typeparamref name="T" /> is a reference type and the span sequence is empty, this method returns <see langword="null" />.</para> | ||
| /// <para>Null values are ignored when determining the minimum value. If the span contains at least one non-null value, the minimum of those values is returned. If the span does not contain any non-null values, <see langword="null" /> is returned.</para> | ||
| /// </remarks> | ||
| public static T? Min<T>(this ReadOnlySpan<T> span, IComparer<T> comparer) |
There was a problem hiding this comment.
It's unclear why this deviates from the enumerable logic in implementation.
I'd generally expect the two entry points to be nearly identical. Covering the acceleration and other checks like: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Linq/src/System/Linq/Min.cs#L292-L373
The biggest difference should be that if (!enumerator.MoveNext()) becomes if (i >= span.Length). The JIT should already know that span.Length can't be negative and should know the same of i since we initialize it to 0 and only ever do ++
This pull request adds new
MinandMaxextension methods forReadOnlySpan<T>to theSystem.Memorylibrary, allowing users to efficiently find the minimum and maximum values in a span, optionally using a custom comparer. Comprehensive unit tests are included to verify correct behavior for various data types and edge cases.New
MinandMaxmethods forReadOnlySpan<T>:Min<T>andMax<T>extension methods toMemoryExtensionsforReadOnlySpan<T>, supporting both default and custom comparers, and handling nullable and reference types gracefully. These methods throw an exception on empty spans of non-nullable value types and returnnullfor empty spans of reference or nullable types. [1] [2]Unit tests:
ReadOnlySpan/MinMax.csto cover empty spans, all-null values, custom comparers, and typical scenarios for both value and reference types.System.Memory.Tests.csprojto ensure the tests are run as part of the test suite.Fixes #127083