【问题标题】:C# merge sort performanceC# 归并排序性能
【发布时间】:2012-08-22 08:07:19
【问题描述】:

只是一个简短的说明,这不是家庭作业。我只是想重温我的算法。我正在使用 C# 中的 MergeSort 并编写了一个可以基于泛型进行排序的递归方法:

class SortAlgorithms
{

    public T[] MergeSort<T> (T[] unsortedArray) where T : System.IComparable<T>
    {
        T[] left, right;
        int middle = unsortedArray.Length / 2;

        left = new T[middle];
        right = new T[unsortedArray.Length - middle];

        if (unsortedArray.Length <= 1)
            return unsortedArray;

        for (int i = 0; i < middle; i++)
        {
            left[i] = unsortedArray[i];
        }

        for (int i = middle; i < unsortedArray.Length; i++)
        {
            right[i - middle] = unsortedArray[i];
        }

        left = MergeSort(left);

        right = MergeSort(right);


        return Merge<T>(left, right);
    }

    private T[] Merge<T> (T[] left, T[] right) where T : System.IComparable<T>
    {
        T[] result = new T[left.Length + right.Length];

        int currentElement = 0;

        while (left.Length > 0 || right.Length > 0)
        {
            if (left.Length > 0 && right.Length > 0)
            {
                if (left[0].CompareTo(right[0]) < 0)
                {
                    result[currentElement] = left[0];
                    left = left.Skip(1).ToArray();
                    currentElement++;
                }
                else
                {
                    result[currentElement] = right[0];
                    right = right.Skip(1).ToArray();
                    currentElement++;
                }
            }
            else if (left.Length > 0)
            {
                result[currentElement] = left[0];
                left = left.Skip(1).ToArray();
                currentElement++;
            }
            else if (right.Length > 0)
            {
                result[currentElement] = right[0];
                right = right.Skip(1).ToArray();
                currentElement++;
            }
        }

        return result;
    }
}

这可行,但速度非常慢。我已经使用 System.Diagnostic.StopWatch 来检查 Array.Sort (使用 QuickSort 算法)的性能,以与我的 MergeSort 进行比较,差异如此之大,我想知道我是否实施了这个错误。有cmets吗?

【问题讨论】:

  • 你读过 Jons 的文章吗? msmvps.com/blogs/jon_skeet/archive/2011/01/06/…
  • 您是否尝试过相同的实现但没有泛型?
  • 很好的答案伙计们。抱歉花了这么长时间才回复,我一直在重写代码,最终得到的代码看起来几乎与 Rafe 建议的完全一样。比原生 Array.Sort 快得多,但仍然慢得多。还是玩了一下。

标签: c# algorithm sorting mergesort


【解决方案1】:

我不是 C# 程序员,但问题可能在于使用这样的语句吗?

left = left.Skip(1).ToArray();

这可能以强制对底层数组进行深拷贝的方式实现。如果是这样,这会将合并的性能从 O(n) 降低到 O(n2),立即将生成的合并排序的性能从 O(n log n) 降低到 O(n2).

(这是因为重复周期从

T(1) = O(1)

T(n) ≤ 2T(n / 2) + O(n)

有解 T(n) = O(n log n), 到

T(1) = O(1)

T(n) ≤ 2T(n / 2) + O(n2)

有解 T(n) = O(n2).)

【讨论】:

  • 它没有实现深拷贝,但它确实会创建一个新数组(带有浅拷贝).. 不确定它是如何改变边界的,但它仍然不理想。
  • @pst- 如果这会创建一个新数组并浅拷贝元素,这也会导致性能下降,因为浅拷贝仍然需要 O(n) 时间。
  • 感谢您的澄清,我不确定这是否是深拷贝的一个方面。
  • +1。我认为根本不需要ToArray() - 删除ToArray 调用并将left/right 设置为IEnumerator(手动Current/MoveNext 调用)将使代码更快并保持其花哨的 LINQ 感觉。
  • @AlexeiLevenkov 这里是“需要的”,因为该方法当前是如何设置的(但是,我同意这是问题所在;-)。不过,一般来说,人们会通过虚拟左/右拆分(例如 C 风格的实现)传递相同的数组,或者使用类似 ArraySegment 的东西。
【解决方案2】:

您不断地以中间数组的形式分配内存。考虑重用原始数组的方向。

【讨论】:

    【解决方案3】:

    正如其他两个答案所说,您正在到处创建新数组,为此花费了大量时间和内存(我猜,您的大部分时间和几乎所有内存使用)。

    再一次,我要补充一点,所有其他条件都相等的递归往往比迭代慢,并且使用更多的堆栈空间(甚至可能会导致溢出,导致足够大的问题,而迭代不会)。

    但是。合并排序非常适合多线程方法,因为您可以让不同的线程处理第一批分区的不同部分。

    因此,如果是我玩这个,我接下来的两个实验将是:

    1. 对于分区的第一位,而不是递归调用MergeSort,我会启动一个新线程,直到每个核心都有一个线程运行(无论我应该按物理核心还是虚拟核心执行在超线程的情况下,它本身就是我要试验的东西)。
    2. 完成后,我会尝试重新编写递归方法以在不进行递归调用的情况下执行相同的操作。

    在处理了ToArray() 问题之后,看看多线程方法如何首先将工作分配给最佳数量的内核,然后让每个内核迭代地完成其工作,这确实会非常有趣。

    【讨论】:

    • 与其处理是否产生一个新线程,你可以创建一个新的Task 并让它全部进入线程池。线程池的大小可能与物理处理器的数量差不多。
    • 根据我的经验,在 iCore 5/7 系列(不要低估进程窃取;-)。此外,线程应该用于小n(叶子和边缘分支),并且切换到边缘的非合并排序是“常见优化”..
    • @JonHanna 物理(虚拟?)核心。 i5 没有超线程。不完全确定所应用的所有因素,但将生成的线程数限制为硬件可以本机处理的一些“相当小的”比率(最好使用线程池/回收)是有好处的。
    • @Servy 将相邻分区保持在同一个线程手中是有好处的,除非你有一个低于该任务的限制,否则达到固定数量的线程比创建新任务更容易切换到完成工作而不是创建更多任务。最佳点所在的位置本身就是一件有趣的事情。
    • @JonHanna 那确实很有趣。
    【解决方案4】:

    首先,这是一个类似问题的简化解决方案的链接:Java mergesort, should the "merge" step be done with queues or arrays?

    您的解决方案很慢,因为您反复分配新的子数组。内存分配比大多数其他操作更昂贵(您有分配成本、收集成本和缓存局部性丢失)。通常这不是问题,但如果您尝试编写严格的排序例程,那么这很重要。对于归并排序,您只需要一个目标数组和一个临时数组。

    分叉线程以进行并行化仍然比这贵几个数量级。因此,除非您有大量数据要排序,否则不要分叉。

    正如我在上面的答案中提到的,加快合并排序的一种方法是利用输入数组中的现有顺序。

    【讨论】:

      猜你喜欢
      • 2014-07-30
      • 2019-03-07
      • 2014-04-08
      • 2012-01-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多