Skip to content

Commit

Permalink
Improve perf of Enumerable.Order{Descending} for primitives (#76733)
Browse files Browse the repository at this point in the history
* Improve perf of Enumerable.Order{Descending} for primitives

For non-floating point primitive types, stable sorts are indistinguishable from unstable sorts.  They could be distinguishable if there are custom keys associated with each item, but Order{Descending} doesn't have separate keys from the elements themselves.  As such, we can avoid all of the overhead associated with the int[] map Order{Descending}By creates as part of implementing the stable sort, avoid always specifying a comparer, etc.  This PR does so for Order{Descending}(comparer) followed by ToArray, ToList, and foreach/GetEnumerator.

* Don't try to optimize custom comparers
  • Loading branch information
stephentoub committed Oct 10, 2022
1 parent ecc1ca1 commit f156fb9
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 11 deletions.
24 changes: 20 additions & 4 deletions src/libraries/System.Linq/src/System/Linq/OrderBy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Runtime.CompilerServices;

namespace System.Linq
{
Expand All @@ -23,7 +24,7 @@ public static partial class Enumerable
/// This method compares elements by using the default comparer <see cref="Comparer{T}.Default"/>.
/// </remarks>
public static IOrderedEnumerable<T> Order<T>(this IEnumerable<T> source) =>
OrderBy(source, EnumerableSorter<T>.IdentityFunc);
Order(source, comparer: null);

/// <summary>
/// Sorts the elements of a sequence in ascending order.
Expand All @@ -42,7 +43,9 @@ public static partial class Enumerable
/// If comparer is <see langword="null"/>, the default comparer <see cref="Comparer{T}.Default"/> is used to compare elements.
/// </remarks>
public static IOrderedEnumerable<T> Order<T>(this IEnumerable<T> source, IComparer<T>? comparer) =>
OrderBy(source, EnumerableSorter<T>.IdentityFunc, comparer);
TypeIsImplicitlyStable<T>() && (comparer is null || comparer == Comparer<T>.Default) ?
new OrderedImplicitlyStableEnumerable<T>(source, descending: false) :
OrderBy(source, EnumerableSorter<T>.IdentityFunc, comparer);

public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
=> new OrderedEnumerable<TSource, TKey>(source, keySelector, null, false, null);
Expand All @@ -66,7 +69,7 @@ public static partial class Enumerable
/// This method compares elements by using the default comparer <see cref="Comparer{T}.Default"/>.
/// </remarks>
public static IOrderedEnumerable<T> OrderDescending<T>(this IEnumerable<T> source) =>
OrderByDescending(source, EnumerableSorter<T>.IdentityFunc);
OrderDescending(source, comparer: null);

/// <summary>
/// Sorts the elements of a sequence in descending order.
Expand All @@ -85,7 +88,9 @@ public static partial class Enumerable
/// If comparer is <see langword="null"/>, the default comparer <see cref="Comparer{T}.Default"/> is used to compare elements.
/// </remarks>
public static IOrderedEnumerable<T> OrderDescending<T>(this IEnumerable<T> source, IComparer<T>? comparer) =>
OrderByDescending(source, EnumerableSorter<T>.IdentityFunc, comparer);
TypeIsImplicitlyStable<T>() && (comparer is null || comparer == Comparer<T>.Default) ?
new OrderedImplicitlyStableEnumerable<T>(source, descending: true) :
OrderByDescending(source, EnumerableSorter<T>.IdentityFunc, comparer);

public static IOrderedEnumerable<TSource> OrderByDescending<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) =>
new OrderedEnumerable<TSource, TKey>(source, keySelector, null, true, null);
Expand Down Expand Up @@ -132,6 +137,17 @@ public static partial class Enumerable

return source.CreateOrderedEnumerable(keySelector, comparer, true);
}

/// <summary>Gets whether the results of an unstable sort will be observably the same as a stable sort.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool TypeIsImplicitlyStable<T>() =>
typeof(T) == typeof(sbyte) || typeof(T) == typeof(byte) ||
typeof(T) == typeof(int) || typeof(T) == typeof(uint) ||
typeof(T) == typeof(short) || typeof(T) == typeof(ushort) ||
typeof(T) == typeof(long) || typeof(T) == typeof(ulong) ||
typeof(T) == typeof(Int128) || typeof(T) == typeof(UInt128) ||
typeof(T) == typeof(nint) || typeof(T) == typeof(nuint) ||
typeof(T) == typeof(bool) || typeof(T) == typeof(char);
}

public interface IOrderedEnumerable<out TElement> : IEnumerable<TElement>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@

using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;

namespace System.Linq
{
internal abstract partial class OrderedEnumerable<TElement> : IPartition<TElement>
{
public TElement[] ToArray()
public virtual TElement[] ToArray()
{
Buffer<TElement> buffer = new Buffer<TElement>(_source);

Expand All @@ -29,7 +29,7 @@ public TElement[] ToArray()
return array;
}

public List<TElement> ToList()
public virtual List<TElement> ToList()
{
Buffer<TElement> buffer = new Buffer<TElement>(_source);
int count = buffer._count;
Expand Down Expand Up @@ -247,4 +247,21 @@ private TElement Last(Buffer<TElement> buffer)
return value;
}
}

internal sealed partial class OrderedImplicitlyStableEnumerable<TElement> : OrderedEnumerable<TElement>
{
public override TElement[] ToArray()
{
TElement[] array = _source.ToArray();
Sort(array, _descending);
return array;
}

public override List<TElement> ToList()
{
List<TElement> list = _source.ToList();
Sort(CollectionsMarshal.AsSpan(list), _descending);
return list;
}
}
}
57 changes: 53 additions & 4 deletions src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal abstract partial class OrderedEnumerable<TElement> : IOrderedEnumerable
private int[] SortedMap(Buffer<TElement> buffer, int minIdx, int maxIdx) =>
GetEnumerableSorter().Sort(buffer._items, buffer._count, minIdx, maxIdx);

public IEnumerator<TElement> GetEnumerator()
public virtual IEnumerator<TElement> GetEnumerator()
{
Buffer<TElement> buffer = new Buffer<TElement>(_source);
if (buffer._count > 0)
Expand Down Expand Up @@ -62,9 +62,7 @@ internal IEnumerator<TElement> GetEnumerator(int minIdx, int maxIdx)

internal abstract EnumerableSorter<TElement> GetEnumerableSorter(EnumerableSorter<TElement>? next);

private CachingComparer<TElement> GetComparer() => GetComparer(null);

internal abstract CachingComparer<TElement> GetComparer(CachingComparer<TElement>? childComparer);
internal abstract CachingComparer<TElement> GetComparer(CachingComparer<TElement>? childComparer = null);

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

Expand Down Expand Up @@ -159,6 +157,57 @@ internal override CachingComparer<TElement> GetComparer(CachingComparer<TElement
}
}

/// <summary>An ordered enumerable used by Order/OrderDescending for Ts that are bitwise indistinguishable for any considered equal.</summary>
internal sealed partial class OrderedImplicitlyStableEnumerable<TElement> : OrderedEnumerable<TElement>
{
private readonly bool _descending;

public OrderedImplicitlyStableEnumerable(IEnumerable<TElement> source, bool descending) : base(source)
{
Debug.Assert(Enumerable.TypeIsImplicitlyStable<TElement>());

if (source is null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}

_descending = descending;
}

internal override CachingComparer<TElement> GetComparer(CachingComparer<TElement>? childComparer) =>
childComparer == null ?
new CachingComparer<TElement, TElement>(EnumerableSorter<TElement>.IdentityFunc, Comparer<TElement>.Default, _descending) :
new CachingComparerWithChild<TElement, TElement>(EnumerableSorter<TElement>.IdentityFunc, Comparer<TElement>.Default, _descending, childComparer);

internal override EnumerableSorter<TElement> GetEnumerableSorter(EnumerableSorter<TElement>? next) =>
new EnumerableSorter<TElement, TElement>(EnumerableSorter<TElement>.IdentityFunc, Comparer<TElement>.Default, _descending, next);

public override IEnumerator<TElement> GetEnumerator()
{
var buffer = new Buffer<TElement>(_source);
if (buffer._count > 0)
{
Sort(buffer._items.AsSpan(0, buffer._count), _descending);
for (int i = 0; i < buffer._count; i++)
{
yield return buffer._items[i];
}
}
}

private static void Sort(Span<TElement> span, bool descending)
{
if (descending)
{
span.Sort(static (a, b) => Comparer<TElement>.Default.Compare(b, a));
}
else
{
span.Sort();
}
}
}

// A comparer that chains comparisons, and pushes through the last element found to be
// lower or higher (depending on use), so as to represent the sort of comparisons
// done by OrderBy().ThenBy() combinations.
Expand Down
42 changes: 42 additions & 0 deletions src/libraries/System.Linq/tests/OrderDescendingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,48 @@ public void SourceReverseOfResultNullPassedAsComparer()
Assert.Equal(expected, source.OrderDescending(null));
}

[Fact]
public void OrderedDescendingToArray()
{
var source = new[]
{
5, 9, 6, 7, 8, 5, 20
};
var expected = new[]
{
20, 9, 8, 7, 6, 5, 5
};

Assert.Equal(expected, source.OrderDescending().ToArray());
}

[Fact]
public void EmptyOrderedDescendingToArray()
{
Assert.Empty(Enumerable.Empty<int>().OrderDescending().ToArray());
}

[Fact]
public void OrderedDescendingToList()
{
var source = new[]
{
5, 9, 6, 7, 8, 5, 20
};
var expected = new[]
{
20, 9, 8, 7, 6, 5, 5
};

Assert.Equal(expected, source.OrderDescending().ToList());
}

[Fact]
public void EmptyOrderedDescendingToList()
{
Assert.Empty(Enumerable.Empty<int>().OrderDescending().ToList());
}

[Fact]
public void SameKeysVerifySortStable()
{
Expand Down
8 changes: 8 additions & 0 deletions src/libraries/System.Linq/tests/OrderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -485,5 +485,13 @@ public void CultureOrderElementAt()
}
}
}

[Fact]
public void StableSort_CustomComparerAlwaysReturns0()
{
byte[] values = new byte[] { 0x45, 0x7D, 0x4B, 0x61, 0x27 };
byte[] newValues = values.Order(Comparer<byte>.Create((a, b) => 0)).ToArray();
AssertExtensions.SequenceEqual(values, newValues);
}
}
}

0 comments on commit f156fb9

Please sign in to comment.