diff --git a/src/System.Memory/ref/System.Memory.cs b/src/System.Memory/ref/System.Memory.cs index 645b2b90fcb1..1185757ed990 100644 --- a/src/System.Memory/ref/System.Memory.cs +++ b/src/System.Memory/ref/System.Memory.cs @@ -165,6 +165,13 @@ public static class MemoryExtensions public static int BinarySearch(this Span span, IComparable comparable) { throw null; } public static int BinarySearch(this Span span, TComparable comparable) where TComparable : IComparable { throw null; } public static int BinarySearch(this Span span, T value, TComparer comparer) where TComparer : IComparer { throw null; } + + public static void Sort(this Span span) { throw null; } + public static void Sort(this Span span, TComparer comparer) where TComparer : IComparer { throw null; } + public static void Sort(this Span span, Comparison comparison) { throw null; } + public static void Sort(this Span keys, Span items) { throw null; } + public static void Sort(this Span keys, Span items, TComparer comparer) where TComparer : IComparer { throw null; } + public static void Sort(this Span keys, Span items, Comparison comparison) { throw null; } } public readonly struct ReadOnlyMemory diff --git a/src/System.Memory/src/System.Memory.csproj b/src/System.Memory/src/System.Memory.csproj index d242e09aec09..67e0161e56d5 100644 --- a/src/System.Memory/src/System.Memory.csproj +++ b/src/System.Memory/src/System.Memory.csproj @@ -25,6 +25,19 @@ + + + + + + + + + + + + + diff --git a/src/System.Memory/src/System/MemoryExtensions.cs b/src/System.Memory/src/System/MemoryExtensions.cs index e2d6128feaf7..c52406991313 100644 --- a/src/System.Memory/src/System/MemoryExtensions.cs +++ b/src/System.Memory/src/System/MemoryExtensions.cs @@ -922,5 +922,82 @@ public static int BinarySearch( value, comparer); return BinarySearch(span, comparable); } + + + /// + /// Sorts the elements in the entire + /// using the implementation of each + /// element of the + /// + /// The to sort. + /// + /// One or more elements do not implement the interface. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Sort(this Span span) => SpanSortHelpersKeys.Sort(span); + + /// + /// Sorts the elements in the entire + /// using the . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Sort(this Span span, TComparer comparer) + where TComparer : IComparer => + SpanSortHelpersKeys.Sort(span, comparer); + + /// + /// Sorts the elements in the entire + /// using the . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Sort(this Span span, Comparison comparison) + { + if (comparison == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.comparison); + + SpanSortHelpersKeys.Sort(span, comparison); + } + + /// + /// Sorts a pair of spans + /// (one contains the keys + /// and the other contains the corresponding items ) + /// based on the keys in the first + /// using the implementation of each + /// element of the . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Sort(this Span keys, Span items) => + SpanSortHelpersKeysValues.Sort(keys, items); + + /// + /// Sorts a pair of spans + /// (one contains the keys + /// and the other contains the corresponding items ) + /// based on the keys in the first + /// using the . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Sort(this Span keys, + Span items, TComparer comparer) + where TComparer : IComparer => + SpanSortHelpersKeysValues.Sort(keys, items, comparer); + + /// + /// Sorts a pair of spans + /// (one contains the keys + /// and the other contains the corresponding items ) + /// based on the keys in the first + /// using the . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Sort(this Span keys, + Span items, Comparison comparison) + { + if (comparison == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.comparison); + + SpanSortHelpersKeysValues.Sort(keys, items, comparison); + } } } diff --git a/src/System.Memory/src/System/SpanSortHelpers.Common.cs b/src/System.Memory/src/System/SpanSortHelpers.Common.cs new file mode 100644 index 000000000000..21518958d50c --- /dev/null +++ b/src/System.Memory/src/System/SpanSortHelpers.Common.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +#if !netstandard +using Internal.Runtime.CompilerServices; +#endif + +namespace System +{ + internal static partial class SpanSortHelpersCommon + { + // This is the threshold where Introspective sort switches to Insertion sort. + // Empirically, 16 seems to speed up most cases without slowing down others, at least for integers. + // Large value types may benefit from a smaller number. + internal const int IntrosortSizeThreshold = 16; + + internal static int FloorLog2PlusOne(int n) + { + Debug.Assert(n >= 2); + int result = 2; + n >>= 2; + while (n > 0) + { + ++result; + n >>= 1; + } + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Swap(ref T items, int i, int j) + { + Debug.Assert(i != j); + Swap(ref Unsafe.Add(ref items, i), ref Unsafe.Add(ref items, j)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Swap(ref T a, ref T b) + { + T temp = a; + a = b; + b = temp; + } + + // This started out with just LessThan. + // However, due to bogus comparers, comparables etc. + // we need to preserve semantics completely to get same result. + internal interface IDirectComparer + { + bool GreaterThan(T x, T y); + bool LessThan(T x, T y); + } + + // + // Type specific DirectComparer(s) to ensure optimal code-gen + // + internal struct SByteDirectComparer : IDirectComparer + { + public bool GreaterThan(sbyte x, sbyte y) => x > y; + public bool LessThan(sbyte x, sbyte y) => x < y; + } + internal struct ByteDirectComparer : IDirectComparer + { + public bool GreaterThan(byte x, byte y) => x > y; + public bool LessThan(byte x, byte y) => x < y; + } + internal struct Int16DirectComparer : IDirectComparer + { + public bool GreaterThan(short x, short y) => x > y; + public bool LessThan(short x, short y) => x < y; + } + internal struct UInt16DirectComparer : IDirectComparer + { + public bool GreaterThan(ushort x, ushort y) => x > y; + public bool LessThan(ushort x, ushort y) => x < y; + } + internal struct Int32DirectComparer : IDirectComparer + { + public bool GreaterThan(int x, int y) => x > y; + public bool LessThan(int x, int y) => x < y; + } + internal struct UInt32DirectComparer : IDirectComparer + { + public bool GreaterThan(uint x, uint y) => x > y; + public bool LessThan(uint x, uint y) => x < y; + } + internal struct Int64DirectComparer : IDirectComparer + { + public bool GreaterThan(long x, long y) => x > y; + public bool LessThan(long x, long y) => x < y; + } + internal struct UInt64DirectComparer : IDirectComparer + { + public bool GreaterThan(ulong x, ulong y) => x > y; + public bool LessThan(ulong x, ulong y) => x < y; + } + internal struct SingleDirectComparer : IDirectComparer + { + public bool GreaterThan(float x, float y) => x > y; + public bool LessThan(float x, float y) => x < y; + } + internal struct DoubleDirectComparer : IDirectComparer + { + public bool GreaterThan(double x, double y) => x > y; + public bool LessThan(double x, double y) => x < y; + } + + internal interface IIsNaN + { + bool IsNaN(T value); + } + internal struct SingleIsNaN : IIsNaN + { + public bool IsNaN(float value) => float.IsNaN(value); + } + internal struct DoubleIsNaN : IIsNaN + { + public bool IsNaN(double value) => double.IsNaN(value); + } + } +} diff --git a/src/System.Memory/src/System/SpanSortHelpers.Keys.Comparison.cs b/src/System.Memory/src/System/SpanSortHelpers.Keys.Comparison.cs new file mode 100644 index 000000000000..05873c804b85 --- /dev/null +++ b/src/System.Memory/src/System/SpanSortHelpers.Keys.Comparison.cs @@ -0,0 +1,271 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +#if !netstandard +using Internal.Runtime.CompilerServices; +#endif + +using static System.SpanSortHelpersCommon; + +namespace System +{ + internal static partial class SpanSortHelpersKeys + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Sort( + ref TKey keys, int length, + Comparison comparison) + { + IntrospectiveSort(ref keys, length, comparison); + } + + private static void IntrospectiveSort( + ref TKey keys, int length, + Comparison comparison) + { + int depthLimit = 2 * FloorLog2PlusOne(length); + IntroSort(ref keys, 0, length - 1, depthLimit, comparison); + } + + private static void IntroSort( + ref TKey keys, + int lo, int hi, int depthLimit, + Comparison comparison) + { + Debug.Assert(comparison != null); + Debug.Assert(lo >= 0); + + while (hi > lo) + { + int partitionSize = hi - lo + 1; + if (partitionSize <= IntrosortSizeThreshold) + { + if (partitionSize == 1) + { + return; + } + if (partitionSize == 2) + { + Sort2(ref keys, lo, hi, comparison); + return; + } + if (partitionSize == 3) + { + ref TKey loRef = ref Unsafe.Add(ref keys, lo); + ref TKey miRef = ref Unsafe.Add(ref keys, hi - 1); + ref TKey hiRef = ref Unsafe.Add(ref keys, hi); + Sort3(ref loRef, ref miRef, ref hiRef, comparison); + return; + } + + InsertionSort(ref keys, lo, hi, comparison); + return; + } + + if (depthLimit == 0) + { + HeapSort(ref keys, lo, hi, comparison); + return; + } + depthLimit--; + + // We should never reach here, unless > 3 elements due to partition size + Debug.Assert(partitionSize > 3); + + int p = PickPivotAndPartition(ref keys, lo, hi, comparison); + // Note we've already partitioned around the pivot and do not have to move the pivot again. + IntroSort(ref keys, p + 1, hi, depthLimit, comparison); + hi = p - 1; + } + } + + private static int PickPivotAndPartition( + ref TKey keys, int lo, int hi, + Comparison comparison) + { + Debug.Assert(comparison != null); + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + // Compute median-of-three. But also partition them, since we've done the comparison. + + // PERF: `lo` or `hi` will never be negative inside the loop, + // so computing median using uints is safe since we know + // `length <= int.MaxValue`, and indices are >= 0 + // and thus cannot overflow an uint. + // Saves one subtraction per loop compared to + // `int middle = lo + ((hi - lo) >> 1);` + int middle = (int)(((uint)hi + (uint)lo) >> 1); + + // Sort lo, mid and hi appropriately, then pick mid as the pivot. + ref TKey keysAtLo = ref Unsafe.Add(ref keys, lo); + ref TKey keysAtMiddle = ref Unsafe.Add(ref keys, middle); + ref TKey keysAtHi = ref Unsafe.Add(ref keys, hi); + Sort3(ref keysAtLo, ref keysAtMiddle, ref keysAtHi, comparison); + + TKey pivot = keysAtMiddle; + + int left = lo; + int right = hi - 1; + // We already partitioned lo and hi and put the pivot in hi - 1. + // And we pre-increment & decrement below. + Swap(ref keysAtMiddle, ref Unsafe.Add(ref keys, right)); + + while (left < right) + { + while (left < (hi - 1) && comparison(Unsafe.Add(ref keys, ++left), pivot) < 0) ; + // Check if bad comparable/comparison + if (left == (hi - 1) && comparison(Unsafe.Add(ref keys, left), pivot) < 0) + ThrowHelper.ThrowArgumentException_BadComparer(comparison); + + while (right > lo && comparison(pivot, Unsafe.Add(ref keys, --right)) < 0) ; + // Check if bad comparable/comparison + if (right == lo && comparison(pivot, Unsafe.Add(ref keys, right)) < 0) + ThrowHelper.ThrowArgumentException_BadComparer(comparison); + + if (left >= right) + break; + + Swap(ref keys, left, right); + } + // Put pivot in the right location. + right = hi - 1; + if (left != right) + { + Swap(ref keys, left, right); + } + return left; + } + + private static void HeapSort( + ref TKey keys, int lo, int hi, + Comparison comparison) + + { + Debug.Assert(comparison != null); + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + int n = hi - lo + 1; + for (int i = n / 2; i >= 1; --i) + { + DownHeap(ref keys, i, n, lo, comparison); + } + for (int i = n; i > 1; --i) + { + Swap(ref keys, lo, lo + i - 1); + DownHeap(ref keys, 1, i - 1, lo, comparison); + } + } + + private static void DownHeap( + ref TKey keys, int i, int n, int lo, + Comparison comparison) + + { + Debug.Assert(comparison != null); + Debug.Assert(lo >= 0); + + // Below lines are equivalent to: TKey d = keys[lo + i - 1]; + ref TKey keysAtLo = ref Unsafe.Add(ref keys, lo); + ref TKey keysAtLoMinus1 = ref Unsafe.Add(ref keysAtLo, -1); // TODO: Use Subtract when available + TKey d = Unsafe.Add(ref keysAtLoMinus1, i); + int nHalf = n / 2; + while (i <= nHalf) + { + int child = i << 1; + + //if (child < n && comparison(keys[lo + child - 1], keys[lo + child]) < 0) + if (child < n && + comparison(Unsafe.Add(ref keysAtLoMinus1, child), Unsafe.Add(ref keysAtLo, child)) < 0) + { + ++child; + } + + //if (!(comparison(d, keys[lo + child - 1]) < 0)) + if (!(comparison(d, Unsafe.Add(ref keysAtLoMinus1, child)) < 0)) + break; + + // Below lines are equivalent to: keys[lo + i - 1] = keys[lo + child - 1] + Unsafe.Add(ref keysAtLoMinus1, i) = Unsafe.Add(ref keysAtLoMinus1, child); + + i = child; + } + // Below lines are equivalent to: keys[lo + i - 1] = d; + Unsafe.Add(ref keysAtLoMinus1, i) = d; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InsertionSort( + ref TKey keys, int lo, int hi, + Comparison comparison) + + { + Debug.Assert(lo >= 0); + Debug.Assert(hi >= lo); + + for (int i = lo; i < hi; ++i) + { + int j = i; + + TKey t = Unsafe.Add(ref keys, j + 1); + + if (j >= lo && comparison(t, Unsafe.Add(ref keys, j)) < 0) + { + do + { + Unsafe.Add(ref keys, j + 1) = Unsafe.Add(ref keys, j); + --j; + } + while (j >= lo && comparison(t, Unsafe.Add(ref keys, j)) < 0); + + Unsafe.Add(ref keys, j + 1) = t; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort3( + ref TKey r0, ref TKey r1, ref TKey r2, + Comparison comparison) + + { + Sort2(ref r0, ref r1, comparison); + Sort2(ref r0, ref r2, comparison); + Sort2(ref r1, ref r2, comparison); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort2( + ref TKey keys, int i, int j, + Comparison comparison) + + { + Debug.Assert(i != j); + + ref TKey a = ref Unsafe.Add(ref keys, i); + ref TKey b = ref Unsafe.Add(ref keys, j); + Sort2(ref a, ref b, comparison); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort2( + ref TKey a, ref TKey b, Comparison comparison) + + { + // This is one of the only places GreaterThan is needed + // but we need to preserve this due to bogus comparers or similar + if (comparison(a, b) > 0) + { + TKey temp = a; + a = b; + b = temp; + } + } + } +} diff --git a/src/System.Memory/src/System/SpanSortHelpers.Keys.IComparable.cs b/src/System.Memory/src/System/SpanSortHelpers.Keys.IComparable.cs new file mode 100644 index 000000000000..54932f1db393 --- /dev/null +++ b/src/System.Memory/src/System/SpanSortHelpers.Keys.IComparable.cs @@ -0,0 +1,270 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +#if !netstandard +using Internal.Runtime.CompilerServices; +#endif + +using static System.SpanSortHelpersCommon; + +namespace System +{ + internal static partial class SpanSortHelpersKeys + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Sort( + ref TKey keys, int length) + where TKey : IComparable + { + IntrospectiveSort(ref keys, length); + } + + private static void IntrospectiveSort( + ref TKey keys, int length) + where TKey : IComparable + { + int depthLimit = 2 * FloorLog2PlusOne(length); + IntroSort(ref keys, 0, length - 1, depthLimit); + } + + private static void IntroSort( + ref TKey keys, + int lo, int hi, int depthLimit) + where TKey : IComparable + { + Debug.Assert(lo >= 0); + + while (hi > lo) + { + int partitionSize = hi - lo + 1; + if (partitionSize <= IntrosortSizeThreshold) + { + if (partitionSize == 1) + { + return; + } + if (partitionSize == 2) + { + Sort2(ref keys, lo, hi); + return; + } + if (partitionSize == 3) + { + ref TKey loRef = ref Unsafe.Add(ref keys, lo); + ref TKey miRef = ref Unsafe.Add(ref keys, hi - 1); + ref TKey hiRef = ref Unsafe.Add(ref keys, hi); + Sort3(ref loRef, ref miRef, ref hiRef); + return; + } + + InsertionSort(ref keys, lo, hi); + return; + } + + if (depthLimit == 0) + { + HeapSort(ref keys, lo, hi); + return; + } + depthLimit--; + + // We should never reach here, unless > 3 elements due to partition size + Debug.Assert(partitionSize > 3); + + int p = PickPivotAndPartition(ref keys, lo, hi); + // Note we've already partitioned around the pivot and do not have to move the pivot again. + IntroSort(ref keys, p + 1, hi, depthLimit); + hi = p - 1; + } + } + + private static int PickPivotAndPartition( + ref TKey keys, int lo, int hi) + where TKey : IComparable + { + + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + // Compute median-of-three. But also partition them, since we've done the comparison. + + // PERF: `lo` or `hi` will never be negative inside the loop, + // so computing median using uints is safe since we know + // `length <= int.MaxValue`, and indices are >= 0 + // and thus cannot overflow an uint. + // Saves one subtraction per loop compared to + // `int middle = lo + ((hi - lo) >> 1);` + int middle = (int)(((uint)hi + (uint)lo) >> 1); + + // Sort lo, mid and hi appropriately, then pick mid as the pivot. + ref TKey keysAtLo = ref Unsafe.Add(ref keys, lo); + ref TKey keysAtMiddle = ref Unsafe.Add(ref keys, middle); + ref TKey keysAtHi = ref Unsafe.Add(ref keys, hi); + Sort3(ref keysAtLo, ref keysAtMiddle, ref keysAtHi); + + TKey pivot = keysAtMiddle; + + int left = lo; + int right = hi - 1; + // We already partitioned lo and hi and put the pivot in hi - 1. + // And we pre-increment & decrement below. + Swap(ref keysAtMiddle, ref Unsafe.Add(ref keys, right)); + + while (left < right) + { + if (pivot == null) + { + while (left < (hi - 1) && Unsafe.Add(ref keys, ++left) == null) ; + while (right > lo && Unsafe.Add(ref keys, --right) != null) ; + } + else + { + while (left < (hi - 1) && pivot.CompareTo(Unsafe.Add(ref keys, ++left)) > 0) ; + // Check if bad comparable/comparer + if (left == (hi - 1) && pivot.CompareTo(Unsafe.Add(ref keys, left)) > 0) + ThrowHelper.ThrowArgumentException_BadComparable(typeof(TKey)); + + while (right > lo && pivot.CompareTo(Unsafe.Add(ref keys, --right)) < 0) ; + // Check if bad comparable/comparer + if (right == lo && pivot.CompareTo(Unsafe.Add(ref keys, right)) < 0) + ThrowHelper.ThrowArgumentException_BadComparable(typeof(TKey)); + } + + if (left >= right) + break; + + Swap(ref keys, left, right); + } + // Put pivot in the right location. + right = hi - 1; + if (left != right) + { + Swap(ref keys, left, right); + } + return left; + } + + private static void HeapSort( + ref TKey keys, int lo, int hi) + where TKey : IComparable + { + + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + int n = hi - lo + 1; + for (int i = n / 2; i >= 1; --i) + { + DownHeap(ref keys, i, n, lo); + } + for (int i = n; i > 1; --i) + { + Swap(ref keys, lo, lo + i - 1); + DownHeap(ref keys, 1, i - 1, lo); + } + } + + private static void DownHeap( + ref TKey keys, int i, int n, int lo) + where TKey : IComparable + { + + Debug.Assert(lo >= 0); + + // Below lines are equivalent to: TKey d = keys[lo + i - 1]; + ref TKey keysAtLo = ref Unsafe.Add(ref keys, lo); + ref TKey keysAtLoMinus1 = ref Unsafe.Add(ref keysAtLo, -1); // TODO: Use Subtract when available + TKey d = Unsafe.Add(ref keysAtLoMinus1, i); + int nHalf = n / 2; + while (i <= nHalf) + { + int child = i << 1; + + //if (child < n && (keys[lo + child - 1] == null || keys[lo + child - 1].CompareTo(keys[lo + child]) < 0)) + if (child < n && + (Unsafe.Add(ref keysAtLoMinus1, child) == null || + Unsafe.Add(ref keysAtLoMinus1, child).CompareTo(Unsafe.Add(ref keysAtLo, child)) < 0)) + { + ++child; + } + + //if (keys[lo + child - 1] == null || keys[lo + child - 1].CompareTo(d) < 0) + if (Unsafe.Add(ref keysAtLoMinus1, child) == null || + Unsafe.Add(ref keysAtLoMinus1, child).CompareTo(d) < 0) + break; + + // Below lines are equivalent to: keys[lo + i - 1] = keys[lo + child - 1] + Unsafe.Add(ref keysAtLoMinus1, i) = Unsafe.Add(ref keysAtLoMinus1, child); + + i = child; + } + // Below lines are equivalent to: keys[lo + i - 1] = d; + Unsafe.Add(ref keysAtLoMinus1, i) = d; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InsertionSort( + ref TKey keys, int lo, int hi) + where TKey : IComparable + { + Debug.Assert(lo >= 0); + Debug.Assert(hi >= lo); + + for (int i = lo; i < hi; ++i) + { + int j = i; + + var t = Unsafe.Add(ref keys, j + 1); + + if (j >= lo && (t == null || t.CompareTo(Unsafe.Add(ref keys, j)) < 0)) + { + do + { + Unsafe.Add(ref keys, j + 1) = Unsafe.Add(ref keys, j); + --j; + } + while (j >= lo && (t == null || t.CompareTo(Unsafe.Add(ref keys, j)) < 0)); + + Unsafe.Add(ref keys, j + 1) = t; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort3( + ref TKey r0, ref TKey r1, ref TKey r2) + where TKey : IComparable + { + Sort2(ref r0, ref r1); + Sort2(ref r0, ref r2); + Sort2(ref r1, ref r2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort2( + ref TKey keys, int i, int j) + where TKey : IComparable + { + Debug.Assert(i != j); + + ref TKey a = ref Unsafe.Add(ref keys, i); + ref TKey b = ref Unsafe.Add(ref keys, j); + Sort2(ref a, ref b); + } + + private static void Sort2(ref TKey a, ref TKey b) + where TKey : IComparable + { + if (a != null && a.CompareTo(b) > 0) + { + TKey temp = a; + a = b; + b = temp; + } + } + } +} diff --git a/src/System.Memory/src/System/SpanSortHelpers.Keys.Specialized.cs b/src/System.Memory/src/System/SpanSortHelpers.Keys.Specialized.cs new file mode 100644 index 000000000000..03be11a7a31e --- /dev/null +++ b/src/System.Memory/src/System/SpanSortHelpers.Keys.Specialized.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +#if !netstandard +using Internal.Runtime.CompilerServices; +#endif + +using static System.SpanSortHelpersCommon; + +namespace System +{ + internal static partial class SpanSortHelpersKeys_DirectComparer + { + // https://github.com/dotnet/coreclr/blob/master/src/classlibnative/bcltype/arrayhelpers.cpp + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TrySortSpecialized( + ref TKey keys, int length) + { + // Type unfolding adopted from https://github.com/dotnet/coreclr/blob/master/src/classlibnative/bcltype/arrayhelpers.cpp#L268 + if (typeof(TKey) == typeof(sbyte)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, length, new SByteDirectComparer()); + return true; + } + else if (typeof(TKey) == typeof(byte) || + typeof(TKey) == typeof(bool)) // Use byte for bools to reduce code size + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, length, new ByteDirectComparer()); + return true; + } + else if (typeof(TKey) == typeof(short)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, length, new Int16DirectComparer()); + return true; + } + else if (typeof(TKey) == typeof(ushort) || + typeof(TKey) == typeof(char)) // Use ushort for chars to reduce code size) + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, length, new UInt16DirectComparer()); + return true; + } + else if (typeof(TKey) == typeof(int)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, length, new Int32DirectComparer()); + return true; + } + else if (typeof(TKey) == typeof(uint)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, length, new UInt32DirectComparer()); + return true; + } + else if (typeof(TKey) == typeof(long)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, length, new Int64DirectComparer()); + return true; + } + else if (typeof(TKey) == typeof(ulong)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, length, new UInt64DirectComparer()); + return true; + } + else if (typeof(TKey) == typeof(float)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + + // Comparison to NaN is always false, so do a linear pass + // and swap all NaNs to the front of the array + var left = NaNPrepass(ref specificKeys, length, new SingleIsNaN()); + + var remaining = length - left; + if (remaining > 1) + { + ref var afterNaNsKeys = ref Unsafe.Add(ref specificKeys, left); + Sort(ref afterNaNsKeys, remaining, new SingleDirectComparer()); + } + return true; + } + else if (typeof(TKey) == typeof(double)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + + // Comparison to NaN is always false, so do a linear pass + // and swap all NaNs to the front of the array + var left = NaNPrepass(ref specificKeys, length, new DoubleIsNaN()); + var remaining = length - left; + if (remaining > 1) + { + ref var afterNaNsKeys = ref Unsafe.Add(ref specificKeys, left); + Sort(ref afterNaNsKeys, remaining, new DoubleDirectComparer()); + } + return true; + } + // TODO: Specialize for string if necessary. What about the == null checks? + //else if (typeof(TKey) == typeof(string)) + //{ + // ref var specificKeys = ref Unsafe.As(ref keys); + // Sort(ref specificKeys, length, new StringDirectComparer()); + // return true; + //} + else + { + return false; + } + } + + // For sorting, move all NaN instances to front of the input array + private static int NaNPrepass( + ref TKey keys, int length, + TIsNaN isNaN) + where TIsNaN : struct, IIsNaN + { + int left = 0; + for (int i = 0; i < length; i++) + { + ref TKey current = ref Unsafe.Add(ref keys, i); + if (isNaN.IsNaN(current)) + { + // TODO: If first index is not NaN or we find just one not NaNs + // we could skip to version that no longer checks this + if (left != i) + { + ref TKey previous = ref Unsafe.Add(ref keys, left); + Swap(ref previous, ref current); + } + ++left; + } + } + return left; + } + } +} diff --git a/src/System.Memory/src/System/SpanSortHelpers.Keys.TComparer.cs b/src/System.Memory/src/System/SpanSortHelpers.Keys.TComparer.cs new file mode 100644 index 000000000000..6e68e00b51d7 --- /dev/null +++ b/src/System.Memory/src/System/SpanSortHelpers.Keys.TComparer.cs @@ -0,0 +1,275 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +#if !netstandard +using Internal.Runtime.CompilerServices; +#endif + +using static System.SpanSortHelpersCommon; + +namespace System +{ + internal static partial class SpanSortHelpersKeys_Comparer + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Sort( + ref TKey keys, int length, + TComparer comparer) + where TComparer : IComparer + { + IntrospectiveSort(ref keys, length, comparer); + } + + private static void IntrospectiveSort( + ref TKey keys, int length, + TComparer comparer) + where TComparer : IComparer + { + int depthLimit = 2 * FloorLog2PlusOne(length); + IntroSort(ref keys, 0, length - 1, depthLimit, comparer); + } + + private static void IntroSort( + ref TKey keys, + int lo, int hi, int depthLimit, + TComparer comparer) + where TComparer : IComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + + while (hi > lo) + { + int partitionSize = hi - lo + 1; + if (partitionSize <= IntrosortSizeThreshold) + { + if (partitionSize == 1) + { + return; + } + if (partitionSize == 2) + { + Sort2(ref keys, lo, hi, comparer); + return; + } + if (partitionSize == 3) + { + ref TKey loRef = ref Unsafe.Add(ref keys, lo); + ref TKey miRef = ref Unsafe.Add(ref keys, hi - 1); + ref TKey hiRef = ref Unsafe.Add(ref keys, hi); + Sort3(ref loRef, ref miRef, ref hiRef, comparer); + return; + } + + InsertionSort(ref keys, lo, hi, comparer); + return; + } + + if (depthLimit == 0) + { + HeapSort(ref keys, lo, hi, comparer); + return; + } + depthLimit--; + + // We should never reach here, unless > 3 elements due to partition size + Debug.Assert(partitionSize > 3); + + int p = PickPivotAndPartition(ref keys, lo, hi, comparer); + // Note we've already partitioned around the pivot and do not have to move the pivot again. + IntroSort(ref keys, p + 1, hi, depthLimit, comparer); + hi = p - 1; + } + } + + private static int PickPivotAndPartition( + ref TKey keys, int lo, int hi, + TComparer comparer) + where TComparer : IComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + // Compute median-of-three. But also partition them, since we've done the comparison. + + // PERF: `lo` or `hi` will never be negative inside the loop, + // so computing median using uints is safe since we know + // `length <= int.MaxValue`, and indices are >= 0 + // and thus cannot overflow an uint. + // Saves one subtraction per loop compared to + // `int middle = lo + ((hi - lo) >> 1);` + int middle = (int)(((uint)hi + (uint)lo) >> 1); + + // Sort lo, mid and hi appropriately, then pick mid as the pivot. + ref TKey keysAtLo = ref Unsafe.Add(ref keys, lo); + ref TKey keysAtMiddle = ref Unsafe.Add(ref keys, middle); + ref TKey keysAtHi = ref Unsafe.Add(ref keys, hi); + Sort3(ref keysAtLo, ref keysAtMiddle, ref keysAtHi, comparer); + + TKey pivot = keysAtMiddle; + + int left = lo; + int right = hi - 1; + // We already partitioned lo and hi and put the pivot in hi - 1. + // And we pre-increment & decrement below. + Swap(ref keysAtMiddle, ref Unsafe.Add(ref keys, right)); + + while (left < right) + { + while (left < (hi - 1) && comparer.Compare(Unsafe.Add(ref keys, ++left), pivot) < 0) ; + // Check if bad comparable/comparer + if (left == (hi - 1) && comparer.Compare(Unsafe.Add(ref keys, left), pivot) < 0) + ThrowHelper.ThrowArgumentException_BadComparer(comparer); + + while (right > lo && comparer.Compare(pivot, Unsafe.Add(ref keys, --right)) < 0) ; + // Check if bad comparable/comparer + if (right == lo && comparer.Compare(pivot, Unsafe.Add(ref keys, right)) < 0) + ThrowHelper.ThrowArgumentException_BadComparer(comparer); + + if (left >= right) + break; + + Swap(ref keys, left, right); + } + // Put pivot in the right location. + right = hi - 1; + if (left != right) + { + Swap(ref keys, left, right); + } + return left; + } + + private static void HeapSort( + ref TKey keys, int lo, int hi, + TComparer comparer) + where TComparer : IComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + int n = hi - lo + 1; + for (int i = n / 2; i >= 1; --i) + { + DownHeap(ref keys, i, n, lo, comparer); + } + for (int i = n; i > 1; --i) + { + Swap(ref keys, lo, lo + i - 1); + DownHeap(ref keys, 1, i - 1, lo, comparer); + } + } + + private static void DownHeap( + ref TKey keys, int i, int n, int lo, + TComparer comparer) + where TComparer : IComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + + // Below lines are equivalent to: TKey d = keys[lo + i - 1]; + ref TKey keysAtLo = ref Unsafe.Add(ref keys, lo); + ref TKey keysAtLoMinus1 = ref Unsafe.Add(ref keysAtLo, -1); // TODO: Use Subtract when available + TKey d = Unsafe.Add(ref keysAtLoMinus1, i); + int nHalf = n / 2; + while (i <= nHalf) + { + int child = i << 1; + + // Below lines are equivalent to: if (child < n && comparer(keys[lo + child - 1], keys[lo + child]) < 0) + if (child < n && + comparer.Compare(Unsafe.Add(ref keysAtLoMinus1, child), Unsafe.Add(ref keysAtLo, child)) < 0) + { + ++child; + } + + // Below lines are equivalent to: if (!(comparer(d, keys[lo + child - 1]) < 0)) + if (!(comparer.Compare(d, Unsafe.Add(ref keysAtLoMinus1, child)) < 0)) + break; + + // Below lines are equivalent to: keys[lo + i - 1] = keys[lo + child - 1] + Unsafe.Add(ref keysAtLoMinus1, i) = Unsafe.Add(ref keysAtLoMinus1, child); + + i = child; + } + // Below lines are equivalent to: keys[lo + i - 1] = d; + Unsafe.Add(ref keysAtLoMinus1, i) = d; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InsertionSort( + ref TKey keys, int lo, int hi, + TComparer comparer) + where TComparer : IComparer + { + Debug.Assert(lo >= 0); + Debug.Assert(hi >= lo); + + for (int i = lo; i < hi; ++i) + { + int j = i; + + var t = Unsafe.Add(ref keys, j + 1); + + if (j >= lo && comparer.Compare(t, Unsafe.Add(ref keys, j)) < 0) + { + do + { + Unsafe.Add(ref keys, j + 1) = Unsafe.Add(ref keys, j); + --j; + } + while (j >= lo && comparer.Compare(t, Unsafe.Add(ref keys, j)) < 0); + + Unsafe.Add(ref keys, j + 1) = t; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort3( + ref TKey r0, ref TKey r1, ref TKey r2, + TComparer comparer) + where TComparer : IComparer + { + Sort2(ref r0, ref r1, comparer); + Sort2(ref r0, ref r2, comparer); + Sort2(ref r1, ref r2, comparer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort2( + ref TKey keys, int i, int j, + TComparer comparer) + where TComparer : IComparer + { + Debug.Assert(i != j); + + ref TKey a = ref Unsafe.Add(ref keys, i); + ref TKey b = ref Unsafe.Add(ref keys, j); + Sort2(ref a, ref b, comparer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort2( + ref TKey a, ref TKey b, TComparer comparer) + where TComparer : IComparer + { + // This is one of the only places GreaterThan is needed + // but we need to preserve this due to bogus comparers or similar + if (comparer.Compare(a, b) > 0) + { + TKey temp = a; + a = b; + b = temp; + } + } + } +} diff --git a/src/System.Memory/src/System/SpanSortHelpers.Keys.TDirectComparer.cs b/src/System.Memory/src/System/SpanSortHelpers.Keys.TDirectComparer.cs new file mode 100644 index 000000000000..a92991656eac --- /dev/null +++ b/src/System.Memory/src/System/SpanSortHelpers.Keys.TDirectComparer.cs @@ -0,0 +1,269 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +#if !netstandard +using Internal.Runtime.CompilerServices; +#endif + +using static System.SpanSortHelpersCommon; + +namespace System +{ + internal static partial class SpanSortHelpersKeys_DirectComparer + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Sort( + ref TKey keys, int length, + TComparer comparer) + where TComparer : IDirectComparer + { + IntrospectiveSort(ref keys, length, comparer); + } + + private static void IntrospectiveSort( + ref TKey keys, int length, + TComparer comparer) + where TComparer : IDirectComparer + { + int depthLimit = 2 * FloorLog2PlusOne(length); + IntroSort(ref keys, 0, length - 1, depthLimit, comparer); + } + + private static void IntroSort( + ref TKey keys, + int lo, int hi, int depthLimit, + TComparer comparer) + where TComparer : IDirectComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + + while (hi > lo) + { + int partitionSize = hi - lo + 1; + if (partitionSize <= IntrosortSizeThreshold) + { + if (partitionSize == 1) + { + return; + } + if (partitionSize == 2) + { + Sort2(ref keys, lo, hi, comparer); + return; + } + if (partitionSize == 3) + { + ref TKey loRef = ref Unsafe.Add(ref keys, lo); + ref TKey miRef = ref Unsafe.Add(ref keys, hi - 1); + ref TKey hiRef = ref Unsafe.Add(ref keys, hi); + Sort3(ref loRef, ref miRef, ref hiRef, comparer); + return; + } + + InsertionSort(ref keys, lo, hi, comparer); + return; + } + + if (depthLimit == 0) + { + HeapSort(ref keys, lo, hi, comparer); + return; + } + depthLimit--; + + // We should never reach here, unless > 3 elements due to partition size + Debug.Assert(partitionSize > 3); + + int p = PickPivotAndPartition(ref keys, lo, hi, comparer); + // Note we've already partitioned around the pivot and do not have to move the pivot again. + IntroSort(ref keys, p + 1, hi, depthLimit, comparer); + hi = p - 1; + } + } + + private static int PickPivotAndPartition( + ref TKey keys, int lo, int hi, + TComparer comparer) + where TComparer : IDirectComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + // Compute median-of-three. But also partition them, since we've done the comparison. + + // PERF: `lo` or `hi` will never be negative inside the loop, + // so computing median using uints is safe since we know + // `length <= int.MaxValue`, and indices are >= 0 + // and thus cannot overflow an uint. + // Saves one subtraction per loop compared to + // `int middle = lo + ((hi - lo) >> 1);` + int middle = (int)(((uint)hi + (uint)lo) >> 1); + + // Sort lo, mid and hi appropriately, then pick mid as the pivot. + ref TKey keysAtLo = ref Unsafe.Add(ref keys, lo); + ref TKey keysAtMiddle = ref Unsafe.Add(ref keys, middle); + ref TKey keysAtHi = ref Unsafe.Add(ref keys, hi); + Sort3(ref keysAtLo, ref keysAtMiddle, ref keysAtHi, comparer); + + TKey pivot = keysAtMiddle; + + int left = lo; + int right = hi - 1; + // We already partitioned lo and hi and put the pivot in hi - 1. + // And we pre-increment & decrement below. + Swap(ref keysAtMiddle, ref Unsafe.Add(ref keys, right)); + + while (left < right) + { + // PERF: For internal direct comparers the range checks are not needed + // since we know they cannot be bogus i.e. pass the pivot without being false. + while (comparer.LessThan(Unsafe.Add(ref keys, ++left), pivot)) ; + while (comparer.LessThan(pivot, Unsafe.Add(ref keys, --right))) ; + + if (left >= right) + break; + + Swap(ref keys, left, right); + } + // Put pivot in the right location. + right = hi - 1; + if (left != right) + { + Swap(ref keys, left, right); + } + return left; + } + + private static void HeapSort( + ref TKey keys, int lo, int hi, + TComparer comparer) + where TComparer : IDirectComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + int n = hi - lo + 1; + for (int i = n / 2; i >= 1; --i) + { + DownHeap(ref keys, i, n, lo, comparer); + } + for (int i = n; i > 1; --i) + { + Swap(ref keys, lo, lo + i - 1); + DownHeap(ref keys, 1, i - 1, lo, comparer); + } + } + + private static void DownHeap( + ref TKey keys, int i, int n, int lo, + TComparer comparer) + where TComparer : IDirectComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + + // Below lines are equivalent to: TKey d = keys[lo + i - 1]; + ref TKey keysAtLo = ref Unsafe.Add(ref keys, lo); + ref TKey keysAtLoMinus1 = ref Unsafe.Add(ref keysAtLo, -1); // TODO: Use Subtract when available + TKey d = Unsafe.Add(ref keysAtLoMinus1, i); + int nHalf = n / 2; + while (i <= nHalf) + { + int child = i << 1; + + // Below lines are equivalent to: if (child < n && comparer(keys[lo + child - 1], keys[lo + child]) < 0) + if (child < n && + comparer.LessThan(Unsafe.Add(ref keysAtLoMinus1, child), Unsafe.Add(ref keysAtLo, child))) + { + ++child; + } + + // Below lines are equivalent to: if (!(comparer(d, keys[lo + child - 1]) < 0)) + if (!(comparer.LessThan(d, Unsafe.Add(ref keysAtLoMinus1, child)))) + break; + + // Below lines are equivalent to: keys[lo + i - 1] = keys[lo + child - 1] + Unsafe.Add(ref keysAtLoMinus1, i) = Unsafe.Add(ref keysAtLoMinus1, child); + + i = child; + } + // Below lines are equivalent to: keys[lo + i - 1] = d; + Unsafe.Add(ref keysAtLoMinus1, i) = d; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InsertionSort( + ref TKey keys, int lo, int hi, + TComparer comparer) + where TComparer : IDirectComparer + { + Debug.Assert(lo >= 0); + Debug.Assert(hi >= lo); + + for (int i = lo; i < hi; ++i) + { + int j = i; + + var t = Unsafe.Add(ref keys, j + 1); + + if (j >= lo && comparer.LessThan(t, Unsafe.Add(ref keys, j))) + { + do + { + Unsafe.Add(ref keys, j + 1) = Unsafe.Add(ref keys, j); + --j; + } + while (j >= lo && comparer.LessThan(t, Unsafe.Add(ref keys, j))); + + Unsafe.Add(ref keys, j + 1) = t; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort3( + ref TKey r0, ref TKey r1, ref TKey r2, + TComparer comparer) + where TComparer : IDirectComparer + { + Sort2(ref r0, ref r1, comparer); + Sort2(ref r0, ref r2, comparer); + Sort2(ref r1, ref r2, comparer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort2( + ref TKey keys, int i, int j, + TComparer comparer) + where TComparer : IDirectComparer + { + Debug.Assert(i != j); + + ref TKey a = ref Unsafe.Add(ref keys, i); + ref TKey b = ref Unsafe.Add(ref keys, j); + Sort2(ref a, ref b, comparer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort2( + ref TKey a, ref TKey b, TComparer comparer) + where TComparer : IDirectComparer + { + // This is one of the only places GreaterThan is needed + // but we need to preserve this due to bogus comparers or similar + if (comparer.GreaterThan(a, b)) + { + TKey temp = a; + a = b; + b = temp; + } + } + } +} diff --git a/src/System.Memory/src/System/SpanSortHelpers.Keys.cs b/src/System.Memory/src/System/SpanSortHelpers.Keys.cs new file mode 100644 index 000000000000..151b0b6ce72e --- /dev/null +++ b/src/System.Memory/src/System/SpanSortHelpers.Keys.cs @@ -0,0 +1,193 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#if !netstandard +using Internal.Runtime.CompilerServices; +#endif + +namespace System +{ + internal static partial class SpanSortHelpersKeys + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Sort(this Span keys) + { + int length = keys.Length; + if (length < 2) + return; + + // PERF: Try specialized here for optimal performance + // Code-gen is weird unless used in loop outside + if (!SpanSortHelpersKeys_DirectComparer.TrySortSpecialized( + ref MemoryMarshal.GetReference(keys), + length)) + { + DefaultSpanSortHelper.s_default.Sort( + ref MemoryMarshal.GetReference(keys), + length); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Sort( + this Span keys, TComparer comparer) + where TComparer : IComparer + { + int length = keys.Length; + if (length < 2) + return; + + DefaultSpanSortHelper.s_default.Sort( + ref MemoryMarshal.GetReference(keys), + length, comparer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Sort( + this Span keys, Comparison comparison) + { + int length = keys.Length; + if (length < 2) + return; + + DefaultSpanSortHelper.s_default.Sort( + ref MemoryMarshal.GetReference(keys), + length, comparison); + } + + internal static class DefaultSpanSortHelper + { + internal static readonly ISpanSortHelper s_default = CreateSortHelper(); + + private static ISpanSortHelper CreateSortHelper() + { + if (typeof(IComparable).IsAssignableFrom(typeof(TKey))) + { + // coreclr uses RuntimeTypeHandle.Allocate + var ctor = typeof(ComparableSpanSortHelper<>) + .MakeGenericType(new Type[] { typeof(TKey) }) + .GetConstructor(Array.Empty()); + + return (ISpanSortHelper)ctor.Invoke(Array.Empty()); + } + else + { + return new SpanSortHelper(); + } + } + } + + internal interface ISpanSortHelper + { + void Sort(ref TKey keys, int length); + void Sort(ref TKey keys, int length, Comparison comparison); + } + + internal class SpanSortHelper : ISpanSortHelper + { + public void Sort(ref TKey keys, int length) + { + SpanSortHelpersKeys_Comparer.Sort(ref keys, length, Comparer.Default); + } + + public void Sort(ref TKey keys, int length, Comparison comparison) + { + SpanSortHelpersKeys.Sort(ref keys, length, comparison); + } + } + + internal class ComparableSpanSortHelper + : ISpanSortHelper + where TKey : IComparable + { + public void Sort(ref TKey keys, int length) + { + SpanSortHelpersKeys.Sort(ref keys, length); + } + + public void Sort(ref TKey keys, int length, Comparison comparison) + { + SpanSortHelpersKeys.Sort(ref keys, length, comparison); + } + } + + + internal static class DefaultSpanSortHelper + where TComparer : IComparer + { + internal static readonly ISpanSortHelper s_default = CreateSortHelper(); + + private static ISpanSortHelper CreateSortHelper() + { + if (typeof(IComparable).IsAssignableFrom(typeof(TKey))) + { + // coreclr uses RuntimeTypeHandle.Allocate + var ctor = typeof(ComparableSpanSortHelper<,>) + .MakeGenericType(new Type[] { typeof(TKey), typeof(TComparer) }) + .GetConstructor(Array.Empty()); + + return (ISpanSortHelper)ctor.Invoke(Array.Empty()); + } + else + { + return new SpanSortHelper(); + } + } + } + + // https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Collections/Generic/ArraySortHelper.cs + internal interface ISpanSortHelper + where TComparer : IComparer + { + void Sort(ref TKey keys, int length, TComparer comparer); + } + + internal class SpanSortHelper : ISpanSortHelper + where TComparer : IComparer + { + public void Sort(ref TKey keys, int length, TComparer comparer) + { + if (typeof(TComparer) == typeof(IComparer) && comparer == null) + { + SpanSortHelpersKeys_Comparer.Sort(ref keys, length, Comparer.Default); + } + else + { + SpanSortHelpersKeys_Comparer.Sort(ref keys, length, comparer); + } + } + } + + internal class ComparableSpanSortHelper + : ISpanSortHelper + where TKey : IComparable + where TComparer : IComparer + { + public void Sort(ref TKey keys, int length, + TComparer comparer) + { + if (comparer == null || + // Cache this in generic traits helper class perhaps + (!typeof(TComparer).IsValueType && + object.ReferenceEquals(comparer, Comparer.Default))) // Or "=="? + { + if (!SpanSortHelpersKeys_DirectComparer.TrySortSpecialized(ref keys, length)) + { + // NOTE: For Bogus Comparable the exception message will be different, when using Comparer.Default + // Since the exception message is thrown internally without knowledge of the comparer + SpanSortHelpersKeys.Sort(ref keys, length); + } + } + else + { + SpanSortHelpersKeys_Comparer.Sort(ref keys, length, comparer); + } + } + } + } +} diff --git a/src/System.Memory/src/System/SpanSortHelpers.KeysValues.Comparison.cs b/src/System.Memory/src/System/SpanSortHelpers.KeysValues.Comparison.cs new file mode 100644 index 000000000000..6c2af8e8cf81 --- /dev/null +++ b/src/System.Memory/src/System/SpanSortHelpers.KeysValues.Comparison.cs @@ -0,0 +1,282 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +#if !netstandard +using Internal.Runtime.CompilerServices; +#endif + +using static System.SpanSortHelpersCommon; + +namespace System +{ + internal static partial class SpanSortHelpersKeysValues + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Sort( + ref TKey keys, ref TValue values, int length, + Comparison comparison) + { + IntrospectiveSort(ref keys, ref values, length, comparison); + } + + private static void IntrospectiveSort( + ref TKey keys, ref TValue values, int length, + Comparison comparison) + { + int depthLimit = 2 * FloorLog2PlusOne(length); + IntroSort(ref keys, ref values, 0, length - 1, depthLimit, comparison); + } + + private static void IntroSort( + ref TKey keys, ref TValue values, + int lo, int hi, int depthLimit, + Comparison comparison) + { + Debug.Assert(comparison != null); + Debug.Assert(lo >= 0); + + while (hi > lo) + { + int partitionSize = hi - lo + 1; + if (partitionSize <= IntrosortSizeThreshold) + { + if (partitionSize == 1) + { + return; + } + if (partitionSize == 2) + { + Sort2(ref keys, ref values, lo, hi, comparison); + return; + } + if (partitionSize == 3) + { + Sort3(ref keys, ref values, lo, hi - 1, hi, comparison); + return; + } + InsertionSort(ref keys, ref values, lo, hi, comparison); + return; + } + + if (depthLimit == 0) + { + HeapSort(ref keys, ref values, lo, hi, comparison); + return; + } + depthLimit--; + + // We should never reach here, unless > 3 elements due to partition size + Debug.Assert(partitionSize > 3); + + int p = PickPivotAndPartition(ref keys, ref values, lo, hi, comparison); + // Note we've already partitioned around the pivot and do not have to move the pivot again. + IntroSort(ref keys, ref values, p + 1, hi, depthLimit, comparison); + hi = p - 1; + } + } + + private static int PickPivotAndPartition( + ref TKey keys, ref TValue values, int lo, int hi, + Comparison comparison) + + { + Debug.Assert(comparison != null); + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + // Compute median-of-three. But also partition them, since we've done the comparison. + + // PERF: `lo` or `hi` will never be negative inside the loop, + // so computing median using uints is safe since we know + // `length <= int.MaxValue`, and indices are >= 0 + // and thus cannot overflow an uint. + // Saves one subtraction per loop compared to + // `int middle = lo + ((hi - lo) >> 1);` + int middle = (int)(((uint)hi + (uint)lo) >> 1); + + // Sort lo, mid and hi appropriately, then pick mid as the pivot. + ref TKey keysAtMiddle = ref Sort3(ref keys, ref values, lo, middle, hi, comparison); + + TKey pivot = keysAtMiddle; + + int left = lo; + int right = hi - 1; + // We already partitioned lo and hi and put the pivot in hi - 1. + // And we pre-increment & decrement below. + Swap(ref keysAtMiddle, ref Unsafe.Add(ref keys, right)); + Swap(ref values, middle, right); + + while (left < right) + { + while (left < (hi - 1) && comparison(Unsafe.Add(ref keys, ++left), pivot) < 0) ; + // Check if bad comparable/comparison + if (left == (hi - 1) && comparison(Unsafe.Add(ref keys, left), pivot) < 0) + ThrowHelper.ThrowArgumentException_BadComparer(comparison); + + while (right > lo && comparison(pivot, Unsafe.Add(ref keys, --right)) < 0) ; + // Check if bad comparable/comparison + if (right == lo && comparison(pivot, Unsafe.Add(ref keys, right)) < 0) + ThrowHelper.ThrowArgumentException_BadComparer(comparison); + + if (left >= right) + break; + + Swap(ref keys, left, right); + Swap(ref values, left, right); + } + // Put pivot in the right location. + right = hi - 1; + if (left != right) + { + Swap(ref keys, left, right); + Swap(ref values, left, right); + } + return left; + } + + private static void HeapSort( + ref TKey keys, ref TValue values, int lo, int hi, + Comparison comparison) + + { + Debug.Assert(comparison != null); + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + int n = hi - lo + 1; + for (int i = n / 2; i >= 1; --i) + { + DownHeap(ref keys, ref values, i, n, lo, comparison); + } + for (int i = n; i > 1; --i) + { + Swap(ref keys, lo, lo + i - 1); + Swap(ref values, lo, lo + i - 1); + DownHeap(ref keys, ref values, 1, i - 1, lo, comparison); + } + } + + private static void DownHeap( + ref TKey keys, ref TValue values, int i, int n, int lo, + Comparison comparison) + + { + Debug.Assert(comparison != null); + Debug.Assert(lo >= 0); + + // Below lines are equivalent to: TKey d = keys[lo + i - 1]; + ref TKey keysAtLo = ref Unsafe.Add(ref keys, lo); + ref TKey keysAtLoMinus1 = ref Unsafe.Add(ref keysAtLo, -1); // TODO: Use Subtract when available + + ref TValue valuesAtLoMinus1 = ref Unsafe.Add(ref values, lo - 1); + + TKey d = Unsafe.Add(ref keysAtLoMinus1, i); + TValue dValue = Unsafe.Add(ref valuesAtLoMinus1, i); + + int nHalf = n / 2; + while (i <= nHalf) + { + int child = i << 1; + + //if (child < n && comparison(keys[lo + child - 1], keys[lo + child]) < 0) + if (child < n && + comparison(Unsafe.Add(ref keysAtLoMinus1, child), Unsafe.Add(ref keysAtLo, child)) < 0) + { + ++child; + } + + //if (!(comparison(d, keys[lo + child - 1]) < 0)) + if (!(comparison(d, Unsafe.Add(ref keysAtLoMinus1, child)) < 0)) + break; + + // Below lines are equivalent to: keys[lo + i - 1] = keys[lo + child - 1] + Unsafe.Add(ref keysAtLoMinus1, i) = Unsafe.Add(ref keysAtLoMinus1, child); + Unsafe.Add(ref valuesAtLoMinus1, i) = Unsafe.Add(ref valuesAtLoMinus1, child); + + i = child; + } + // Below lines are equivalent to: keys[lo + i - 1] = d; + Unsafe.Add(ref keysAtLoMinus1, i) = d; + Unsafe.Add(ref valuesAtLoMinus1, i) = dValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InsertionSort( + ref TKey keys, ref TValue values, int lo, int hi, + Comparison comparison) + + { + Debug.Assert(lo >= 0); + Debug.Assert(hi >= lo); + + for (int i = lo; i < hi; ++i) + { + int j = i; + + var t = Unsafe.Add(ref keys, j + 1); + + if (j >= lo && comparison(t, Unsafe.Add(ref keys, j)) < 0) + { + var v = Unsafe.Add(ref values, j + 1); + do + { + Unsafe.Add(ref keys, j + 1) = Unsafe.Add(ref keys, j); + Unsafe.Add(ref values, j + 1) = Unsafe.Add(ref values, j); + --j; + } + while (j >= lo && comparison(t, Unsafe.Add(ref keys, j)) < 0); + + Unsafe.Add(ref keys, j + 1) = t; + Unsafe.Add(ref values, j + 1) = v; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ref TKey Sort3( + ref TKey keys, ref TValue values, int i0, int i1, int i2, + Comparison comparison) + + { + ref var r0 = ref Unsafe.Add(ref keys, i0); + ref var r1 = ref Unsafe.Add(ref keys, i1); + ref var r2 = ref Unsafe.Add(ref keys, i2); + Sort2(ref r0, ref r1, comparison, ref values, i0, i1); + Sort2(ref r0, ref r2, comparison, ref values, i0, i2); + Sort2(ref r1, ref r2, comparison, ref values, i1, i2); + return ref r1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort2( + ref TKey keys, ref TValue values, int i, int j, + Comparison comparison) + + { + Debug.Assert(i != j); + + ref TKey a = ref Unsafe.Add(ref keys, i); + ref TKey b = ref Unsafe.Add(ref keys, j); + Sort2(ref a, ref b, comparison, ref values, i, j); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort2( + ref TKey a, ref TKey b, Comparison comparison, + ref TValue values, int i, int j) + + { + // This is one of the only places GreaterThan is needed + // but we need to preserve this due to bogus comparers or similar + if (comparison(a, b) > 0) + { + Swap(ref a, ref b); + Swap(ref values, i, j); + } + } + } +} diff --git a/src/System.Memory/src/System/SpanSortHelpers.KeysValues.IComparable.cs b/src/System.Memory/src/System/SpanSortHelpers.KeysValues.IComparable.cs new file mode 100644 index 000000000000..fef473ac76fb --- /dev/null +++ b/src/System.Memory/src/System/SpanSortHelpers.KeysValues.IComparable.cs @@ -0,0 +1,283 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +#if !netstandard +using Internal.Runtime.CompilerServices; +#endif + +using static System.SpanSortHelpersCommon; + +namespace System +{ + internal static partial class SpanSortHelpersKeysValues + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Sort( + ref TKey keys, ref TValue values, int length) + where TKey : IComparable + { + IntrospectiveSort(ref keys, ref values, length); + } + + private static void IntrospectiveSort( + ref TKey keys, ref TValue values, int length) + where TKey : IComparable + { + int depthLimit = 2 * FloorLog2PlusOne(length); + IntroSort(ref keys, ref values, 0, length - 1, depthLimit); + } + + private static void IntroSort( + ref TKey keys, ref TValue values, + int lo, int hi, int depthLimit) + where TKey : IComparable + { + Debug.Assert(lo >= 0); + + while (hi > lo) + { + int partitionSize = hi - lo + 1; + if (partitionSize <= IntrosortSizeThreshold) + { + if (partitionSize == 1) + { + return; + } + if (partitionSize == 2) + { + Sort2(ref keys, ref values, lo, hi); + return; + } + if (partitionSize == 3) + { + Sort3(ref keys, ref values, lo, hi - 1, hi); + return; + } + InsertionSort(ref keys, ref values, lo, hi); + return; + } + + if (depthLimit == 0) + { + HeapSort(ref keys, ref values, lo, hi); + return; + } + depthLimit--; + + // We should never reach here, unless > 3 elements due to partition size + Debug.Assert(partitionSize > 3); + + int p = PickPivotAndPartition(ref keys, ref values, lo, hi); + // Note we've already partitioned around the pivot and do not have to move the pivot again. + IntroSort(ref keys, ref values, p + 1, hi, depthLimit); + hi = p - 1; + } + } + + private static int PickPivotAndPartition( + ref TKey keys, ref TValue values, int lo, int hi) + where TKey : IComparable + { + + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + // Compute median-of-three. But also partition them, since we've done the comparison. + + // PERF: `lo` or `hi` will never be negative inside the loop, + // so computing median using uints is safe since we know + // `length <= int.MaxValue`, and indices are >= 0 + // and thus cannot overflow an uint. + // Saves one subtraction per loop compared to + // `int middle = lo + ((hi - lo) >> 1);` + int middle = (int)(((uint)hi + (uint)lo) >> 1); + + // Sort lo, mid and hi appropriately, then pick mid as the pivot. + ref TKey keysAtMiddle = ref Sort3(ref keys, ref values, lo, middle, hi); + + TKey pivot = keysAtMiddle; + + int left = lo; + int right = hi - 1; + // We already partitioned lo and hi and put the pivot in hi - 1. + // And we pre-increment & decrement below. + Swap(ref keysAtMiddle, ref Unsafe.Add(ref keys, right)); + Swap(ref values, middle, right); + + while (left < right) + { + if (pivot == null) + { + while (left < (hi - 1) && Unsafe.Add(ref keys, ++left) == null) ; + while (right > lo && Unsafe.Add(ref keys, --right) != null) ; + } + else + { + while (left < (hi - 1) && pivot.CompareTo(Unsafe.Add(ref keys, ++left)) > 0) ; + // Check if bad comparable/comparer + if (left == (hi - 1) && pivot.CompareTo(Unsafe.Add(ref keys, left)) > 0) + ThrowHelper.ThrowArgumentException_BadComparable(typeof(TKey)); + + while (right > lo && pivot.CompareTo(Unsafe.Add(ref keys, --right)) < 0) ; + // Check if bad comparable/comparer + if (right == lo && pivot.CompareTo(Unsafe.Add(ref keys, right)) < 0) + ThrowHelper.ThrowArgumentException_BadComparable(typeof(TKey)); + } + + if (left >= right) + break; + + Swap(ref keys, left, right); + Swap(ref values, left, right); + } + // Put pivot in the right location. + right = hi - 1; + if (left != right) + { + Swap(ref keys, left, right); + Swap(ref values, left, right); + } + return left; + } + + private static void HeapSort( + ref TKey keys, ref TValue values, int lo, int hi + ) + where TKey : IComparable + { + + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + int n = hi - lo + 1; + for (int i = n / 2; i >= 1; --i) + { + DownHeap(ref keys, ref values, i, n, lo); + } + for (int i = n; i > 1; --i) + { + Swap(ref keys, lo, lo + i - 1); + Swap(ref values, lo, lo + i - 1); + DownHeap(ref keys, ref values, 1, i - 1, lo); + } + } + + private static void DownHeap( + ref TKey keys, ref TValue values, int i, int n, int lo) + where TKey : IComparable + { + + Debug.Assert(lo >= 0); + + // Below lines are equivalent to: TKey d = keys[lo + i - 1]; + ref TKey keysAtLo = ref Unsafe.Add(ref keys, lo); + ref TKey keysAtLoMinus1 = ref Unsafe.Add(ref keysAtLo, -1); // TODO: Use Subtract when available + + ref TValue valuesAtLoMinus1 = ref Unsafe.Add(ref values, lo - 1); + + TKey d = Unsafe.Add(ref keysAtLoMinus1, i); + TValue dValue = Unsafe.Add(ref valuesAtLoMinus1, i); + + int nHalf = n / 2; + while (i <= nHalf) + { + int child = i << 1; + + //if (child < n && (keys[lo + child - 1] == null || keys[lo + child - 1].CompareTo(keys[lo + child]) < 0)) + if (child < n && + (Unsafe.Add(ref keysAtLoMinus1, child) == null || + Unsafe.Add(ref keysAtLoMinus1, child).CompareTo(Unsafe.Add(ref keysAtLo, child)) < 0)) + { + ++child; + } + + //if (keys[lo + child - 1] == null || keys[lo + child - 1].CompareTo(d) < 0) + if (Unsafe.Add(ref keysAtLoMinus1, child) == null || + Unsafe.Add(ref keysAtLoMinus1, child).CompareTo(d) < 0) + break; + + // Below lines are equivalent to: keys[lo + i - 1] = keys[lo + child - 1] + Unsafe.Add(ref keysAtLoMinus1, i) = Unsafe.Add(ref keysAtLoMinus1, child); + Unsafe.Add(ref valuesAtLoMinus1, i) = Unsafe.Add(ref valuesAtLoMinus1, child); + + i = child; + } + // Below lines are equivalent to: keys[lo + i - 1] = d; + Unsafe.Add(ref keysAtLoMinus1, i) = d; + Unsafe.Add(ref valuesAtLoMinus1, i) = dValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InsertionSort( + ref TKey keys, ref TValue values, int lo, int hi) + where TKey : IComparable + { + Debug.Assert(lo >= 0); + Debug.Assert(hi >= lo); + + for (int i = lo; i < hi; ++i) + { + int j = i; + + var t = Unsafe.Add(ref keys, j + 1); + + if (j >= lo && (t == null || t.CompareTo(Unsafe.Add(ref keys, j)) < 0)) + { + var v = Unsafe.Add(ref values, j + 1); + do + { + Unsafe.Add(ref keys, j + 1) = Unsafe.Add(ref keys, j); + Unsafe.Add(ref values, j + 1) = Unsafe.Add(ref values, j); + --j; + } + while (j >= lo && (t == null || t.CompareTo(Unsafe.Add(ref keys, j)) < 0)); + //while (j >= lo && (t == null || t.CompareTo(keys[j]) < 0)) + + Unsafe.Add(ref keys, j + 1) = t; + Unsafe.Add(ref values, j + 1) = v; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ref TKey Sort3( + ref TKey keys, ref TValue values, int i0, int i1, int i2) + where TKey : IComparable + { + ref var r0 = ref Unsafe.Add(ref keys, i0); + ref var r1 = ref Unsafe.Add(ref keys, i1); + ref var r2 = ref Unsafe.Add(ref keys, i2); + Sort2(ref r0, ref r1, ref values, i0, i1); + Sort2(ref r0, ref r2, ref values, i0, i2); + Sort2(ref r1, ref r2, ref values, i1, i2); + return ref r1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort2( + ref TKey keys, ref TValue values, int i, int j) + where TKey : IComparable + { + Debug.Assert(i != j); + + ref TKey a = ref Unsafe.Add(ref keys, i); + ref TKey b = ref Unsafe.Add(ref keys, j); + Sort2(ref a, ref b, ref values, i, j); + } + + private static void Sort2( + ref TKey a, ref TKey b, ref TValue values, int i, int j) + where TKey : IComparable + { + if (a != null && a.CompareTo(b) > 0) + { + Swap(ref a, ref b); + Swap(ref values, i, j); + } + } + } +} diff --git a/src/System.Memory/src/System/SpanSortHelpers.KeysValues.Specialized.cs b/src/System.Memory/src/System/SpanSortHelpers.KeysValues.Specialized.cs new file mode 100644 index 000000000000..0f3ec6348a47 --- /dev/null +++ b/src/System.Memory/src/System/SpanSortHelpers.KeysValues.Specialized.cs @@ -0,0 +1,175 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +#if !netstandard +using Internal.Runtime.CompilerServices; +#endif + +using static System.SpanSortHelpersCommon; + +namespace System +{ + internal static partial class SpanSortHelpersKeysValues_DirectComparer + { + // https://github.com/dotnet/coreclr/blob/master/src/classlibnative/bcltype/arrayhelpers.cpp + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TrySortSpecialized( + ref TKey keys, ref TValue values, int length) + { + // TODO: Change to do specific value tests below, since otherwise not fast... maybe + if (typeof(TKey) != typeof(TValue)) + { + return false; + } + else + // Types unfolding adopted from https://github.com/dotnet/coreclr/blob/master/src/classlibnative/bcltype/arrayhelpers.cpp#L268 + if (typeof(TKey) == typeof(sbyte)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, ref values, length, new SByteDirectComparer()); + return true; + } + else if (typeof(TKey) == typeof(byte) || + typeof(TKey) == typeof(bool)) // Use byte for bools to reduce code size + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, ref values, length, new ByteDirectComparer()); + return true; + } + else if (typeof(TKey) == typeof(short)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, ref values, length, new Int16DirectComparer()); + return true; + } + else if (typeof(TKey) == typeof(ushort) || + typeof(TKey) == typeof(char)) // Use ushort for chars to reduce code size) + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, ref values, length, new UInt16DirectComparer()); + return true; + } + else if (typeof(TKey) == typeof(int)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, ref values, length, new Int32DirectComparer()); + return true; + } + else if (typeof(TKey) == typeof(uint)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, ref values, length, new UInt32DirectComparer()); + return true; + } + else if (typeof(TKey) == typeof(long)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, ref values, length, new Int64DirectComparer()); + return true; + } + else if (typeof(TKey) == typeof(ulong)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + Sort(ref specificKeys, ref values, length, new UInt64DirectComparer()); + return true; + } + // Array.Sort only uses NaNPrepass when both key and value are the same, + // to give exactly the same result we have to do the same. + // Not only that, the comparisons will then be different since when using normal sort + // code the IComparable<> path is different than the specialized. + else if (typeof(TKey) == typeof(float) && typeof(TValue) == typeof(float)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + // Array.Sort only uses NaNPrepass when both key and value are the same + //if (typeof(TValue) == typeof(float)) + { + // Comparison to NaN is always false, so do a linear pass + // and swap all NaNs to the front of the array + var left = NaNPrepass(ref specificKeys, ref values, length, new SingleIsNaN()); + + var remaining = length - left; + if (remaining > 1) + { + ref var afterNaNsKeys = ref Unsafe.Add(ref specificKeys, left); + ref var afterNaNsValues = ref Unsafe.Add(ref values, left); + Sort(ref afterNaNsKeys, ref afterNaNsValues, remaining, new SingleDirectComparer()); + } + } + //else + //{ + // Sort(ref specificKeys, ref values, length, new SingleDirectComparer()); + //} + return true; + } + // Array.Sort only uses NaNPrepass when both key and value are the same, + // to give exactly the same result we have to do the same. + // Not only that, the comparisons will then be different since when using normal sort + // code the IComparable<> path is different than the specialized. + else if (typeof(TKey) == typeof(double) && typeof(TValue) == typeof(double)) + { + ref var specificKeys = ref Unsafe.As(ref keys); + // Array.Sort only uses NaNPrepass when both key and value are the same + //if (typeof(TValue) == typeof(double)) + { + // Comparison to NaN is always false, so do a linear pass + // and swap all NaNs to the front of the array + var left = NaNPrepass(ref specificKeys, ref values, length, new DoubleIsNaN()); + + var remaining = length - left; + if (remaining > 1) + { + ref var afterNaNsKeys = ref Unsafe.Add(ref specificKeys, left); + ref var afterNaNsValues = ref Unsafe.Add(ref values, left); + Sort(ref afterNaNsKeys, ref afterNaNsValues, remaining, new DoubleDirectComparer()); + } + } + //else + //{ + // Sort(ref specificKeys, ref values, length, new DoubleDirectComparer()); + //} + return true; + } + // TODO: Specialize for string if necessary. What about the == null checks? + //else if (typeof(TKey) == typeof(string)) + //{ + // ref var specificKeys = ref Unsafe.As(ref keys); + // Sort(ref specificKeys, ref values, length, new StringDirectComparer()); + // return true; + //} + else + { + return false; + } + } + + // For sorting, move all NaN instances to front of the input array + private static int NaNPrepass( + ref TKey keys, ref TValue values, int length, + TIsNaN isNaN) + where TIsNaN : struct, IIsNaN + { + int left = 0; + for (int i = 0; i < length; i++) + { + ref TKey current = ref Unsafe.Add(ref keys, i); + if (isNaN.IsNaN(current)) + { + // TODO: If first index is not NaN or we find just one not NaNs + // we could skip to version that no longer checks this + if (left != i) + { + ref TKey previous = ref Unsafe.Add(ref keys, left); + Swap(ref previous, ref current); + Swap(ref values, left, i); + } + ++left; + } + } + return left; + } + } +} diff --git a/src/System.Memory/src/System/SpanSortHelpers.KeysValues.TComparer.cs b/src/System.Memory/src/System/SpanSortHelpers.KeysValues.TComparer.cs new file mode 100644 index 000000000000..38eefeb4dd07 --- /dev/null +++ b/src/System.Memory/src/System/SpanSortHelpers.KeysValues.TComparer.cs @@ -0,0 +1,286 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +#if !netstandard +using Internal.Runtime.CompilerServices; +#endif + +using static System.SpanSortHelpersCommon; + +namespace System +{ + internal static partial class SpanSortHelpersKeysValues + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Sort( + ref TKey keys, ref TValue values, int length, + TComparer comparer) + where TComparer : IComparer + { + IntrospectiveSort(ref keys, ref values, length, comparer); + } + + private static void IntrospectiveSort( + ref TKey keys, ref TValue values, int length, + TComparer comparer) + where TComparer : IComparer + { + int depthLimit = 2 * FloorLog2PlusOne(length); + IntroSort(ref keys, ref values, 0, length - 1, depthLimit, comparer); + } + + private static void IntroSort( + ref TKey keys, ref TValue values, + int lo, int hi, int depthLimit, + TComparer comparer) + where TComparer : IComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + + while (hi > lo) + { + int partitionSize = hi - lo + 1; + if (partitionSize <= IntrosortSizeThreshold) + { + if (partitionSize == 1) + { + return; + } + if (partitionSize == 2) + { + Sort2(ref keys, ref values, lo, hi, comparer); + return; + } + if (partitionSize == 3) + { + Sort3(ref keys, ref values, lo, hi - 1, hi, comparer); + return; + } + InsertionSort(ref keys, ref values, lo, hi, comparer); + return; + } + + if (depthLimit == 0) + { + HeapSort(ref keys, ref values, lo, hi, comparer); + return; + } + depthLimit--; + + // We should never reach here, unless > 3 elements due to partition size + Debug.Assert(partitionSize > 3); + + int p = PickPivotAndPartition(ref keys, ref values, lo, hi, comparer); + // Note we've already partitioned around the pivot and do not have to move the pivot again. + IntroSort(ref keys, ref values, p + 1, hi, depthLimit, comparer); + hi = p - 1; + } + } + + private static int PickPivotAndPartition( + ref TKey keys, ref TValue values, int lo, int hi, + TComparer comparer) + where TComparer : IComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + // Compute median-of-three. But also partition them, since we've done the comparison. + + // PERF: `lo` or `hi` will never be negative inside the loop, + // so computing median using uints is safe since we know + // `length <= int.MaxValue`, and indices are >= 0 + // and thus cannot overflow an uint. + // Saves one subtraction per loop compared to + // `int middle = lo + ((hi - lo) >> 1);` + int middle = (int)(((uint)hi + (uint)lo) >> 1); + + // Sort lo, mid and hi appropriately, then pick mid as the pivot. + ref TKey keysAtMiddle = ref Sort3(ref keys, ref values, lo, middle, hi, comparer); + + TKey pivot = keysAtMiddle; + + int left = lo; + int right = hi - 1; + // We already partitioned lo and hi and put the pivot in hi - 1. + // And we pre-increment & decrement below. + Swap(ref keysAtMiddle, ref Unsafe.Add(ref keys, right)); + Swap(ref values, middle, right); + + while (left < right) + { + while (left < (hi - 1) && comparer.Compare(Unsafe.Add(ref keys, ++left), pivot) < 0) ; + // Check if bad comparable/comparer + if (left == (hi - 1) && comparer.Compare(Unsafe.Add(ref keys, left), pivot) < 0) + ThrowHelper.ThrowArgumentException_BadComparer(comparer); + + while (right > lo && comparer.Compare(pivot, Unsafe.Add(ref keys, --right)) < 0) ; + // Check if bad comparable/comparer + if (right == lo && comparer.Compare(pivot, Unsafe.Add(ref keys, right)) < 0) + ThrowHelper.ThrowArgumentException_BadComparer(comparer); + + if (left >= right) + break; + + Swap(ref keys, left, right); + Swap(ref values, left, right); + } + // Put pivot in the right location. + right = hi - 1; + if (left != right) + { + Swap(ref keys, left, right); + Swap(ref values, left, right); + } + return left; + } + + private static void HeapSort( + ref TKey keys, ref TValue values, int lo, int hi, + TComparer comparer) + where TComparer : IComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + int n = hi - lo + 1; + for (int i = n / 2; i >= 1; --i) + { + DownHeap(ref keys, ref values, i, n, lo, comparer); + } + for (int i = n; i > 1; --i) + { + Swap(ref keys, lo, lo + i - 1); + Swap(ref values, lo, lo + i - 1); + DownHeap(ref keys, ref values, 1, i - 1, lo, comparer); + } + } + + private static void DownHeap( + ref TKey keys, ref TValue values, int i, int n, int lo, + TComparer comparer) + where TComparer : IComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + + // Below lines are equivalent to: TKey d = keys[lo + i - 1]; + ref TKey keysAtLo = ref Unsafe.Add(ref keys, lo); + ref TKey keysAtLoMinus1 = ref Unsafe.Add(ref keysAtLo, -1); // TODO: Use Subtract when available + + ref TValue valuesAtLoMinus1 = ref Unsafe.Add(ref values, lo - 1); + + TKey d = Unsafe.Add(ref keysAtLoMinus1, i); + TValue dValue = Unsafe.Add(ref valuesAtLoMinus1, i); + + int nHalf = n / 2; + while (i <= nHalf) + { + int child = i << 1; + + // Below lines are equivalent to: if (child < n && comparer(keys[lo + child - 1], keys[lo + child]) < 0) + if (child < n && + comparer.Compare(Unsafe.Add(ref keysAtLoMinus1, child), Unsafe.Add(ref keysAtLo, child)) < 0) + { + ++child; + } + + // Below lines are equivalent to: if (!(comparer(d, keys[lo + child - 1]) < 0)) + if (!(comparer.Compare(d, Unsafe.Add(ref keysAtLoMinus1, child)) < 0)) + break; + + // Below lines are equivalent to: keys[lo + i - 1] = keys[lo + child - 1] + Unsafe.Add(ref keysAtLoMinus1, i) = Unsafe.Add(ref keysAtLoMinus1, child); + Unsafe.Add(ref valuesAtLoMinus1, i) = Unsafe.Add(ref valuesAtLoMinus1, child); + + i = child; + } + // Below lines are equivalent to: keys[lo + i - 1] = d; + Unsafe.Add(ref keysAtLoMinus1, i) = d; + Unsafe.Add(ref valuesAtLoMinus1, i) = dValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InsertionSort( + ref TKey keys, ref TValue values, int lo, int hi, + TComparer comparer) + where TComparer : IComparer + { + Debug.Assert(lo >= 0); + Debug.Assert(hi >= lo); + + for (int i = lo; i < hi; ++i) + { + int j = i; + + var t = Unsafe.Add(ref keys, j + 1); + + if (j >= lo && comparer.Compare(t, Unsafe.Add(ref keys, j)) < 0) + { + var v = Unsafe.Add(ref values, j + 1); + do + { + Unsafe.Add(ref keys, j + 1) = Unsafe.Add(ref keys, j); + Unsafe.Add(ref values, j + 1) = Unsafe.Add(ref values, j); + --j; + } + while (j >= lo && comparer.Compare(t, Unsafe.Add(ref keys, j)) < 0); + + Unsafe.Add(ref keys, j + 1) = t; + Unsafe.Add(ref values, j + 1) = v; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ref TKey Sort3( + ref TKey keys, ref TValue values, int i0, int i1, int i2, + TComparer comparer) + where TComparer : IComparer + { + ref var r0 = ref Unsafe.Add(ref keys, i0); + ref var r1 = ref Unsafe.Add(ref keys, i1); + ref var r2 = ref Unsafe.Add(ref keys, i2); + Sort2(ref r0, ref r1, comparer, ref values, i0, i1); + Sort2(ref r0, ref r2, comparer, ref values, i0, i2); + Sort2(ref r1, ref r2, comparer, ref values, i1, i2); + return ref r1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort2( + ref TKey keys, ref TValue values, int i, int j, + TComparer comparer) + where TComparer : IComparer + { + Debug.Assert(i != j); + + ref TKey a = ref Unsafe.Add(ref keys, i); + ref TKey b = ref Unsafe.Add(ref keys, j); + Sort2(ref a, ref b, comparer, ref values, i, j); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort2( + ref TKey a, ref TKey b, TComparer comparer, + ref TValue values, int i, int j) + where TComparer : IComparer + { + // This is one of the only places GreaterThan is needed + // but we need to preserve this due to bogus comparers or similar + if (comparer.Compare(a, b) > 0) + { + Swap(ref a, ref b); + Swap(ref values, i, j); + } + } + } +} diff --git a/src/System.Memory/src/System/SpanSortHelpers.KeysValues.TDirectComparer.cs b/src/System.Memory/src/System/SpanSortHelpers.KeysValues.TDirectComparer.cs new file mode 100644 index 000000000000..2897f9dc862b --- /dev/null +++ b/src/System.Memory/src/System/SpanSortHelpers.KeysValues.TDirectComparer.cs @@ -0,0 +1,280 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +#if !netstandard +using Internal.Runtime.CompilerServices; +#endif + +using static System.SpanSortHelpersCommon; + +namespace System +{ + internal static partial class SpanSortHelpersKeysValues_DirectComparer + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Sort( + ref TKey keys, ref TValue values, int length, + TComparer comparer) + where TComparer : IDirectComparer + { + IntrospectiveSort(ref keys, ref values, length, comparer); + } + + private static void IntrospectiveSort( + ref TKey keys, ref TValue values, int length, + TComparer comparer) + where TComparer : IDirectComparer + { + int depthLimit = 2 * FloorLog2PlusOne(length); + IntroSort(ref keys, ref values, 0, length - 1, depthLimit, comparer); + } + + private static void IntroSort( + ref TKey keys, ref TValue values, + int lo, int hi, int depthLimit, + TComparer comparer) + where TComparer : IDirectComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + + while (hi > lo) + { + int partitionSize = hi - lo + 1; + if (partitionSize <= IntrosortSizeThreshold) + { + if (partitionSize == 1) + { + return; + } + if (partitionSize == 2) + { + Sort2(ref keys, ref values, lo, hi, comparer); + return; + } + if (partitionSize == 3) + { + Sort3(ref keys, ref values, lo, hi - 1, hi, comparer); + return; + } + InsertionSort(ref keys, ref values, lo, hi, comparer); + return; + } + + if (depthLimit == 0) + { + HeapSort(ref keys, ref values, lo, hi, comparer); + return; + } + depthLimit--; + + // We should never reach here, unless > 3 elements due to partition size + Debug.Assert(partitionSize > 3); + + int p = PickPivotAndPartition(ref keys, ref values, lo, hi, comparer); + // Note we've already partitioned around the pivot and do not have to move the pivot again. + IntroSort(ref keys, ref values, p + 1, hi, depthLimit, comparer); + hi = p - 1; + } + } + + private static int PickPivotAndPartition( + ref TKey keys, ref TValue values, int lo, int hi, + TComparer comparer) + where TComparer : IDirectComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + // Compute median-of-three. But also partition them, since we've done the comparison. + + // PERF: `lo` or `hi` will never be negative inside the loop, + // so computing median using uints is safe since we know + // `length <= int.MaxValue`, and indices are >= 0 + // and thus cannot overflow an uint. + // Saves one subtraction per loop compared to + // `int middle = lo + ((hi - lo) >> 1);` + int middle = (int)(((uint)hi + (uint)lo) >> 1); + + // Sort lo, mid and hi appropriately, then pick mid as the pivot. + ref TKey keysAtMiddle = ref Sort3(ref keys, ref values, lo, middle, hi, comparer); + + TKey pivot = keysAtMiddle; + + int left = lo; + int right = hi - 1; + // We already partitioned lo and hi and put the pivot in hi - 1. + // And we pre-increment & decrement below. + Swap(ref keysAtMiddle, ref Unsafe.Add(ref keys, right)); + Swap(ref values, middle, right); + + while (left < right) + { + // PERF: For internal direct comparers the range checks are not needed + // since we know they cannot be bogus i.e. pass the pivot without being false. + while (comparer.LessThan(Unsafe.Add(ref keys, ++left), pivot)) ; + while (comparer.LessThan(pivot, Unsafe.Add(ref keys, --right))) ; + + if (left >= right) + break; + + Swap(ref keys, left, right); + Swap(ref values, left, right); + } + // Put pivot in the right location. + right = hi - 1; + if (left != right) + { + Swap(ref keys, left, right); + Swap(ref values, left, right); + } + return left; + } + + private static void HeapSort( + ref TKey keys, ref TValue values, int lo, int hi, + TComparer comparer) + where TComparer : IDirectComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + Debug.Assert(hi > lo); + + int n = hi - lo + 1; + for (int i = n / 2; i >= 1; --i) + { + DownHeap(ref keys, ref values, i, n, lo, comparer); + } + for (int i = n; i > 1; --i) + { + Swap(ref keys, lo, lo + i - 1); + Swap(ref values, lo, lo + i - 1); + DownHeap(ref keys, ref values, 1, i - 1, lo, comparer); + } + } + + private static void DownHeap( + ref TKey keys, ref TValue values, int i, int n, int lo, + TComparer comparer) + where TComparer : IDirectComparer + { + Debug.Assert(comparer != null); + Debug.Assert(lo >= 0); + + // Below lines are equivalent to: TKey d = keys[lo + i - 1]; + ref TKey keysAtLo = ref Unsafe.Add(ref keys, lo); + ref TKey keysAtLoMinus1 = ref Unsafe.Add(ref keysAtLo, -1); // TODO: Use Subtract when available + + ref TValue valuesAtLoMinus1 = ref Unsafe.Add(ref values, lo - 1); + + TKey d = Unsafe.Add(ref keysAtLoMinus1, i); + TValue dValue = Unsafe.Add(ref valuesAtLoMinus1, i); + + int nHalf = n / 2; + while (i <= nHalf) + { + int child = i << 1; + + // Below lines are equivalent to: if (child < n && comparer(keys[lo + child - 1], keys[lo + child]) < 0) + if (child < n && + comparer.LessThan(Unsafe.Add(ref keysAtLoMinus1, child), Unsafe.Add(ref keysAtLo, child))) + { + ++child; + } + + // Below lines are equivalent to: if (!(comparer(d, keys[lo + child - 1]) < 0)) + if (!(comparer.LessThan(d, Unsafe.Add(ref keysAtLoMinus1, child)))) + break; + + // Below lines are equivalent to: keys[lo + i - 1] = keys[lo + child - 1] + Unsafe.Add(ref keysAtLoMinus1, i) = Unsafe.Add(ref keysAtLoMinus1, child); + Unsafe.Add(ref valuesAtLoMinus1, i) = Unsafe.Add(ref valuesAtLoMinus1, child); + + i = child; + } + // Below lines are equivalent to: keys[lo + i - 1] = d; + Unsafe.Add(ref keysAtLoMinus1, i) = d; + Unsafe.Add(ref valuesAtLoMinus1, i) = dValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InsertionSort( + ref TKey keys, ref TValue values, int lo, int hi, + TComparer comparer) + where TComparer : IDirectComparer + { + Debug.Assert(lo >= 0); + Debug.Assert(hi >= lo); + + for (int i = lo; i < hi; ++i) + { + int j = i; + + var t = Unsafe.Add(ref keys, j + 1); + + if (j >= lo && comparer.LessThan(t, Unsafe.Add(ref keys, j))) + { + var v = Unsafe.Add(ref values, j + 1); + do + { + Unsafe.Add(ref keys, j + 1) = Unsafe.Add(ref keys, j); + Unsafe.Add(ref values, j + 1) = Unsafe.Add(ref values, j); + --j; + } + while (j >= lo && comparer.LessThan(t, Unsafe.Add(ref keys, j))); + + Unsafe.Add(ref keys, j + 1) = t; + Unsafe.Add(ref values, j + 1) = v; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ref TKey Sort3( + ref TKey keys, ref TValue values, int i0, int i1, int i2, + TComparer comparer) + where TComparer : IDirectComparer + { + ref var r0 = ref Unsafe.Add(ref keys, i0); + ref var r1 = ref Unsafe.Add(ref keys, i1); + ref var r2 = ref Unsafe.Add(ref keys, i2); + Sort2(ref r0, ref r1, comparer, ref values, i0, i1); + Sort2(ref r0, ref r2, comparer, ref values, i0, i2); + Sort2(ref r1, ref r2, comparer, ref values, i1, i2); + return ref r1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort2( + ref TKey keys, ref TValue values, int i, int j, + TComparer comparer) + where TComparer : IDirectComparer + { + Debug.Assert(i != j); + + ref TKey a = ref Unsafe.Add(ref keys, i); + ref TKey b = ref Unsafe.Add(ref keys, j); + Sort2(ref a, ref b, comparer, ref values, i, j); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sort2( + ref TKey a, ref TKey b, TComparer comparer, + ref TValue values, int i, int j) + where TComparer : IDirectComparer + { + // This is one of the only places GreaterThan is needed + // but we need to preserve this due to bogus comparers or similar + if (comparer.GreaterThan(a, b)) + { + Swap(ref a, ref b); + Swap(ref values, i, j); + } + } + } +} diff --git a/src/System.Memory/src/System/SpanSortHelpers.KeysValues.cs b/src/System.Memory/src/System/SpanSortHelpers.KeysValues.cs new file mode 100644 index 000000000000..307e68bfb3bc --- /dev/null +++ b/src/System.Memory/src/System/SpanSortHelpers.KeysValues.cs @@ -0,0 +1,203 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#if !netstandard +using Internal.Runtime.CompilerServices; +#endif + +namespace System +{ + internal static partial class SpanSortHelpersKeysValues + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Sort(this Span keys, Span values) + { + int length = keys.Length; + if (length != values.Length) + ThrowHelper.ThrowArgumentException_ItemsMustHaveSameLength(); + if (length < 2) + return; + + // PERF: Try specialized here for optimal performance + // Code-gen is weird unless used in loop outside + if (!SpanSortHelpersKeysValues_DirectComparer.TrySortSpecialized( + ref MemoryMarshal.GetReference(keys), + ref MemoryMarshal.GetReference(values), + length)) + { + DefaultSpanSortHelper.s_default.Sort( + ref MemoryMarshal.GetReference(keys), + ref MemoryMarshal.GetReference(values), + length); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Sort( + this Span keys, Span values, TComparer comparer) + where TComparer : IComparer + { + int length = keys.Length; + if (length != values.Length) + ThrowHelper.ThrowArgumentException_ItemsMustHaveSameLength(); + if (length < 2) + return; + + DefaultSpanSortHelper.s_default.Sort( + ref MemoryMarshal.GetReference(keys), + ref MemoryMarshal.GetReference(values), + length, comparer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Sort( + this Span keys, Span values, Comparison comparison) + { + int length = keys.Length; + if (length != values.Length) + ThrowHelper.ThrowArgumentException_ItemsMustHaveSameLength(); + if (length < 2) + return; + + DefaultSpanSortHelper.s_default.Sort( + ref MemoryMarshal.GetReference(keys), + ref MemoryMarshal.GetReference(values), + length, comparison); + } + + internal static class DefaultSpanSortHelper + { + internal static readonly ISpanSortHelper s_default = CreateSortHelper(); + + private static ISpanSortHelper CreateSortHelper() + { + if (typeof(IComparable).IsAssignableFrom(typeof(TKey))) + { + // coreclr uses RuntimeTypeHandle.Allocate + var ctor = typeof(ComparableSpanSortHelper<,>) + .MakeGenericType(new Type[] { typeof(TKey), typeof(TValue) }) + .GetConstructor(Array.Empty()); + + return (ISpanSortHelper)ctor.Invoke(Array.Empty()); + } + else + { + return new SpanSortHelper(); + } + } + } + + internal interface ISpanSortHelper + { + void Sort(ref TKey keys, ref TValue values, int length); + void Sort(ref TKey keys, ref TValue values, int length, Comparison comparison); + } + + internal class SpanSortHelper : ISpanSortHelper + { + public void Sort(ref TKey keys, ref TValue values, int length) + { + SpanSortHelpersKeysValues.Sort(ref keys, ref values, length, Comparer.Default); + } + + public void Sort(ref TKey keys, ref TValue values, int length, Comparison comparison) + { + SpanSortHelpersKeysValues.Sort(ref keys, ref values, length, comparison); + } + } + + internal class ComparableSpanSortHelper + : ISpanSortHelper + where TKey : IComparable + { + public void Sort(ref TKey keys, ref TValue values, int length) + { + SpanSortHelpersKeysValues.Sort(ref keys, ref values, length); + } + + public void Sort(ref TKey keys, ref TValue values, int length, Comparison comparison) + { + SpanSortHelpersKeysValues.Sort(ref keys, ref values, length, comparison); + } + } + + + internal static class DefaultSpanSortHelper + where TComparer : IComparer + { + internal static readonly ISpanSortHelper s_default = CreateSortHelper(); + + private static ISpanSortHelper CreateSortHelper() + { + if (typeof(IComparable).IsAssignableFrom(typeof(TKey))) + { + // coreclr uses RuntimeTypeHandle.Allocate + var ctor = typeof(ComparableSpanSortHelper<,,>) + .MakeGenericType(new Type[] { typeof(TKey), typeof(TValue), typeof(TComparer) }) + .GetConstructor(Array.Empty()); + + return (ISpanSortHelper)ctor.Invoke(Array.Empty()); + } + else + { + return new SpanSortHelper(); + } + } + } + + // https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Collections/Generic/ArraySortHelper.cs + internal interface ISpanSortHelper + where TComparer : IComparer + { + void Sort(ref TKey keys, ref TValue values, int length, TComparer comparer); + } + + internal class SpanSortHelper : ISpanSortHelper + where TComparer : IComparer + { + public void Sort(ref TKey keys, ref TValue values, int length, TComparer comparer) + { + if (typeof(TComparer) == typeof(IComparer) && comparer == null) + { + SpanSortHelpersKeysValues.Sort(ref keys, ref values, length, Comparer.Default); + } + else + { + SpanSortHelpersKeysValues.Sort(ref keys, ref values, length, comparer); + } + } + } + + internal class ComparableSpanSortHelper + : ISpanSortHelper + where TKey : IComparable + where TComparer : IComparer + { + public void Sort(ref TKey keys, ref TValue values, int length, + TComparer comparer) + { + if (comparer == null || + // Cache this in generic traits helper class perhaps + (!typeof(TComparer).IsValueType && + object.ReferenceEquals(comparer, Comparer.Default))) // Or "=="? + { + if (!SpanSortHelpersKeysValues_DirectComparer.TrySortSpecialized(ref keys, ref values, length)) + { + // NOTE: For Bogus Comparable the exception message will be different, when using Comparer.Default + // Since the exception message is thrown internally without knowledge of the comparer + SpanSortHelpersKeysValues.Sort(ref keys, ref values, length); + } + } + else + { + SpanSortHelpersKeysValues.Sort(ref keys, ref values, length, comparer); + } + } + } + } +} diff --git a/src/System.Memory/src/System/ThrowHelper.cs b/src/System.Memory/src/System/ThrowHelper.cs index 1bc66984a0b9..0e319f40f31e 100644 --- a/src/System.Memory/src/System/ThrowHelper.cs +++ b/src/System.Memory/src/System/ThrowHelper.cs @@ -72,6 +72,23 @@ internal static class ThrowHelper [MethodImpl(MethodImplOptions.NoInlining)] private static Exception CreateArgumentException_OverlapAlignmentMismatch() { return new ArgumentException(SR.Argument_OverlapAlignmentMismatch); } + internal static void ThrowArgumentException_ItemsMustHaveSameLength() { throw CreateArgumentException_ItemsMustHaveSameLength(); } + [MethodImpl(MethodImplOptions.NoInlining)] + private static Exception CreateArgumentException_ItemsMustHaveSameLength() { return new ArgumentException("Items must have same length as keys."); } + + // coreclr does not have an exception for bad IComparable but instead throws with comparer == null + internal static void ThrowArgumentException_BadComparer(object comparer) { throw CreateArgumentException_BadComparer(comparer); } + [MethodImpl(MethodImplOptions.NoInlining)] + private static Exception CreateArgumentException_BadComparer(object comparer) { return new ArgumentException( + string.Format("Unable to sort because the IComparer.Compare() method returns inconsistent results. Either a value does not compare equal to itself, or one value repeatedly compared to another value yields different results. IComparer: '{0}'.", comparer)); + } + // here we throw if bad comparable, including the case when user uses Comparer.Default and TKey is IComparable + internal static void ThrowArgumentException_BadComparable(Type comparableType) { throw CreateArgumentException_BadComparable(comparableType); } + [MethodImpl(MethodImplOptions.NoInlining)] + private static Exception CreateArgumentException_BadComparable(Type comparableType) { return new ArgumentException( + string.Format("Unable to sort because the IComparable.CompareTo() method returns inconsistent results. Either a value does not compare equal to itself, or one value repeatedly compared to another value yields different results. IComparable: '{0}'.", comparableType.FullName)); + } + // // Enable use of ThrowHelper from TryFormat() routines without introducing dozens of non-code-coveraged "bytesWritten = 0; return false" boilerplate. // @@ -104,6 +121,7 @@ internal enum ExceptionArgument ownedMemory, pointer, comparable, - comparer + comparer, + comparison } } diff --git a/src/System.Memory/tests/Performance/Perf.Span.Sort.cs b/src/System.Memory/tests/Performance/Perf.Span.Sort.cs new file mode 100644 index 000000000000..18ff5c20b61b --- /dev/null +++ b/src/System.Memory/tests/Performance/Perf.Span.Sort.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Xunit.Performance; +using Xunit; + +namespace System.Memory.Tests +{ + public class Perf_Span_Sort + { + private const int InnerCountForNoSorting = 1000000; + + [Benchmark(InnerIterationCount = InnerCountForNoSorting)] + public void ArraySort_Int_Length_0() + { + int[] array = new int[0]; + BenchmarkRepeatableArray(array); + } + [Benchmark(InnerIterationCount = InnerCountForNoSorting)] + public void ArraySort_Int_Length_1() + { + int[] array = new int[1]; + BenchmarkRepeatableArray(array); + } + [Benchmark()] + [InlineData(10)] + [InlineData(100)] + [InlineData(10000)] + [InlineData(1000000)] + public void ArraySort_Int_Random(int length) + { + BenchmarkAndAssertArrayInt(length); + } + + [Benchmark(InnerIterationCount = InnerCountForNoSorting)] + public void SpanSort_Int_Length_0() + { + Span span = new int[0]; + BenchmarkRepeatableSpan(span); + } + [Benchmark(InnerIterationCount = InnerCountForNoSorting)] + public void SpanSort_Int_Length_1() + { + Span span = new int[1]; + BenchmarkRepeatableSpan(span); + } + [Benchmark()] + [InlineData(10)] + [InlineData(100)] + [InlineData(10000)] + [InlineData(1000000)] + public void SpanSort_Int_Random(int length) + { + BenchmarkAndAssertSpanInt(length); + } + + private static void BenchmarkAndAssertArrayInt(int length) + { + BenchmarkAndAssertArray(length, i => i); + } + + const int Seed = 213718398; + private static void BenchmarkAndAssertArray(int length, Func toValue) + where T : IComparable + { + var random = CreateRandomArray(length, toValue); + var work = new T[length]; + + foreach (BenchmarkIteration iteration in Benchmark.Iterations) + { + Array.Copy(random, work, random.Length); + + using (iteration.StartMeasurement()) + { + Array.Sort(work); + } + } + } + + private static void BenchmarkAndAssertSpanInt(int length) + { + BenchmarkAndAssertSpan(length, i => i); + } + + private static void BenchmarkAndAssertSpan(int length, Func toValue) + where T : IComparable + { + var random = CreateRandomArray(length, toValue); + var work = new T[length]; + Span spanWork = work; + + foreach (BenchmarkIteration iteration in Benchmark.Iterations) + { + Array.Copy(random, work, random.Length); + + using (iteration.StartMeasurement()) + { + spanWork.Sort(); + } + } + } + + private static void BenchmarkRepeatableSpan(Span span) + { + foreach (BenchmarkIteration iteration in Benchmark.Iterations) + { + using (iteration.StartMeasurement()) + { + for (int i = 0; i < Benchmark.InnerIterationCount; i++) + { + span.Sort(); + } + } + } + } + + private static void BenchmarkRepeatableArray(int[] array) + { + foreach (BenchmarkIteration iteration in Benchmark.Iterations) + { + using (iteration.StartMeasurement()) + { + for (int i = 0; i < Benchmark.InnerIterationCount; i++) + { + Array.Sort(array); + } + } + } + } + + private static T[] CreateRandomArray(int length, Func toValue) + where T : IComparable + { + var random = new Random(Seed); + var array = new T[length]; + for (int i = 0; i < array.Length; i++) + { + array[i] = toValue(random.Next()); + } + return array; + } + } +} diff --git a/src/System.Memory/tests/Performance/System.Memory.Performance.Tests.csproj b/src/System.Memory/tests/Performance/System.Memory.Performance.Tests.csproj index 6ae77fcdd3a8..32bdb22c435b 100644 --- a/src/System.Memory/tests/Performance/System.Memory.Performance.Tests.csproj +++ b/src/System.Memory/tests/Performance/System.Memory.Performance.Tests.csproj @@ -14,6 +14,7 @@ + @@ -34,4 +35,4 @@ - \ No newline at end of file + diff --git a/src/System.Memory/tests/Span/Sort.Fillers.cs b/src/System.Memory/tests/Span/Sort.Fillers.cs new file mode 100644 index 000000000000..1a523af2e62d --- /dev/null +++ b/src/System.Memory/tests/Span/Sort.Fillers.cs @@ -0,0 +1,180 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace System.SpanTests +{ + public static partial class SortSpanTests + { + public interface ISpanFiller + { + void Fill(Span span, int sliceLength, Func toValue); + } + public class ConstantSpanFiller : ISpanFiller + { + readonly int _fill; + + public ConstantSpanFiller(int fill) + { + _fill = fill; + } + + public void Fill(Span span, int sliceLength, Func toValue) + { + span.Fill(toValue(_fill)); + } + } + public class DecrementingSpanFiller : ISpanFiller + { + public void Fill(Span span, int sliceLength, Func toValue) + { + DecrementingFill(span, toValue); + } + + public static void DecrementingFill(Span span, Func toValue) + { + for (int i = 0; i < span.Length; i++) + { + span[i] = toValue(span.Length - i - 1); + } + } + } + public class ModuloDecrementingSpanFiller : ISpanFiller + { + readonly int _modulo; + + public ModuloDecrementingSpanFiller(int modulo) + { + _modulo = modulo; + } + + public void Fill(Span span, int sliceLength, Func toValue) + { + ModuloFill(span, _modulo, toValue); + } + + public static void ModuloFill(Span span, int modulo, Func toValue) + { + for (int i = 0; i < span.Length; i++) + { + int v = (span.Length - i - 1) % modulo; + span[i] = toValue(v); + } + } + } + public class IncrementingSpanFiller : ISpanFiller + { + public void Fill(Span span, int sliceLength, Func toValue) + { + IncrementingFill(span, toValue); + } + + public static void IncrementingFill(Span span, Func toValue) + { + for (int i = 0; i < span.Length; i++) + { + span[i] = toValue(i); + } + } + } + public class MedianOfThreeKillerSpanFiller : ISpanFiller + { + public void Fill(Span span, int sliceLength, Func toValue) + { + // Each slice must be median of three! + int i = 0; + for (; i < span.Length - sliceLength; i += sliceLength) + { + InitializeMedianOfThreeKiller(span.Slice(i, sliceLength), toValue); + } + // Fill remainder just to be sure + InitializeMedianOfThreeKiller(span.Slice(i, span.Length - i), toValue); + } + + public static void InitializeMedianOfThreeKiller(Span span, Func toValue) + { + var length = span.Length; + // if n is odd, set the last element to n-1, and proceed + // with n decremented by 1 + if (length % 2 != 0) + { + span[length - 1] = toValue(length); + --length; + } + var m = length / 2; + for (int i = 0; i < m; ++i) + { + // first half of array (even indices) + if (i % 2 == 0) + span[i] = toValue(i + 1); + // first half of array (odd indices) + else + span[i] = toValue(m + i + (m % 2 != 0 ? 1 : 0)); + // second half of array + span[m + i] = toValue((i + 1) * 2); + } + } + } + public class PartialRandomShuffleSpanFiller : ISpanFiller + { + readonly ISpanFiller _spanFiller; + readonly double _fractionRandomShuffles; + readonly int _seed; + + public PartialRandomShuffleSpanFiller(ISpanFiller spanFiller, double fractionRandomShuffles, int seed) + { + _spanFiller = spanFiller; + _fractionRandomShuffles = fractionRandomShuffles; + _seed = seed; + } + + public void Fill(Span span, int sliceLength, Func toValue) + { + _spanFiller.Fill(span, sliceLength, toValue); + + RandomShuffle(span, _fractionRandomShuffles); + } + + private void RandomShuffle(Span span, double fractionRandomShuffles) + { + var random = new Random(_seed); + int shuffleCount = Math.Max(0, (int)(span.Length * fractionRandomShuffles)); + for (int i = 0; i < shuffleCount; i++) + { + var a = random.Next(span.Length); + var b = random.Next(span.Length); + var temp = span[a]; + span[a] = span[b]; + span[b] = temp; + } + } + } + public class RandomSpanFiller : ISpanFiller + { + readonly int _seed; + + public RandomSpanFiller(int seed) + { + _seed = seed; + } + + public void Fill(Span span, int sliceLength, Func toValue) + { + var random = new Random(_seed); + RandomFill(random, span, toValue); + } + + public static void RandomFill(Random random, Span span, Func toValue) + { + for (int i = 0; i < span.Length; i++) + { + span[i] = toValue(random.Next()); + } + } + } + } +} diff --git a/src/System.Memory/tests/Span/Sort.Helpers.cs b/src/System.Memory/tests/Span/Sort.Helpers.cs new file mode 100644 index 000000000000..2146ccf6cfb0 --- /dev/null +++ b/src/System.Memory/tests/Span/Sort.Helpers.cs @@ -0,0 +1,658 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace System.SpanTests +{ + public static partial class SortSpanTests + { + internal static void Test_Keys_Int8(ISortCases sortCases) => + Test(sortCases, i => (sbyte)i, sbyte.MinValue); + internal static void Test_Keys_UInt8(ISortCases sortCases) => + Test(sortCases, i => (byte)i, byte.MaxValue); + internal static void Test_Keys_Int16(ISortCases sortCases) => + Test(sortCases, i => (short)i, short.MinValue); + internal static void Test_Keys_UInt16(ISortCases sortCases) => + Test(sortCases, i => (ushort)i, ushort.MaxValue); + internal static void Test_Keys_Int32(ISortCases sortCases) => + Test(sortCases, i => (int)i, int.MinValue); + internal static void Test_Keys_UInt32(ISortCases sortCases) => + Test(sortCases, i => (uint)i, uint.MaxValue); + internal static void Test_Keys_Int64(ISortCases sortCases) => + Test(sortCases, i => (long)i, long.MinValue); + internal static void Test_Keys_UInt64(ISortCases sortCases) => + Test(sortCases, i => (ulong)i, ulong.MaxValue); + internal static void Test_Keys_Single(ISortCases sortCases) => + Test(sortCases, i => (float)i, float.NaN); + internal static void Test_Keys_Double(ISortCases sortCases) => + Test(sortCases, i => (double)i, double.NaN); + internal static void Test_Keys_Boolean(ISortCases sortCases) => + Test(sortCases, i => i % 2 == 0, false); + internal static void Test_Keys_Char(ISortCases sortCases) => + Test(sortCases, i => (char)i, char.MaxValue); + internal static void Test_Keys_String(ISortCases sortCases) => + Test(sortCases, i => i.ToString("D9"), null); + internal static void Test_Keys_ComparableStructInt32(ISortCases sortCases) => + Test(sortCases, i => new ComparableStructInt32(i), new ComparableStructInt32(int.MinValue)); + internal static void Test_Keys_ComparableClassInt32(ISortCases sortCases) => + Test(sortCases, i => new ComparableClassInt32(i), null); + internal static void Test_Keys_BogusComparable(ISortCases sortCases) => + Test(sortCases, i => new BogusComparable(i), null); + + internal static void Test(ISortCases sortCase, Func toKey, TKey specialKey) + where TKey : IComparable + { + foreach (var unsorted in sortCase.EnumerateTests(toKey, specialKey)) + { + TestSortOverloads(unsorted); + } + } + internal static void TestSortOverloads(ArraySegment keys) + where TKey : IComparable + { + var copy = (TKey[])keys.Array.Clone(); + + TestSort(keys); + TestSort(keys, Comparer.Default); + TestSort(keys, Comparer.Default.Compare); + TestSort(keys, new CustomComparer()); + TestSort(keys, (IComparer)null); + TestSort(keys, new BogusComparer()); + } + internal static void TestSort( + ArraySegment keysToSort) + where TKey : IComparable + { + var expected = new ArraySegment((TKey[])keysToSort.Array.Clone(), + keysToSort.Offset, keysToSort.Count); + + var expectedException = RunAndCatchException(() => + Array.Sort(expected.Array, expected.Offset, expected.Count)); + + var actualException = RunAndCatchException(() => + { + Span keysSpan = keysToSort; + keysSpan.Sort(); + }); + + AssertExceptionEquals(expectedException, actualException); + // We assert the full arrays are as expected, to check for possible under/overflow + Assert.Equal(expected.Array, keysToSort.Array); + } + + internal static void TestSort( + ArraySegment keysToSort, + TComparer comparer) + where TComparer : IComparer + { + var expected = new ArraySegment((TKey[])keysToSort.Array.Clone(), + keysToSort.Offset, keysToSort.Count); + + var expectedException = RunAndCatchException(() => + Array.Sort(expected.Array, expected.Offset, expected.Count, comparer)); + + var actualException = RunAndCatchException(() => + { + Span keysSpan = keysToSort; + keysSpan.Sort(comparer); + }); + + AssertExceptionEquals(expectedException, actualException); + + // We assert the full arrays are as expected, to check for possible under/overflow + Assert.Equal(expected.Array, keysToSort.Array); + } + internal static void TestSort( + ArraySegment keysToSort, + Comparison comparison) + { + var expected = new ArraySegment((TKey[])keysToSort.Array.Clone(), + keysToSort.Offset, keysToSort.Count); + // Array.Sort doesn't have a comparison version for segments + Exception expectedException = null; + if (expected.Offset == 0 && expected.Count == expected.Array.Length) + { + expectedException = RunAndCatchException(() => + Array.Sort(expected.Array, comparison)); + } + else + { + expectedException = RunAndCatchException(() => + Array.Sort(expected.Array, expected.Offset, expected.Count, + new ComparisonComparer(comparison))); + } + + var actualException = RunAndCatchException(() => + { + Span keysSpan = keysToSort; + keysSpan.Sort(comparison); + }); + + AssertExceptionEquals(expectedException, actualException); + // We assert the full arrays are as expected, to check for possible under/overflow + Assert.Equal(expected.Array, keysToSort.Array); + } + + internal static void Test_KeysValues_Int8_Int32(ISortCases sortCases) => + Test(sortCases, i => (sbyte)i, sbyte.MinValue, i => i); + internal static void Test_KeysValues_UInt8_Int32(ISortCases sortCases) => + Test(sortCases, i => (byte)i, byte.MaxValue, i => i); + internal static void Test_KeysValues_Int16_Int32(ISortCases sortCases) => + Test(sortCases, i => (short)i, short.MinValue, i => i); + internal static void Test_KeysValues_UInt16_Int32(ISortCases sortCases) => + Test(sortCases, i => (ushort)i, ushort.MaxValue, i => i); + internal static void Test_KeysValues_Int32_Int32(ISortCases sortCases) => + Test(sortCases, i => (int)i, int.MinValue, i => i); + internal static void Test_KeysValues_UInt32_Int32(ISortCases sortCases) => + Test(sortCases, i => (uint)i, uint.MaxValue, i => i); + internal static void Test_KeysValues_Int64_Int32(ISortCases sortCases) => + Test(sortCases, i => (long)i, long.MinValue, i => i); + internal static void Test_KeysValues_UInt64_Int32(ISortCases sortCases) => + Test(sortCases, i => (ulong)i, ulong.MaxValue, i => i); + internal static void Test_KeysValues_Single_Int32(ISortCases sortCases) => + Test(sortCases, i => (float)i, float.NaN, i => i); + internal static void Test_KeysValues_Double_Int32(ISortCases sortCases) => + Test(sortCases, i => (double)i, double.NaN, i => i); + internal static void Test_KeysValues_Boolean_Int32(ISortCases sortCases) => + Test(sortCases, i => i % 2 == 0, false, i => i); + internal static void Test_KeysValues_Char_Int32(ISortCases sortCases) => + Test(sortCases, i => (char)i, char.MaxValue, i => i); + internal static void Test_KeysValues_String_Int32(ISortCases sortCases) => + Test(sortCases, i => i.ToString("D9"), null, i => i); + internal static void Test_KeysValues_ComparableStructInt32_Int32(ISortCases sortCases) => + Test(sortCases, i => new ComparableStructInt32(i), new ComparableStructInt32(int.MinValue), i => i); + internal static void Test_KeysValues_ComparableClassInt32_Int32(ISortCases sortCases) => + Test(sortCases, i => new ComparableClassInt32(i), null, i => i); + internal static void Test_KeysValues_BogusComparable_Int32(ISortCases sortCases) => + Test(sortCases, i => new BogusComparable(i), null, i => i); + + internal static void Test(ISortCases sortCase, + Func toKey, TKey specialKey, Func toValue) + where TKey : IComparable + { + foreach (var unsortedKeys in sortCase.EnumerateTests(toKey, specialKey)) + { + var length = unsortedKeys.Array.Length; + var values = new TValue[length]; + // Items are always based on "unique" int values + new IncrementingSpanFiller().Fill(values, length, toValue); + var unsortedValues = new ArraySegment(values, unsortedKeys.Offset, unsortedKeys.Count); + TestSortOverloads(unsortedKeys, unsortedValues); + } + } + internal static void TestSortOverloads(ArraySegment keys, ArraySegment values) + where TKey : IComparable + { + var copy = (TKey[])keys.Array.Clone(); + + TestSort(keys, values); + TestSort(keys, values, Comparer.Default); + TestSort(keys, values, Comparer.Default.Compare); + TestSort(keys, values, new CustomComparer()); + TestSort(keys, values, (IComparer)null); + TestSort(keys, values, new BogusComparer()); + } + internal static void TestSort( + ArraySegment keysToSort, ArraySegment valuesToSort) + where TKey : IComparable + { + var expectedKeys = new ArraySegment((TKey[])keysToSort.Array.Clone(), + keysToSort.Offset, keysToSort.Count); + var expectedValues = new ArraySegment((TValue[])valuesToSort.Array.Clone(), + valuesToSort.Offset, valuesToSort.Count); + Assert.Equal(expectedKeys.Offset, expectedValues.Offset); + Assert.Equal(expectedKeys.Count, expectedValues.Count); + + var expectedException = RunAndCatchException(() => + { + if (expectedKeys.Offset == 0 && expectedKeys.Count == expectedKeys.Array.Length) + { + Array.Sort(expectedKeys.Array, expectedValues.Array, expectedKeys.Offset, expectedKeys.Count); + } + else + { + // HACK: To avoid the fact that .net core Array.Sort still computes + // the depth limit incorrectly, see https://github.com/dotnet/coreclr/pull/16002 + // This can result in Array.Sort NOT calling HeapSort when Span does. + // And then values for identical keys may be sorted differently. + Span ks = expectedKeys; + Span vs = expectedValues; + var noSegmentKeys = ks.ToArray(); + var noSegmentValues = vs.ToArray(); + try + { + Array.Sort(noSegmentKeys, noSegmentValues); + } + finally + { + new Span(noSegmentKeys).CopyTo(ks); + new Span(noSegmentValues).CopyTo(vs); + } + } + }); + + var actualException = RunAndCatchException(() => + { + Span keysSpan = keysToSort; + Span valuesSpan = valuesToSort; + keysSpan.Sort(valuesSpan); + }); + + AssertExceptionEquals(expectedException, actualException); + // We assert the full arrays are as expected, to check for possible under/overflow + Assert.Equal(expectedKeys.Array, keysToSort.Array); + Assert.Equal(expectedValues.Array, valuesToSort.Array); + } + internal static void TestSort( + ArraySegment keysToSort, ArraySegment valuesToSort, + TComparer comparer) + where TComparer : IComparer + { + var expectedKeys = new ArraySegment((TKey[])keysToSort.Array.Clone(), + keysToSort.Offset, keysToSort.Count); + var expectedValues = new ArraySegment((TValue[])valuesToSort.Array.Clone(), + valuesToSort.Offset, valuesToSort.Count); + + var expectedException = RunAndCatchException(() => + Array.Sort(expectedKeys.Array, expectedValues.Array, expectedKeys.Offset, expectedKeys.Count, comparer)); + + var actualException = RunAndCatchException(() => + { + Span keysSpan = keysToSort; + Span valuesSpan = valuesToSort; + keysSpan.Sort(valuesSpan, comparer); + }); + + AssertExceptionEquals(expectedException, actualException); + // We assert the full arrays are as expected, to check for possible under/overflow + Assert.Equal(expectedKeys.Array, keysToSort.Array); + Assert.Equal(expectedValues.Array, valuesToSort.Array); + } + internal static void TestSort( + ArraySegment keysToSort, ArraySegment valuesToSort, + Comparison comparison) + { + var expectedKeys = new ArraySegment((TKey[])keysToSort.Array.Clone(), + keysToSort.Offset, keysToSort.Count); + var expectedValues = new ArraySegment((TValue[])valuesToSort.Array.Clone(), + valuesToSort.Offset, valuesToSort.Count); + // Array.Sort doesn't have a comparison version for segments + var expectedException = RunAndCatchException(() => + Array.Sort(expectedKeys.Array, expectedValues.Array, expectedKeys.Offset, expectedKeys.Count, new ComparisonComparer(comparison))); + + var actualException = RunAndCatchException(() => + { + Span keysSpan = keysToSort; + Span valuesSpan = valuesToSort; + keysSpan.Sort(valuesSpan, comparison); + }); + + AssertExceptionEquals(expectedException, actualException); + // We assert the full arrays are as expected, to check for possible under/overflow + Assert.Equal(expectedKeys.Array, keysToSort.Array); + Assert.Equal(expectedValues.Array, valuesToSort.Array); + } + + public interface ISortCases + { + IEnumerable> EnumerateTests(Func toKey, TKey specialKey); + } + public class FillerSortCases : ISortCases + { + public FillerSortCases(int maxLength, ISpanFiller filler) + { + MaxLength = maxLength; + Filler = filler ?? throw new ArgumentNullException(nameof(filler)); + } + + public int MinLength => 2; + public int MaxLength { get; } + public ISpanFiller Filler { get; } + + public IEnumerable> EnumerateTests(Func toKey, TKey specialKey) + { + for (int length = MinLength; length <= MaxLength; length++) + { + var unsorted = new TKey[length]; + Filler.Fill(unsorted, length, toKey); + yield return new ArraySegment(unsorted); + } + } + + public override string ToString() + { + return $"Lengths [{MinLength}, {MaxLength,4}] {nameof(Filler)}={Filler.GetType().Name.Replace("SpanFiller", "")} "; + } + } + public class LengthZeroSortCases : ISortCases + { + public IEnumerable> EnumerateTests(Func toKey, TKey specialKey) + { + yield return new ArraySegment(Array.Empty()); + } + + public override string ToString() + => GetType().Name.Replace(nameof(ISortCases).Remove(0, 1), string.Empty); + } + public class LengthOneSortCases : ISortCases + { + public IEnumerable> EnumerateTests(Func toKey, TKey specialKey) + { + yield return new ArraySegment(new[] { toKey(-1) }); + } + + public override string ToString() + => GetType().Name.Replace(nameof(ISortCases).Remove(0, 1), string.Empty); + } + public class AllLengthTwoSortCases : ISortCases + { + public IEnumerable> EnumerateTests(Func toKey, TKey specialKey) + { + const int length = 2; + for (int i = 0; i < length; i++) + { + for (int j = 0; j < length; j++) + { + yield return new ArraySegment(new[] { toKey(i), toKey(j) }); + } + } + } + + public override string ToString() + => GetType().Name.Replace(nameof(ISortCases).Remove(0, 1), string.Empty); + } + public class AllLengthThreeSortCases : ISortCases + { + public IEnumerable> EnumerateTests(Func toKey, TKey specialKey) + { + const int length = 3; + for (int i = 0; i < length; i++) + { + for (int j = 0; j < length; j++) + { + for (int k = 0; k < length; k++) + { + yield return new ArraySegment(new[] { toKey(i), toKey(j), toKey(k) }); + } + } + } + } + + public override string ToString() + => GetType().Name.Replace(nameof(ISortCases).Remove(0, 1), string.Empty); + } + public class AllLengthFourSortCases : ISortCases + { + public IEnumerable> EnumerateTests(Func toKey, TKey specialKey) + { + const int length = 4; + for (int i = 0; i < length; i++) + { + for (int j = 0; j < length; j++) + { + for (int k = 0; k < length; k++) + { + for (int l = 0; l < length; l++) + { + yield return new ArraySegment(new[] { toKey(i), toKey(j), toKey(k), toKey(l) }); + } + } + } + } + } + + public override string ToString() + => GetType().Name.Replace(nameof(ISortCases).Remove(0, 1), string.Empty); + } + public class PadAndSliceSortCases : ISortCases + { + readonly ISortCases _sortCases; + readonly int _slicePadding; + + public PadAndSliceSortCases(ISortCases sortCases, int slicePadding) + { + _sortCases = sortCases ?? throw new ArgumentNullException(nameof(sortCases)); + _slicePadding = slicePadding; + } + + public IEnumerable> EnumerateTests(Func toKey, TKey specialKey) + { + return _sortCases.EnumerateTests(toKey, specialKey).Select(ks => + { + var newKeys = new TKey[ks.Count + 2 * _slicePadding]; + Array.Copy(ks.Array, ks.Offset, newKeys, _slicePadding, ks.Count); + var padKey = toKey(unchecked((int)0xCECECECE)); + for (int i = 0; i < _slicePadding; i++) + { + newKeys[i] = padKey; + newKeys[newKeys.Length - i - 1] = padKey; + } + return new ArraySegment(newKeys, _slicePadding, ks.Count); + }); + } + + public override string ToString() + => GetType().Name.Replace(nameof(ISortCases).Remove(0, 1), string.Empty) + + $":{_slicePadding} " + _sortCases.ToString(); + } + public class StepwiseSpecialSortCases : ISortCases + { + readonly ISortCases _sortCases; + readonly int _step; + + public StepwiseSpecialSortCases(ISortCases sortCases, int step) + { + _sortCases = sortCases ?? throw new ArgumentNullException(nameof(sortCases)); + _step = step; + } + + public IEnumerable> EnumerateTests(Func toKey, TKey specialKey) + { + return _sortCases.EnumerateTests(toKey, specialKey).Select(ks => + { + for (int i = 0; i < ks.Count; i += _step) + { + ks.Array[i + ks.Offset] = specialKey; + } + return ks; + }); + } + + public override string ToString() + => GetType().Name.Replace(nameof(ISortCases).Remove(0, 1), string.Empty) + + $":{_step} " + _sortCases.ToString(); + } + + + internal struct CustomComparer : IComparer + where TKey : IComparable + { + public int Compare(TKey x, TKey y) => object.ReferenceEquals(x, y) ? 0 : (x != null ? x.CompareTo(y) : -1); + } + + internal struct StructCustomComparer : IComparer + where TKey : struct, IComparable + { + public int Compare(TKey x, TKey y) => x.CompareTo(y); + } + + internal struct BogusComparer : IComparer + where TKey : IComparable + { + public int Compare(TKey x, TKey y) => 1; // Always greater + } + + public struct ComparableStructInt32 : IComparable + { + public readonly int Value; + + public ComparableStructInt32(int value) + { + Value = value; + } + + public int CompareTo(ComparableStructInt32 other) + { + return this.Value.CompareTo(other.Value); + } + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => $"ComparableStruct {Value}"; + } + + public class ComparableClassInt32 : IComparable + { + public readonly int Value; + + public ComparableClassInt32(int value) + { + Value = value; + } + + public int CompareTo(ComparableClassInt32 other) + { + return other != null ? Value.CompareTo(other.Value) : 1; + } + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => $"ComparableClass {Value}"; + } + + public class BogusComparable + : IComparable + , IEquatable + { + public readonly int Value; + + public BogusComparable(int value) + { + Value = value; + } + + public int CompareTo(BogusComparable other) => 1; + + public bool Equals(BogusComparable other) + { + if (other == null) + return false; + return Value.Equals(other.Value); + } + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => $"Bogus {Value}"; + } + + public struct ValueIdStruct : IComparable, IEquatable + { + public ValueIdStruct(int value, int identity) + { + Value = value; + Id = identity; + } + + public int Value { get; } + public int Id { get; } + + // Sort by value + public int CompareTo(ValueIdStruct other) => + Value.CompareTo(other.Value); + + // Check equality by both + public bool Equals(ValueIdStruct other) => + Value.Equals(other.Value) && Id.Equals(other.Id); + + public override bool Equals(object obj) + { + if (obj is ValueIdStruct) + { + return Equals((ValueIdStruct)obj); + } + return false; + } + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => $"{Value} Id:{Id}"; + } + + public class ValueIdClass : IComparable, IEquatable + { + public ValueIdClass(int value, int identity) + { + Value = value; + Id = identity; + } + + public int Value { get; } + public int Id { get; } + + // Sort by value + public int CompareTo(ValueIdClass other) => + Value.CompareTo(other.Value); + + // Check equality by both + public bool Equals(ValueIdClass other) => + other != null && Value.Equals(other.Value) && Id.Equals(other.Id); + + public override bool Equals(object obj) + { + return Equals(obj as ValueIdClass); + } + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => $"{Value} Id:{Id}"; + } + + // Used for array sort + internal class ComparisonComparer : IComparer + { + readonly Comparison _comparison; + + public ComparisonComparer(Comparison comparison) + { + _comparison = comparison; + } + + public int Compare(TKey x, TKey y) => _comparison(x, y); + } + + + internal static Exception RunAndCatchException(Action sort) + { + try + { + sort(); + } + catch (Exception e) + { + return e; + } + return null; + } + + internal static void AssertExceptionEquals(Exception expectedException, Exception actualException) + { + if (expectedException != null) + { + Assert.IsType(expectedException.GetType(), actualException); + if (expectedException.Message != actualException.Message) + { + Assert.StartsWith("Unable to sort because the IComparable.CompareTo() method returns inconsistent results. Either a value does not compare equal to itself, or one value repeatedly compared to another value yields different results. IComparable: '", actualException.Message); + Assert.EndsWith("'.", actualException.Message); + } + } + else + { + Assert.Null(actualException); + } + } + } +} diff --git a/src/System.Memory/tests/Span/Sort.cs b/src/System.Memory/tests/Span/Sort.cs new file mode 100644 index 000000000000..c93dddaa1197 --- /dev/null +++ b/src/System.Memory/tests/Span/Sort.cs @@ -0,0 +1,815 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using Xunit; +using static System.SpanTests.SortSpanTests; + +namespace System.SpanTests +{ + public static partial class SpanTests + { + // Existing coreclr tests seem to be in here: + // https://github.com/dotnet/coreclr/tree/master/tests/src/CoreMangLib/cti/system/array + // E.g. arraysort1.cs etc. + // These have not been used for the tests below, + // instead all tests are based on using Array.Sort for computing the expected. + + const string SortTrait = nameof(SortTrait); + const string SortTraitValue = nameof(SortTraitValue); + + const int FastMaxLength = 50; + const int SlowMaxLength = 512; + public static readonly TheoryData s_fastSortTests = CreateSortCases(FastMaxLength); + public static readonly TheoryData s_slowSortTests = CreateSortCases(SlowMaxLength); + + static TheoryData CreateSortCases(int maxLength) + { + var cases = new ISortCases[] + { + new LengthZeroSortCases(), + new LengthOneSortCases(), + new AllLengthTwoSortCases(), + new AllLengthThreeSortCases(), + new AllLengthFourSortCases(), + new FillerSortCases(maxLength, new ConstantSpanFiller(42) ), + new FillerSortCases(maxLength, new DecrementingSpanFiller() ), + new FillerSortCases(maxLength, new ModuloDecrementingSpanFiller(25) ), + new FillerSortCases(maxLength, new ModuloDecrementingSpanFiller(256) ), + new FillerSortCases(maxLength, new IncrementingSpanFiller() ), + new FillerSortCases(maxLength, new MedianOfThreeKillerSpanFiller() ), + new FillerSortCases(maxLength, new PartialRandomShuffleSpanFiller(new IncrementingSpanFiller(), 0.2, 16281) ), + new FillerSortCases(maxLength, new RandomSpanFiller(1873318) ), + }; + var allCases = cases.Concat(cases.Select(c => new PadAndSliceSortCases(c, 2))) + .Concat(cases.Select(c => new StepwiseSpecialSortCases(c, 3))); + + var theoryData = new TheoryData(); + foreach (var c in allCases) + { + theoryData.Add(c); + } + return theoryData; + } + + // How do we create a not comparable? I.e. something Comparer.Default fails on? + //struct NotComparable { int i; string s; IntPtr p; } + //[Fact] + //[Trait(SortTrait, SortTraitValue)] + //public static void Sort_NotComparableThrows() + //{ + // var comparer = Comparer.Default; + // Assert.Throws(() => new Span(new NotComparable[16]) + // .Sort()); + // Assert.Throws(() => new Span(new NotComparable[16]) + // .Sort(comparer)); + //} + + + [Fact] + [Trait(SortTrait, SortTraitValue)] + public static void Sort_NullComparerDoesNotThrow() + { + new Span(new int[] { 3 }).Sort((IComparer)null); + } + + [Fact] + [Trait(SortTrait, SortTraitValue)] + public static void Sort_NullComparisonThrows() + { + Assert.Throws(() => new Span(new int[] { }).Sort((Comparison)null)); + Assert.Throws(() => new Span(new string[] { }).Sort((Comparison)null)); + } + + [Fact] + [Trait(SortTrait, SortTraitValue)] + public static void Sort_KeysValues_UInt8_Int32_PatternWithRepeatedKeys_ArraySort_DifferentOutputs() + { + var keys = new byte[]{ 237, 236, 235, 234, 233, 232, 231, 230, 229, 228, 227, 226, 225, 224, 223, 222, 221, 220, 219, 218, 217, 216, 215, 214, 213, 212, 211, 210, 209, 208, 207, 206, 205, 204, 203, 202, 201, 200, 199, 198, 197, 196, 195, 194, 193, 192, 191, 190, 189, 188, 187, 186, 185, 184, 183, 182, 181, 180, 179, 178, 177, 176, 175, 174, 173, 172, 171, 170, 169, 168, 167, 166, 165, 164, 163, 162, 161, 160, 159, 158, 157, 156, 155, 154, 153, 152, 151, 150, 149, 148, 147, 146, 145, 144, 143, 142, 141, 140, 139, 138, 137, 136, 135, 134, 133, 132, 131, 130, 129, 128, 127, 126, 125, 124, 123, 122, 121, 120, 119, 118, 117, 116, 115, 114, 113, 112, 111, 110, 109, 108, 107, 106, 105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16 + , 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240, 239, 238, 237, 236, 235, 234, 233, 232, 231, 230, 229, 228, 227, 226, 225, 224, 223, 222, 221, 220, 219, 218, 217, 216, 215, 214, 213, 212, 211, 210, 209, 208, 207, 206, 205, 204, 203, 202, 201, 200, 199, 198, 197, 196, 195, 194, 193, 192, 191, 190, 189, 188, 187, 186, 185, 184, 183, 182, 181, 180, 179, 178, 177, 176, 175, 174, 173, 172, 171, 170, 169, 168, 167, 166, 165, 164, 163, 162, 161, 160, 159, 158, 157, 156, 155, 154, 153, 152, 151, 150, 149, 148, 147, 146, 145, 144, 143, 142, 141, 140, 139, 138, 137, 136, 135, 134, 133, 132, 131, 130, 129, 128, 127, 126, 125, 124, 123, 122, 121, 120, 119, 118, 117, 116, 115, 114, 113, 112, 111, 110, 109, 108, 107, 106, 105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 5, + 2, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }; + var values = Enumerable.Range(0, keys.Length).ToArray(); + + var arraySortKeysNoComparer = (byte[])keys.Clone(); + var arraySortValuesNoComparer = (int[])values.Clone(); + Array.Sort(arraySortKeysNoComparer, arraySortValuesNoComparer); + var arraySortKeysComparer = (byte[])keys.Clone(); + var arraySortValuesComparer = (int[])values.Clone(); + Array.Sort(arraySortKeysComparer, arraySortValuesComparer, new StructCustomComparer()); + // Keys are the same + Assert.Equal(arraySortKeysNoComparer, arraySortKeysComparer); + // Values are **not** the same, for same keys they are sometimes swapped + // NOTE: Commented out so test passes, but uncomment to see the difference + //Assert.Equal(arraySortValuesNoComparer, arraySortValuesComparer); + + // The problem only seems to occur when HeapSort is used, so length has to be a certain minimum size + // Although the depth limit of course is dynamic, but we need to be bigger than some multiple of 16 due to insertion sort + + var keysSegment = new ArraySegment(keys); + var valuesSegment = new ArraySegment(values); + TestSort(keysSegment, valuesSegment); // Array.Sort gives a different result here, than the other two, specifically for two equal keys, the values are swapped + TestSort(keysSegment, valuesSegment, new CustomComparer()); + TestSort(keysSegment, valuesSegment, new StructCustomComparer()); + } + + [Fact] + [Trait(SortTrait, SortTraitValue)] + public static void Sort_KeysValues_UInt8_Int32_PatternWithRepeatedKeys_ArraySort_DifferentOutputs_02() + { + var keys = new byte[] { 206, 206, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240, 239, 238, 237, 236, 235, 234, 233, 232, 231, 230, 229, 228, 227, 226, 225, 224, 223, 222, 221, 220, 219, 218, 217, 216, 215, 214, 213, 212, 211, 210, 209, 208, 207, 206, 205, 204, 203, 202, 201, 200, 199, 198, 197, 196, 195, 194, 193, 192, 191, 190, 189, 188, 187, 186, 185, 184, 183, 182, 181, 180, 179, 178, 177, 176, 175, 174, 173, 172, 171, 170, 169, 168, 167, 166, 165, 164, 163, 162, 161, 160, 159, 158, 157, 156, 155, 154, 153, 152, 151, 150, 149, 148, 147, 146, 145, 144, 143, 142, 141, 140, 139, 138, 137, 136, 135, 134, 133, 132, 131, 130, 129, 128, 127, 126, 125, 124, 123, 122, 121, 120, 119, 118, 117, 116, 115, 114, 113, 112, 111, 110, 109, 108, 107, 106, 105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, + 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240, 239, 238, 237, 236, 235, 234, 233, 232, 231, 230, 229, 228, 227, 226, 225, 224, 223, 222, 221, 220, 219, 218, 217, 216, 215, 214, 213, 212, 211, 210, 209, 208, 207, 206, 205, 204, 203, 202, 201, 200, 199, 198, 197, 196, 195, 194, 193, 192, 191, 190, 189, 188, 187, 186, 185, 184, 183, 182, 181, 180, 179, 178, 177, 176, 175, 174, 173, 172, 171, 170, 169, 168, 167, 166, 165, 164, 163, 162, 161, 160, 159, 158, 157, 156, 155, 154, 153, 152, 151, 150, 149, 148, 147, 146, 145, 144, 143, 142, 141, 140, 139, 138, 137, 136, 135, 134, 133, 132, 131, 130, 129, 128, 127, 126, 125, 124, 123, 122, 121, 120, 119, 118, 117, 116, 115, 114, 113, 112, 111, 110, 109, 108, 107, 106, 105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76 + , 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 206, 206 }; + var values = Enumerable.Range(0, keys.Length).ToArray(); + var offset = 2; + var count = 511; + + var keysSegment = new ArraySegment(keys, offset, count); + var valuesSegment = new ArraySegment(values, offset, count); + // Array.Sort gives a different result here, this is due to difference in depth limit, and hence Span calls HeapSort, Array does not + // HACK: In this method to ensure Array.Sort is never called with an actual segment and thus this test passes + TestSort(keysSegment, valuesSegment); + } + + [Fact] + [Trait(SortTrait, SortTraitValue)] + public static void Sort_KeysValues_UInt8_Int32_PatternWithRepeatedKeys_ArraySort_DifferentOutputs_02_NoOffsets() + { + var keys = new byte[] { 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240, 239, 238, 237, 236, 235, 234, 233, 232, 231, 230, 229, 228, 227, 226, 225, 224, 223, 222, 221, 220, 219, 218, 217, 216, 215, 214, 213, 212, 211, 210, 209, 208, 207, 206, 205, 204, 203, 202, 201, 200, 199, 198, 197, 196, 195, 194, 193, 192, 191, 190, 189, 188, 187, 186, 185, 184, 183, 182, 181, 180, 179, 178, 177, 176, 175, 174, 173, 172, 171, 170, 169, 168, 167, 166, 165, 164, 163, 162, 161, 160, 159, 158, 157, 156, 155, 154, 153, 152, 151, 150, 149, 148, 147, 146, 145, 144, 143, 142, 141, 140, 139, 138, 137, 136, 135, 134, 133, 132, 131, 130, 129, 128, 127, 126, 125, 124, 123, 122, 121, 120, 119, 118, 117, 116, 115, 114, 113, 112, 111, 110, 109, 108, 107, 106, 105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, + 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240, 239, 238, 237, 236, 235, 234, 233, 232, 231, 230, 229, 228, 227, 226, 225, 224, 223, 222, 221, 220, 219, 218, 217, 216, 215, 214, 213, 212, 211, 210, 209, 208, 207, 206, 205, 204, 203, 202, 201, 200, 199, 198, 197, 196, 195, 194, 193, 192, 191, 190, 189, 188, 187, 186, 185, 184, 183, 182, 181, 180, 179, 178, 177, 176, 175, 174, 173, 172, 171, 170, 169, 168, 167, 166, 165, 164, 163, 162, 161, 160, 159, 158, 157, 156, 155, 154, 153, 152, 151, 150, 149, 148, 147, 146, 145, 144, 143, 142, 141, 140, 139, 138, 137, 136, 135, 134, 133, 132, 131, 130, 129, 128, 127, 126, 125, 124, 123, 122, 121, 120, 119, 118, 117, 116, 115, 114, 113, 112, 111, 110, 109, 108, 107, 106, 105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76 + , 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }; + var values = Enumerable.Range(0, keys.Length).ToArray(); + var offset = 0; + var count = keys.Length; + + var keysSegment = new ArraySegment(keys, offset, count); + var valuesSegment = new ArraySegment(values, offset, count); + // Array.Sort gives a different result here, this is due to difference in depth limit, and hence Span calls HeapSort, Array does not + TestSort(keysSegment, valuesSegment); + } + + [Fact] + [Trait(SortTrait, SortTraitValue)] + public static void Sort_KeysValues_BogusComparable_Int32_PatternWithRepeatedKeys_ArraySort_DifferentOutputs() + { + var keysInt32 = new int[] { -825307442, -825307442, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, -825307442, -825307442 }; + var keys = keysInt32.Select(k => new BogusComparable(k)).ToArray(); + var values = Enumerable.Range(0, keys.Length).ToArray(); + var offset = 2; + var count = 17; + + var keysSegment = new ArraySegment(keys, offset, count); + var valuesSegment = new ArraySegment(values, offset, count); + // Array.Sort gives a different result here, this is due to difference in depth limit, and hence Span calls HeapSort, Array does not + + TestSort(keysSegment, valuesSegment); + } + + [Fact] + [Trait(SortTrait, SortTraitValue)] + public static void Sort_Int32_BogusComparer() + { + TestSort(new ArraySegment(new int[] { 0, 1 }), new BogusComparer()); + } + [Fact] + [Trait(SortTrait, SortTraitValue)] + public static void Sort_Keys_BogusComparable_ConstantPattern() + { + var s = new ArraySegment(Enumerable.Range(0, 17).Select(i => new BogusComparable(42)).ToArray()); + TestSort(s); + } + + + [Fact] + [Trait(SortTrait, SortTraitValue)] + public static void Sort_KeysValues_Single_Single_NaN() + { + TestSort(new ArraySegment(new[] { float.NaN, 0f, 0f, float.NaN }), + new ArraySegment(new[] { 1f, 2f, 3f, 4f })); + } + [Fact] + [Trait(SortTrait, SortTraitValue)] + public static void Sort_KeysValues_Double_Double_NaN() + { + TestSort(new ArraySegment(new[] { double.NaN, 0.0, 0.0, double.NaN }), + new ArraySegment(new[] { 1d, 2d, 3d, 4d })); + } + [Fact] + [Trait(SortTrait, SortTraitValue)] + public static void Sort_KeysValues_Single_Int32_NaN() + { + TestSort(new ArraySegment(new[] { float.NaN, 0f, 0f, float.NaN }), + new ArraySegment(new[] { 1, 2, 3, 4 })); + // Array.Sort only uses NaNPrePass when both key and value are float + // Array.Sort outputs: double.NaN, double.NaN, 0, 0, + // 1, 4, 2, 3 + } + [Fact] + [Trait(SortTrait, SortTraitValue)] + public static void Sort_KeysValues_Double_Int32_NaN() + { + TestSort(new ArraySegment(new[] { double.NaN, 0.0, 0.0, double.NaN }), + new ArraySegment(new[] { 1, 2, 3, 4 })); + // Array.Sort only uses NaNPrePass when both key and value are double + // Array.Sort outputs: double.NaN, double.NaN, 0, 0, + // 1, 4, 2, 3 + } + + #region Keys Tests + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_Int8(ISortCases sortCases) + { + Test_Keys_Int8(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_UInt8(ISortCases sortCases) + { + Test_Keys_UInt8(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_Int16(ISortCases sortCases) + { + Test_Keys_Int16(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_UInt16(ISortCases sortCases) + { + Test_Keys_UInt16(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_Int32(ISortCases sortCases) + { + Test_Keys_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_UInt32(ISortCases sortCases) + { + Test_Keys_UInt32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_Int64(ISortCases sortCases) + { + Test_Keys_Int64(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_UInt64(ISortCases sortCases) + { + Test_Keys_UInt64(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_Single(ISortCases sortCases) + { + Test_Keys_Single(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_Double(ISortCases sortCases) + { + Test_Keys_Double(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_Boolean(ISortCases sortCases) + { + Test_Keys_Boolean(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_Char(ISortCases sortCases) + { + Test_Keys_Char(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_String(ISortCases sortCases) + { + Test_Keys_String(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_ComparableStructInt32(ISortCases sortCases) + { + Test_Keys_ComparableStructInt32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_ComparableClassInt32(ISortCases sortCases) + { + Test_Keys_ComparableClassInt32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_Keys_BogusComparable(ISortCases sortCases) + { + Test_Keys_BogusComparable(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_Int8_OuterLoop(ISortCases sortCases) + { + Test_Keys_Int8(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_UInt8_OuterLoop(ISortCases sortCases) + { + Test_Keys_UInt8(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_Int16_OuterLoop(ISortCases sortCases) + { + Test_Keys_Int16(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_UInt16_OuterLoop(ISortCases sortCases) + { + Test_Keys_UInt16(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_Int32_OuterLoop(ISortCases sortCases) + { + Test_Keys_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_UInt32_OuterLoop(ISortCases sortCases) + { + Test_Keys_UInt32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_Int64_OuterLoop(ISortCases sortCases) + { + Test_Keys_Int64(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_UInt64_OuterLoop(ISortCases sortCases) + { + Test_Keys_UInt64(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_Single_OuterLoop(ISortCases sortCases) + { + Test_Keys_Single(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_Double_OuterLoop(ISortCases sortCases) + { + Test_Keys_Double(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_Boolean_OuterLoop(ISortCases sortCases) + { + Test_Keys_Boolean(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_Char_OuterLoop(ISortCases sortCases) + { + Test_Keys_Char(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_String_OuterLoop(ISortCases sortCases) + { + Test_Keys_String(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_ComparableStructInt32_OuterLoop(ISortCases sortCases) + { + Test_Keys_ComparableStructInt32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_ComparableClassInt32_OuterLoop(ISortCases sortCases) + { + Test_Keys_ComparableClassInt32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_Keys_BogusComparable_OuterLoop(ISortCases sortCases) + { + Test_Keys_BogusComparable(sortCases); + } + #endregion + + #region Keys and Values Tests + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_Int8_Int32(ISortCases sortCases) + { + Test_KeysValues_Int8_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_UInt8_Int32(ISortCases sortCases) + { + Test_KeysValues_UInt8_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_Int16_Int32(ISortCases sortCases) + { + Test_KeysValues_Int16_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_UInt16_Int32(ISortCases sortCases) + { + Test_KeysValues_UInt16_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_Int32_Int32(ISortCases sortCases) + { + Test_KeysValues_Int32_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_UInt32_Int32(ISortCases sortCases) + { + Test_KeysValues_UInt32_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_Int64_Int32(ISortCases sortCases) + { + Test_KeysValues_Int64_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_UInt64_Int32(ISortCases sortCases) + { + Test_KeysValues_UInt64_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_Single_Int32(ISortCases sortCases) + { + Test_KeysValues_Single_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_Double_Int32(ISortCases sortCases) + { + Test_KeysValues_Double_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_Boolean_Int32(ISortCases sortCases) + { + Test_KeysValues_Boolean_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_Char_Int32(ISortCases sortCases) + { + Test_KeysValues_Char_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_String_Int32(ISortCases sortCases) + { + Test_KeysValues_String_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_ComparableStructInt32_Int32(ISortCases sortCases) + { + Test_KeysValues_ComparableStructInt32_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_ComparableClassInt32_Int32(ISortCases sortCases) + { + Test_KeysValues_ComparableClassInt32_Int32(sortCases); + } + + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_fastSortTests))] + public static void Sort_KeysValues_BogusComparable_Int32(ISortCases sortCases) + { + Test_KeysValues_BogusComparable_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_Int8_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_Int8_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_UInt8_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_UInt8_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_Int16_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_Int16_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_UInt16_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_UInt16_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_Int32_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_Int32_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_UInt32_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_UInt32_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_Int64_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_Int64_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_UInt64_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_UInt64_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_Single_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_Single_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_Double_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_Double_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_Boolean_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_Boolean_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_Char_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_Char_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_String_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_String_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_ComparableStructInt32_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_ComparableStructInt32_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_ComparableClassInt32_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_ComparableClassInt32_Int32(sortCases); + } + + [OuterLoop] + [Theory] + [Trait(SortTrait, SortTraitValue)] + [MemberData(nameof(s_slowSortTests))] + public static void Sort_KeysValues_BogusComparable_Int32_OuterLoop(ISortCases sortCases) + { + Test_KeysValues_BogusComparable_Int32(sortCases); + } + #endregion + + // NOTE: Sort_MaxLength_NoOverflow test is constrained to run on Windows and MacOSX because it causes + // problems on Linux due to the way deferred memory allocation works. On Linux, the allocation can + // succeed even if there is not enough memory but then the test may get killed by the OOM killer at the + // time the memory is accessed which triggers the full memory allocation. + [Fact] + [Trait(SortTrait, SortTraitValue)] + [OuterLoop] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.OSX)] + public unsafe static void Sort_MaxLength_NoOverflow() + { + if (sizeof(IntPtr) == sizeof(long)) + { + // Allocate maximum length span native memory + var length = int.MaxValue; + if (!AllocationHelper.TryAllocNative(new IntPtr(length), out IntPtr memory)) + { + Console.WriteLine($"Span.Sort test {nameof(Sort_MaxLength_NoOverflow)} skipped (could not alloc memory)."); + return; + } + try + { + var span = new Span(memory.ToPointer(), length); + var filler = new DecrementingSpanFiller(); + const byte fill = 42; + span.Fill(fill); + span[0] = 255; + span[1] = 254; + span[span.Length - 2] = 1; + span[span.Length - 1] = 0; + + span.Sort(); + + Assert.Equal(span[0], (byte)0); + Assert.Equal(span[1], (byte)1); + Assert.Equal(span[span.Length - 2], (byte)254); + Assert.Equal(span[span.Length - 1], (byte)255); + for (int i = 2; i < length - 2; i++) + { + Assert.Equal(span[i], fill); + } + } + finally + { + AllocationHelper.ReleaseNative(ref memory); + } + } + } + } +} diff --git a/src/System.Memory/tests/System.Memory.Tests.csproj b/src/System.Memory/tests/System.Memory.Tests.csproj index 95e5d874ffa3..c719ff7a283a 100644 --- a/src/System.Memory/tests/System.Memory.Tests.csproj +++ b/src/System.Memory/tests/System.Memory.Tests.csproj @@ -52,6 +52,9 @@ + + + @@ -179,4 +182,4 @@ - \ No newline at end of file +