【问题标题】:Why is there a 1000x performance difference between two versions of merge sort in haskell为什么haskell中两个版本的归并排序有1000倍的性能差异
【发布时间】:2013-08-21 19:49:07
【问题描述】:

编辑:

事实证明,慢版本实际上是插入排序 O(n^2) 而不是合并排序 O(n log n) 来解释性能问题。我想我可以让未来的读者免于费力通过代码来发现这个答案的痛苦。


原创从这里开始-------------------------------------------

我已经在 haskell 中编写了两个版本的合并排序,但我无法理解为什么一个版本比另一个版本快 1000 倍。在这两种情况下,我们首先将列表中的项目排序为列表,创建列表列表。然后我们将列表配对并合并它们,直到只剩下一个列表。问题似乎是我在慢版本中调用“doMerge(x1:x2:xs)= doMerge $ merge x1 x2:doMerge xs”,但在快速版本中调用doMerge(mergePairs xs)。 1000倍的速度差异让我感到惊讶!

-- Better version: takes 0.34 seconds to sort a 100,000 integer list.
betMergeSort :: [Int] -> [Int]
betMergeSort list = doMerge $ map (\x -> [x]) list
  where
    doMerge :: [[Int]] -> [Int]
    doMerge [] = []
    doMerge [xs] = xs
    doMerge xs = doMerge (mergePairs xs)

    mergePairs :: [[Int]] -> [[Int]]
    mergePairs (x1:x2:xs) = merge x1 x2 : mergePairs xs
    mergePairs xs = xs

    -- expects two sorted lists and returns one sorted list.
    merge :: [Int] -> [Int] -> [Int]
    merge [] ys = ys
    merge xs [] = xs
    merge (x:xs) (y:ys) = if x <= y
                            then x : merge xs (y:ys)
                            else y : merge (x:xs) ys


-- Slow version: takes 350 seconds to sort a 100,000 integer list.
slowMergeSort :: [Int] -> [Int]
slowMergeSort list = head $ doMerge $ map (\x -> [x]) list
  where
    doMerge :: [[Int]] -> [[Int]]
    doMerge [] = []
    doMerge (oneList:[]) = [oneList]
    doMerge (x1:x2:xs) = doMerge $ merge x1 x2 : doMerge xs

    -- expects two sorted list and returns one sorted list.
    merge :: [Int] -> [Int] -> [Int]
    merge [] ys = ys
    merge xs [] = xs
    merge (x:xs) (y:ys) = if x <= y then x : merge xs (y:ys) else y : merge (x:xs) ys

查看分析器输出,很明显慢版本正在分配更多的内存方式。我不确定为什么。这两个版本在我的脑海中似乎很相似。有人能解释一下为什么分配如此不同吗?

slowMergeSort 分析结果:

    Wed Aug 21 12:24 2013 Time and Allocation Profiling Report  (Final)

       mergeSort +RTS -sstderr -p -RTS s

    total time  =       12.02 secs   (12017 ticks @ 1000 us, 1 processor)
    total alloc = 17,222,571,672 bytes  (excludes profiling overheads)

COST CENTRE         MODULE  %time %alloc

slowMergeSort.merge Main     99.2   99.4


                                                                               individual     inherited
COST CENTRE               MODULE                             no.     entries  %time %alloc   %time %alloc

MAIN                      MAIN                                74           0    0.0    0.0   100.0  100.0
 main                     Main                               149           0    0.0    0.0   100.0  100.0
  main.sortedL            Main                               165           1    0.0    0.0    99.3   99.5
   slowMergeSort          Main                               167           1    0.0    0.0    99.3   99.5
    slowMergeSort.\       Main                               170       40000    0.0    0.0     0.0    0.0
    slowMergeSort.doMerge Main                               168       79999    0.0    0.0    99.2   99.5
     slowMergeSort.merge  Main                               169   267588870   99.2   99.4    99.2   99.4
  main.sortVersion        Main                               161           1    0.0    0.0     0.0    0.0
  randomInts              Main                               151           1    0.0    0.0     0.7    0.5
   force                  Main                               155           1    0.0    0.0     0.0    0.0
    force.go              Main                               156       40001    0.0    0.0     0.0    0.0
   randomInts.result      Main                               152           1    0.7    0.5     0.7    0.5

libMergeSort 分析

    Wed Aug 21 12:23 2013 Time and Allocation Profiling Report  (Final)

       mergeSort +RTS -sstderr -p -RTS l

    total time  =        0.12 secs   (124 ticks @ 1000 us, 1 processor)
    total alloc = 139,965,768 bytes  (excludes profiling overheads)

COST CENTRE              MODULE  %time %alloc

randomInts.result        Main     66.9   64.0
libMergeSort.merge       Main     24.2   30.4
main                     Main      4.0    0.0
libMergeSort             Main      2.4    3.2
libMergeSort.merge_pairs Main      1.6    2.3


                                                                                    individual     inherited
COST CENTRE                    MODULE                             no.     entries  %time %alloc   %time %alloc

MAIN                           MAIN                                74           0    0.0    0.0   100.0  100.0
 main                          Main                               149           0    4.0    0.0   100.0  100.0
  main.sortedL                 Main                               165           1    0.0    0.0    28.2   35.9
   libMergeSort                Main                               167           1    2.4    3.2    28.2   35.9
    libMergeSort.\             Main                               171       40000    0.0    0.0     0.0    0.0
    libMergeSort.libMergeSort' Main                               168          17    0.0    0.0    25.8   32.7
     libMergeSort.merge_pairs  Main                               169       40015    1.6    2.3    25.8   32.7
      libMergeSort.merge       Main                               170      614711   24.2   30.4    24.2   30.4
  main.sortVersion             Main                               161           1    0.0    0.0     0.0    0.0
  randomInts                   Main                               151           1    0.0    0.0    67.7   64.0
   force                       Main                               155           1    0.0    0.0     0.8    0.0
    force.go                   Main                               156       40001    0.8    0.0     0.8    0.0
   randomInts.result           Main                               152           1   66.9   64.0    66.9   64.0

【问题讨论】:

  • 您能否修复慢速合并分析结果的格式?
  • 您是否使用[Integer] 类型的参数而不是[Int] 来运行slowMergeSort 基准测试?
  • 您应该使它们都采用 [Int] 或都采用多态性,因此您实际上是在比较同一事物。部分(但可能不是全部)差异可能是因为多态性较慢。
  • 第二个版本反复合并很长的列表。 (a,b) 表示您合并两个长度为 a 和 b 的列表。第一个版本是:Nx(1,1), (N/2)x(2,2), (N/4)x(4,4) 等等;第二个是 Nx(1,1),然后是 (2,2), (2,4), (2,6)。最后,您将合并长度为 ~ (2,N) 的列表。如果你不走运,你将不得不遍历第二个(非常长的)列表。这会破坏性能。
  • 在分析的版本中,两者都采用了 [Int]。我在运行分析之前开始发布帖子,修复类型以使其匹配,然后发布分析。我忘了在这里更新 slowMergeSort 代码。现在我有了。

标签: performance haskell


【解决方案1】:

其中的第二个是O(n^2),不应使用,因为它是错误的算法(不应称为归并排序)。

doMerge (x1:x2:xs) = doMerge $ merge x1 x2 : doMerge xs

完全排序xs,它只比原始列表短一个常数。将一个很短的列表与一个很长的列表合并相当于插入,所以我们看到这实际上只是插入排序。合并排序的全部意义在于分而治之,这不是分而治之。随着列表的长度越来越长,速度比将继续变差。

第一个是适当的合并排序,因为它合并大约偶数长度的列表。

【讨论】:

  • 谢谢@Philip JF:我对这个有点不好意思!
猜你喜欢
  • 2012-11-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-27
  • 1970-01-01
  • 2015-06-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多