Skip to content

Commit

Permalink
Improve Array.Sort(T[]) performance (#35297)
Browse files Browse the repository at this point in the history
* Improve Array.Sort(T[]) performance

A variety of tweaks to improve `Array.Sort<T>(T[])` performance and address a regression left over from moving the array sorting implementation from native to managed.  The two most impactful are using `Unsafe.*` in `PickPivotAndPartition` to avoid bounds checks and aggressive inlining on `SwapIfGreater`.  A few other small improvements to codegen round it out.

I only made the unsafe changes in the `Sort<T>(T[])` implementation, and not in the more complicated implementations, such as for `Sort<T>(T[], Comparer<T>)` and `Sort<TKey, TValue>(TKey[], TValue[])`, but I did make some of the smaller changes for consistency across the file.

* Address PR feedback, and more tweaks
  • Loading branch information
stephentoub authored Apr 24, 2020
1 parent 89d08de commit f73ceee
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 297 deletions.
53 changes: 37 additions & 16 deletions src/libraries/System.Private.CoreLib/src/System/Array.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
Expand All @@ -26,6 +27,11 @@ public abstract partial class Array : ICloneable, IList, IStructuralComparable,
internal const int MaxArrayLength = 0X7FEFFFFF;
internal const int MaxByteArrayLength = 0x7FFFFFC7;

// 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;

// This ctor exists solely to prevent C# from generating a protected .ctor that violates the surface area.
private protected Array() { }

Expand Down Expand Up @@ -1916,11 +1922,11 @@ private void IntrospectiveSort(int left, int length)

try
{
IntroSort(left, length + left - 1, 2 * IntrospectiveSortUtilities.FloorLog2PlusOne(length));
IntroSort(left, length + left - 1, 2 * (BitOperations.Log2((uint)length) + 1));
}
catch (IndexOutOfRangeException)
{
IntrospectiveSortUtilities.ThrowOrIgnoreBadComparer(comparer);
ThrowHelper.ThrowArgumentException_BadComparer(comparer);
}
catch (Exception e)
{
Expand All @@ -1930,20 +1936,22 @@ private void IntrospectiveSort(int left, int length)

private void IntroSort(int lo, int hi, int depthLimit)
{
Debug.Assert(hi > lo);
Debug.Assert(depthLimit >= 0);

while (hi > lo)
{
int partitionSize = hi - lo + 1;
if (partitionSize <= IntrospectiveSortUtilities.IntrosortSizeThreshold)
if (partitionSize <= IntrosortSizeThreshold)
{
if (partitionSize == 1)
{
return;
}
Debug.Assert(partitionSize >= 2);

if (partitionSize == 2)
{
SwapIfGreater(lo, hi);
return;
}

if (partitionSize == 3)
{
SwapIfGreater(lo, hi - 1);
Expand Down Expand Up @@ -1971,8 +1979,11 @@ private void IntroSort(int lo, int hi, int depthLimit)

private int PickPivotAndPartition(int lo, int hi)
{
Debug.Assert(hi - lo >= IntrosortSizeThreshold);

// Compute median-of-three. But also partition them, since we've done the comparison.
int mid = lo + (hi - lo) / 2;

// Sort lo, mid and hi appropriately, then pick mid as the pivot.
SwapIfGreater(lo, mid);
SwapIfGreater(lo, hi);
Expand All @@ -1994,7 +2005,10 @@ private int PickPivotAndPartition(int lo, int hi)
}

// Put pivot in the right location.
Swap(left, hi - 1);
if (left != hi - 1)
{
Swap(left, hi - 1);
}
return left;
}

Expand Down Expand Up @@ -2122,11 +2136,11 @@ private void IntrospectiveSort(int left, int length)

try
{
IntroSort(left, length + left - 1, 2 * IntrospectiveSortUtilities.FloorLog2PlusOne(length));
IntroSort(left, length + left - 1, 2 * (BitOperations.Log2((uint)length) + 1));
}
catch (IndexOutOfRangeException)
{
IntrospectiveSortUtilities.ThrowOrIgnoreBadComparer(comparer);
ThrowHelper.ThrowArgumentException_BadComparer(comparer);
}
catch (Exception e)
{
Expand All @@ -2136,20 +2150,22 @@ private void IntrospectiveSort(int left, int length)

private void IntroSort(int lo, int hi, int depthLimit)
{
Debug.Assert(hi > lo);
Debug.Assert(depthLimit >= 0);

while (hi > lo)
{
int partitionSize = hi - lo + 1;
if (partitionSize <= IntrospectiveSortUtilities.IntrosortSizeThreshold)
if (partitionSize <= IntrosortSizeThreshold)
{
if (partitionSize == 1)
{
return;
}
Debug.Assert(partitionSize >= 2);

if (partitionSize == 2)
{
SwapIfGreater(lo, hi);
return;
}

if (partitionSize == 3)
{
SwapIfGreater(lo, hi - 1);
Expand Down Expand Up @@ -2177,6 +2193,8 @@ private void IntroSort(int lo, int hi, int depthLimit)

private int PickPivotAndPartition(int lo, int hi)
{
Debug.Assert(hi - lo >= IntrosortSizeThreshold);

// Compute median-of-three. But also partition them, since we've done the comparison.
int mid = lo + (hi - lo) / 2;

Expand All @@ -2200,7 +2218,10 @@ private int PickPivotAndPartition(int lo, int hi)
}

// Put pivot in the right location.
Swap(left, hi - 1);
if (left != hi - 1)
{
Swap(left, hi - 1);
}
return left;
}

Expand Down
Loading

0 comments on commit f73ceee

Please sign in to comment.