From 10525af145946553d4e3b35d44de05ca5c0c89b0 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 6 Oct 2022 22:52:39 -0400 Subject: [PATCH 1/2] 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. --- .../System.Linq/src/System/Linq/OrderBy.cs | 24 +++++-- .../System/Linq/OrderedEnumerable.SpeedOpt.cs | 23 ++++++- .../src/System/Linq/OrderedEnumerable.cs | 64 +++++++++++++++++-- .../System.Linq/tests/OrderDescendingTests.cs | 42 ++++++++++++ 4 files changed, 142 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/OrderBy.cs b/src/libraries/System.Linq/src/System/Linq/OrderBy.cs index 62bac18eba58e7..317bc0c394d6eb 100644 --- a/src/libraries/System.Linq/src/System/Linq/OrderBy.cs +++ b/src/libraries/System.Linq/src/System/Linq/OrderBy.cs @@ -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 { @@ -23,7 +24,7 @@ public static partial class Enumerable /// This method compares elements by using the default comparer . /// public static IOrderedEnumerable Order(this IEnumerable source) => - OrderBy(source, EnumerableSorter.IdentityFunc); + Order(source, comparer: null); /// /// Sorts the elements of a sequence in ascending order. @@ -42,7 +43,9 @@ public static IOrderedEnumerable Order(this IEnumerable source) => /// If comparer is , the default comparer is used to compare elements. /// public static IOrderedEnumerable Order(this IEnumerable source, IComparer? comparer) => - OrderBy(source, EnumerableSorter.IdentityFunc, comparer); + TypeIsImplicitlyStable() ? + new OrderedImplicitlyStableEnumerable(source, comparer, descending: false) : + OrderBy(source, EnumerableSorter.IdentityFunc, comparer); public static IOrderedEnumerable OrderBy(this IEnumerable source, Func keySelector) => new OrderedEnumerable(source, keySelector, null, false, null); @@ -66,7 +69,7 @@ public static IOrderedEnumerable OrderBy(this IEnumerabl /// This method compares elements by using the default comparer . /// public static IOrderedEnumerable OrderDescending(this IEnumerable source) => - OrderByDescending(source, EnumerableSorter.IdentityFunc); + OrderDescending(source, comparer: null); /// /// Sorts the elements of a sequence in descending order. @@ -85,7 +88,9 @@ public static IOrderedEnumerable OrderDescending(this IEnumerable sourc /// If comparer is , the default comparer is used to compare elements. /// public static IOrderedEnumerable OrderDescending(this IEnumerable source, IComparer? comparer) => - OrderByDescending(source, EnumerableSorter.IdentityFunc, comparer); + TypeIsImplicitlyStable()? + new OrderedImplicitlyStableEnumerable(source, comparer, descending: true) : + OrderByDescending(source, EnumerableSorter.IdentityFunc, comparer); public static IOrderedEnumerable OrderByDescending(this IEnumerable source, Func keySelector) => new OrderedEnumerable(source, keySelector, null, true, null); @@ -132,6 +137,17 @@ public static IOrderedEnumerable ThenByDescending(this I return source.CreateOrderedEnumerable(keySelector, comparer, true); } + + /// Gets whether the results of an unstable sort will be observably the same as a stable sort. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TypeIsImplicitlyStable() => + 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 : IEnumerable diff --git a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs index b90dbf28a9537b..3eeb61201dc158 100644 --- a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs @@ -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 : IPartition { - public TElement[] ToArray() + public virtual TElement[] ToArray() { Buffer buffer = new Buffer(_source); @@ -29,7 +29,7 @@ public TElement[] ToArray() return array; } - public List ToList() + public virtual List ToList() { Buffer buffer = new Buffer(_source); int count = buffer._count; @@ -247,4 +247,21 @@ private TElement Last(Buffer buffer) return value; } } + + internal sealed partial class OrderedImplicitlyStableEnumerable : OrderedEnumerable + { + public override TElement[] ToArray() + { + TElement[] array = _source.ToArray(); + Sort(array, _comparer, _descending); + return array; + } + + public override List ToList() + { + List list = _source.ToList(); + Sort(CollectionsMarshal.AsSpan(list), _comparer, _descending); + return list; + } + } } diff --git a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs index 36f6b992712c33..1811f7eab37433 100644 --- a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs +++ b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs @@ -18,7 +18,7 @@ internal abstract partial class OrderedEnumerable : IOrderedEnumerable private int[] SortedMap(Buffer buffer, int minIdx, int maxIdx) => GetEnumerableSorter().Sort(buffer._items, buffer._count, minIdx, maxIdx); - public IEnumerator GetEnumerator() + public virtual IEnumerator GetEnumerator() { Buffer buffer = new Buffer(_source); if (buffer._count > 0) @@ -62,9 +62,7 @@ internal IEnumerator GetEnumerator(int minIdx, int maxIdx) internal abstract EnumerableSorter GetEnumerableSorter(EnumerableSorter? next); - private CachingComparer GetComparer() => GetComparer(null); - - internal abstract CachingComparer GetComparer(CachingComparer? childComparer); + internal abstract CachingComparer GetComparer(CachingComparer? childComparer = null); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -159,6 +157,64 @@ internal override CachingComparer GetComparer(CachingComparerAn ordered enumerable used by Order/OrderDescending for Ts that are bitwise indistinguishable for any considered equal. + internal sealed partial class OrderedImplicitlyStableEnumerable : OrderedEnumerable + { + private readonly IComparer _comparer; + private readonly bool _descending; + + public OrderedImplicitlyStableEnumerable(IEnumerable source, IComparer? comparer, bool descending) : base(source) + { + Debug.Assert(Enumerable.TypeIsImplicitlyStable()); + + if (source is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + } + + _comparer = comparer ?? Comparer.Default; + _descending = descending; + } + + internal override CachingComparer GetComparer(CachingComparer? childComparer) => + childComparer == null ? + new CachingComparer(EnumerableSorter.IdentityFunc, _comparer, _descending) : + new CachingComparerWithChild(EnumerableSorter.IdentityFunc, _comparer, _descending, childComparer); + + internal override EnumerableSorter GetEnumerableSorter(EnumerableSorter? next) => + new EnumerableSorter(EnumerableSorter.IdentityFunc, _comparer, _descending, next); + + public override IEnumerator GetEnumerator() + { + var buffer = new Buffer(_source); + if (buffer._count > 0) + { + Sort(buffer._items.AsSpan(0, buffer._count), _comparer, _descending); + for (int i = 0; i < buffer._count; i++) + { + yield return buffer._items[i]; + } + } + } + + private static void Sort(Span span, IComparer comparer, bool descending) + { + if (!descending) + { + span.Sort(comparer); + } + else if (comparer == Comparer.Default) + { + span.Sort(static (a, b) => Comparer.Default.Compare(b, a)); + } + else + { + IComparer capturedComparer = comparer; + span.Sort((a, b) => capturedComparer.Compare(b, a)); + } + } + } + // 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. diff --git a/src/libraries/System.Linq/tests/OrderDescendingTests.cs b/src/libraries/System.Linq/tests/OrderDescendingTests.cs index 2e77f6a9da189a..47fd110aab1363 100644 --- a/src/libraries/System.Linq/tests/OrderDescendingTests.cs +++ b/src/libraries/System.Linq/tests/OrderDescendingTests.cs @@ -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().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().OrderDescending().ToList()); + } + [Fact] public void SameKeysVerifySortStable() { From c26b57bb4254bfd29bfa21830c8734d34bc35d06 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sun, 9 Oct 2022 14:33:43 -0400 Subject: [PATCH 2/2] Don't try to optimize custom comparers --- .../System.Linq/src/System/Linq/OrderBy.cs | 8 +++---- .../System/Linq/OrderedEnumerable.SpeedOpt.cs | 4 ++-- .../src/System/Linq/OrderedEnumerable.cs | 23 +++++++------------ src/libraries/System.Linq/tests/OrderTests.cs | 8 +++++++ 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/OrderBy.cs b/src/libraries/System.Linq/src/System/Linq/OrderBy.cs index 317bc0c394d6eb..aa7a08ee81c9f6 100644 --- a/src/libraries/System.Linq/src/System/Linq/OrderBy.cs +++ b/src/libraries/System.Linq/src/System/Linq/OrderBy.cs @@ -43,8 +43,8 @@ public static IOrderedEnumerable Order(this IEnumerable source) => /// If comparer is , the default comparer is used to compare elements. /// public static IOrderedEnumerable Order(this IEnumerable source, IComparer? comparer) => - TypeIsImplicitlyStable() ? - new OrderedImplicitlyStableEnumerable(source, comparer, descending: false) : + TypeIsImplicitlyStable() && (comparer is null || comparer == Comparer.Default) ? + new OrderedImplicitlyStableEnumerable(source, descending: false) : OrderBy(source, EnumerableSorter.IdentityFunc, comparer); public static IOrderedEnumerable OrderBy(this IEnumerable source, Func keySelector) @@ -88,8 +88,8 @@ public static IOrderedEnumerable OrderDescending(this IEnumerable sourc /// If comparer is , the default comparer is used to compare elements. /// public static IOrderedEnumerable OrderDescending(this IEnumerable source, IComparer? comparer) => - TypeIsImplicitlyStable()? - new OrderedImplicitlyStableEnumerable(source, comparer, descending: true) : + TypeIsImplicitlyStable() && (comparer is null || comparer == Comparer.Default) ? + new OrderedImplicitlyStableEnumerable(source, descending: true) : OrderByDescending(source, EnumerableSorter.IdentityFunc, comparer); public static IOrderedEnumerable OrderByDescending(this IEnumerable source, Func keySelector) => diff --git a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs index 3eeb61201dc158..6ea4b069a1e9ae 100644 --- a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs @@ -253,14 +253,14 @@ internal sealed partial class OrderedImplicitlyStableEnumerable : Orde public override TElement[] ToArray() { TElement[] array = _source.ToArray(); - Sort(array, _comparer, _descending); + Sort(array, _descending); return array; } public override List ToList() { List list = _source.ToList(); - Sort(CollectionsMarshal.AsSpan(list), _comparer, _descending); + Sort(CollectionsMarshal.AsSpan(list), _descending); return list; } } diff --git a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs index 1811f7eab37433..1bcdb6b2b780d1 100644 --- a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs +++ b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs @@ -160,10 +160,9 @@ internal override CachingComparer GetComparer(CachingComparerAn ordered enumerable used by Order/OrderDescending for Ts that are bitwise indistinguishable for any considered equal. internal sealed partial class OrderedImplicitlyStableEnumerable : OrderedEnumerable { - private readonly IComparer _comparer; private readonly bool _descending; - public OrderedImplicitlyStableEnumerable(IEnumerable source, IComparer? comparer, bool descending) : base(source) + public OrderedImplicitlyStableEnumerable(IEnumerable source, bool descending) : base(source) { Debug.Assert(Enumerable.TypeIsImplicitlyStable()); @@ -172,24 +171,23 @@ public OrderedImplicitlyStableEnumerable(IEnumerable source, IComparer ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); } - _comparer = comparer ?? Comparer.Default; _descending = descending; } internal override CachingComparer GetComparer(CachingComparer? childComparer) => childComparer == null ? - new CachingComparer(EnumerableSorter.IdentityFunc, _comparer, _descending) : - new CachingComparerWithChild(EnumerableSorter.IdentityFunc, _comparer, _descending, childComparer); + new CachingComparer(EnumerableSorter.IdentityFunc, Comparer.Default, _descending) : + new CachingComparerWithChild(EnumerableSorter.IdentityFunc, Comparer.Default, _descending, childComparer); internal override EnumerableSorter GetEnumerableSorter(EnumerableSorter? next) => - new EnumerableSorter(EnumerableSorter.IdentityFunc, _comparer, _descending, next); + new EnumerableSorter(EnumerableSorter.IdentityFunc, Comparer.Default, _descending, next); public override IEnumerator GetEnumerator() { var buffer = new Buffer(_source); if (buffer._count > 0) { - Sort(buffer._items.AsSpan(0, buffer._count), _comparer, _descending); + Sort(buffer._items.AsSpan(0, buffer._count), _descending); for (int i = 0; i < buffer._count; i++) { yield return buffer._items[i]; @@ -197,20 +195,15 @@ public override IEnumerator GetEnumerator() } } - private static void Sort(Span span, IComparer comparer, bool descending) + private static void Sort(Span span, bool descending) { - if (!descending) - { - span.Sort(comparer); - } - else if (comparer == Comparer.Default) + if (descending) { span.Sort(static (a, b) => Comparer.Default.Compare(b, a)); } else { - IComparer capturedComparer = comparer; - span.Sort((a, b) => capturedComparer.Compare(b, a)); + span.Sort(); } } } diff --git a/src/libraries/System.Linq/tests/OrderTests.cs b/src/libraries/System.Linq/tests/OrderTests.cs index 6258f06c8b67e8..3560959b6a8ea4 100644 --- a/src/libraries/System.Linq/tests/OrderTests.cs +++ b/src/libraries/System.Linq/tests/OrderTests.cs @@ -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.Create((a, b) => 0)).ToArray(); + AssertExtensions.SequenceEqual(values, newValues); + } } }