为什么我的 C# 快速排序实现比 List<T>.Sort 慢得多

9

我已经在C#中实现了快速排序的一个版本,并进行了一些快速基准测试,以与C# List<T>.Sort 进行比较。我发现我的实现比库版本慢得多。我正在尝试理解为什么会这样。以下是一些粗略的基准测试数字。对于输入,我使用了随机生成的整数列表(均匀分布)和少量重复元素。请注意,所有基准测试时间都是若干次运行的平均值。

100,000 elements
My code         0.096 seconds
List<T>.Sort    0.011 seconds

1,000,000 elements
My code         1.10 seconds
List<T>.Sort    0.14 seconds

我的代码如下。这是我尝试过的优化措施及其结果列表:
  • 内联 - 我试图内联我的 SwapChoosePivotIndex 函数,使用时有约 10% 的提升。
  • 枢轴选择 - 我知道我使用了一个天真的枢轴选择方法。我尝试过选用三个随机元素的中位数来作为枢轴,但没有多大改进。我猜测这是因为我用于基准测试的输入数据是均匀随机的。
  • 并行处理 - 我尝试使递归的 Partition 调用并行处理。这似乎实际上增加了执行时间。
  • 特殊情况小输入 - 我尝试切换到插入排序来处理小输入(就像 List<T>.Sort 所声称的那样)。这可以使性能提高约 20%。

通过这些优化措施的组合,我已经将代码压缩到了...

100,000 elements -   0.062 seconds
1,000,000 elements - 0.740 seconds 

这仍然比库排序慢得多。我的代码中是否有任何明显的问题可以解释性能差距,还是剩下的70-80%差距来自更小的调整?请注意,下面的代码是我的基本未优化版本

public class Quicksorter<T> where T : IComparable<T>
{
    protected Random random;
    public Quicksorter()
    {
        random = new Random();
    }

    public void Sort(IList<T> items)
    {
        Partition(items, 0, items.Count-1);
    }

    private void Partition(IList<T> items, int startIndex, int endIndex)
    {
        if (startIndex >= endIndex)
            return;

        int pivotIndex = ChoosePivotIndex(items, startIndex, endIndex);
        T pivot = items[pivotIndex];

        // swap pivot and first element
        Swap(items, startIndex, pivotIndex);

        int partitionIndex = startIndex + 1;
        for (int frontierIndex = partitionIndex; frontierIndex <= endIndex; frontierIndex++)
        {
            if (items[frontierIndex].CompareTo(pivot) < 0)
            {
                Swap(items, frontierIndex, partitionIndex);
                partitionIndex++;
            }
        }
        // put pivot back
        items[startIndex] = items[partitionIndex-1];
        items[partitionIndex - 1] = pivot;

        // recursively sort left half
        Partition(items, startIndex, partitionIndex - 2);
        // recursively sort right half
        Partition(items, partitionIndex, endIndex);
    }

    protected virtual int ChoosePivotIndex(IList<T> items, int startIndex, int endIndex)
    {
        return random.Next(startIndex, endIndex);
    }

    private void Swap(IList<T> items, int aIndex, int bIndex)
    {
        T temp = items[aIndex];
        items[aIndex] = items[bIndex];
        items[bIndex] = temp;
    }
}

你一定会在启用优化的发布配置下构建你的代码,以防万一,对吧? - Anri
List.Sort 可能使用 Array.Sort,对于非微不足道的输入,它可以使用快速排序或堆排序。快速排序和堆排序的性能取决于输入值的范围和顺序,因此您需要在许多不同的输入上运行它以获得实际的时间测量。 - Keith Payne
@KaushalDeSilva,这样做背后有什么动机/直觉吗? - mattnedrich
@mattnedrich 我没有证据,但首先我认为固定长度的列表比动态长度的列表更好;其次,我认为也许数组(由于具有严格的长度和数据类型)在内存中存储方式不同,可以实现更快的访问。再次强调,我没有证据。 - Kaushal De Silva
List<T> 是使用数组实现的。当您调用 List.Sort 时,它会转而调用 Array.Sort。在排序时间方面,数组和列表之间没有区别(除了一个额外的函数调用)。将 Partition 修改为接受一个数组最终会要求 Quicksort 方法从传递的 IList 创建一个数组——以 O(n) 的额外空间代价——并且在排序完成后还需要将其复制回 IList。这可能非常昂贵。 - Jim Mischel
显示剩余2条评论
1个回答

11

由于.NET框架为整数、字符串和其他内置类型提供了特殊的排序方法,因此它们不会产生调用委托进行比较等开销。如果您使用内置类型来比较排序,通常库排序将更快。


3
List<T>.Sort 调用 Array.TrySZSort(Array keys, Array items, int left, int right),它是用本地代码实现的,并且处理了所有基本值类型的优化快速排序。参考链接 - Alex Wiese
那么,如果我使用除内置类型之外的东西进行比较,我应该会看到类似的结果(或者至少比我目前看到的更多)吗?此外,根据@KeithPayne所说,这是否有记录在案? - mattnedrich
@KeithPayne:我所知道的除了参考源代码之外没有任何文档记录。请参阅http://referencesource.microsoft.com/。 - Jim Mischel
@mattnedrich:如果你在对复杂类型进行排序并传递比较委托给List.Sort,那么你可能可以比.NET的sort函数更好地完成。不过这将取决于版本。.NET 4.0拥有一个非常优化的中值三分快速排序。.NET 4.5使用了Introsort,其中的Quicksort部分不如.NET 4.0中的精细。请参见http://zimbry.blogspot.com/2012/01/nets-sort-is-not-secure-dont-use-it.html以查看一个快排,它比.NET快排表现更好。当然,现在没有必要使用该实现,因为.NET 4.5使用了Introsort。 - Jim Mischel
1
相关代码位于http://www.dotnetframework.org/default.aspx/DotNET/DotNET/8@0/untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Array@cs/2/Array@cs。搜索“TrySZSort”。 - Jim Mischel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接