Skip to content

Commit f156fb9

Browse files
authored
Improve perf of Enumerable.Order{Descending} for primitives (#76733)
* 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
1 parent ecc1ca1 commit f156fb9

File tree

5 files changed

+143
-11
lines changed

5 files changed

+143
-11
lines changed

src/libraries/System.Linq/src/System/Linq/OrderBy.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Generic;
5+
using System.Runtime.CompilerServices;
56

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

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

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

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

9095
public static IOrderedEnumerable<TSource> OrderByDescending<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) =>
9196
new OrderedEnumerable<TSource, TKey>(source, keySelector, null, true, null);
@@ -132,6 +137,17 @@ public static IOrderedEnumerable<TSource> ThenByDescending<TSource, TKey>(this I
132137

133138
return source.CreateOrderedEnumerable(keySelector, comparer, true);
134139
}
140+
141+
/// <summary>Gets whether the results of an unstable sort will be observably the same as a stable sort.</summary>
142+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
143+
internal static bool TypeIsImplicitlyStable<T>() =>
144+
typeof(T) == typeof(sbyte) || typeof(T) == typeof(byte) ||
145+
typeof(T) == typeof(int) || typeof(T) == typeof(uint) ||
146+
typeof(T) == typeof(short) || typeof(T) == typeof(ushort) ||
147+
typeof(T) == typeof(long) || typeof(T) == typeof(ulong) ||
148+
typeof(T) == typeof(Int128) || typeof(T) == typeof(UInt128) ||
149+
typeof(T) == typeof(nint) || typeof(T) == typeof(nuint) ||
150+
typeof(T) == typeof(bool) || typeof(T) == typeof(char);
135151
}
136152

137153
public interface IOrderedEnumerable<out TElement> : IEnumerable<TElement>

src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33

44
using System.Collections;
55
using System.Collections.Generic;
6-
using System.Diagnostics.CodeAnalysis;
6+
using System.Runtime.InteropServices;
77

88
namespace System.Linq
99
{
1010
internal abstract partial class OrderedEnumerable<TElement> : IPartition<TElement>
1111
{
12-
public TElement[] ToArray()
12+
public virtual TElement[] ToArray()
1313
{
1414
Buffer<TElement> buffer = new Buffer<TElement>(_source);
1515

@@ -29,7 +29,7 @@ public TElement[] ToArray()
2929
return array;
3030
}
3131

32-
public List<TElement> ToList()
32+
public virtual List<TElement> ToList()
3333
{
3434
Buffer<TElement> buffer = new Buffer<TElement>(_source);
3535
int count = buffer._count;
@@ -247,4 +247,21 @@ private TElement Last(Buffer<TElement> buffer)
247247
return value;
248248
}
249249
}
250+
251+
internal sealed partial class OrderedImplicitlyStableEnumerable<TElement> : OrderedEnumerable<TElement>
252+
{
253+
public override TElement[] ToArray()
254+
{
255+
TElement[] array = _source.ToArray();
256+
Sort(array, _descending);
257+
return array;
258+
}
259+
260+
public override List<TElement> ToList()
261+
{
262+
List<TElement> list = _source.ToList();
263+
Sort(CollectionsMarshal.AsSpan(list), _descending);
264+
return list;
265+
}
266+
}
250267
}

src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ internal abstract partial class OrderedEnumerable<TElement> : IOrderedEnumerable
1818
private int[] SortedMap(Buffer<TElement> buffer, int minIdx, int maxIdx) =>
1919
GetEnumerableSorter().Sort(buffer._items, buffer._count, minIdx, maxIdx);
2020

21-
public IEnumerator<TElement> GetEnumerator()
21+
public virtual IEnumerator<TElement> GetEnumerator()
2222
{
2323
Buffer<TElement> buffer = new Buffer<TElement>(_source);
2424
if (buffer._count > 0)
@@ -62,9 +62,7 @@ internal IEnumerator<TElement> GetEnumerator(int minIdx, int maxIdx)
6262

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

65-
private CachingComparer<TElement> GetComparer() => GetComparer(null);
66-
67-
internal abstract CachingComparer<TElement> GetComparer(CachingComparer<TElement>? childComparer);
65+
internal abstract CachingComparer<TElement> GetComparer(CachingComparer<TElement>? childComparer = null);
6866

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

@@ -159,6 +157,57 @@ internal override CachingComparer<TElement> GetComparer(CachingComparer<TElement
159157
}
160158
}
161159

160+
/// <summary>An ordered enumerable used by Order/OrderDescending for Ts that are bitwise indistinguishable for any considered equal.</summary>
161+
internal sealed partial class OrderedImplicitlyStableEnumerable<TElement> : OrderedEnumerable<TElement>
162+
{
163+
private readonly bool _descending;
164+
165+
public OrderedImplicitlyStableEnumerable(IEnumerable<TElement> source, bool descending) : base(source)
166+
{
167+
Debug.Assert(Enumerable.TypeIsImplicitlyStable<TElement>());
168+
169+
if (source is null)
170+
{
171+
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
172+
}
173+
174+
_descending = descending;
175+
}
176+
177+
internal override CachingComparer<TElement> GetComparer(CachingComparer<TElement>? childComparer) =>
178+
childComparer == null ?
179+
new CachingComparer<TElement, TElement>(EnumerableSorter<TElement>.IdentityFunc, Comparer<TElement>.Default, _descending) :
180+
new CachingComparerWithChild<TElement, TElement>(EnumerableSorter<TElement>.IdentityFunc, Comparer<TElement>.Default, _descending, childComparer);
181+
182+
internal override EnumerableSorter<TElement> GetEnumerableSorter(EnumerableSorter<TElement>? next) =>
183+
new EnumerableSorter<TElement, TElement>(EnumerableSorter<TElement>.IdentityFunc, Comparer<TElement>.Default, _descending, next);
184+
185+
public override IEnumerator<TElement> GetEnumerator()
186+
{
187+
var buffer = new Buffer<TElement>(_source);
188+
if (buffer._count > 0)
189+
{
190+
Sort(buffer._items.AsSpan(0, buffer._count), _descending);
191+
for (int i = 0; i < buffer._count; i++)
192+
{
193+
yield return buffer._items[i];
194+
}
195+
}
196+
}
197+
198+
private static void Sort(Span<TElement> span, bool descending)
199+
{
200+
if (descending)
201+
{
202+
span.Sort(static (a, b) => Comparer<TElement>.Default.Compare(b, a));
203+
}
204+
else
205+
{
206+
span.Sort();
207+
}
208+
}
209+
}
210+
162211
// A comparer that chains comparisons, and pushes through the last element found to be
163212
// lower or higher (depending on use), so as to represent the sort of comparisons
164213
// done by OrderBy().ThenBy() combinations.

src/libraries/System.Linq/tests/OrderDescendingTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,48 @@ public void SourceReverseOfResultNullPassedAsComparer()
103103
Assert.Equal(expected, source.OrderDescending(null));
104104
}
105105

106+
[Fact]
107+
public void OrderedDescendingToArray()
108+
{
109+
var source = new[]
110+
{
111+
5, 9, 6, 7, 8, 5, 20
112+
};
113+
var expected = new[]
114+
{
115+
20, 9, 8, 7, 6, 5, 5
116+
};
117+
118+
Assert.Equal(expected, source.OrderDescending().ToArray());
119+
}
120+
121+
[Fact]
122+
public void EmptyOrderedDescendingToArray()
123+
{
124+
Assert.Empty(Enumerable.Empty<int>().OrderDescending().ToArray());
125+
}
126+
127+
[Fact]
128+
public void OrderedDescendingToList()
129+
{
130+
var source = new[]
131+
{
132+
5, 9, 6, 7, 8, 5, 20
133+
};
134+
var expected = new[]
135+
{
136+
20, 9, 8, 7, 6, 5, 5
137+
};
138+
139+
Assert.Equal(expected, source.OrderDescending().ToList());
140+
}
141+
142+
[Fact]
143+
public void EmptyOrderedDescendingToList()
144+
{
145+
Assert.Empty(Enumerable.Empty<int>().OrderDescending().ToList());
146+
}
147+
106148
[Fact]
107149
public void SameKeysVerifySortStable()
108150
{

src/libraries/System.Linq/tests/OrderTests.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,5 +485,13 @@ public void CultureOrderElementAt()
485485
}
486486
}
487487
}
488+
489+
[Fact]
490+
public void StableSort_CustomComparerAlwaysReturns0()
491+
{
492+
byte[] values = new byte[] { 0x45, 0x7D, 0x4B, 0x61, 0x27 };
493+
byte[] newValues = values.Order(Comparer<byte>.Create((a, b) => 0)).ToArray();
494+
AssertExtensions.SequenceEqual(values, newValues);
495+
}
488496
}
489497
}

0 commit comments

Comments
 (0)