用于“取n(排序xs)”(“排序前缀”)问题的内存有效算法

时间:2011-05-10 12:16:53

标签: sorting haskell memory-management lazy-evaluation

我想从懒惰列表中获取n个最大的元素。

我听说在Data.List.sort中实现的mergesort是惰性的,它不会产生超过必要的元素。这在比较方面可能是正确的,但在内存使用方面肯定不是这样。以下程序说明了这个问题:

{-# LANGUAGE ScopedTypeVariables #-}

module Main where

import qualified Data.Heap as Heap
import qualified Data.List as List

import System.Random.MWC
import qualified Data.Vector.Unboxed as Vec

import System.Environment

limitSortL n xs = take n (List.sort xs)
limitSortH n xs = List.unfoldr Heap.uncons (List.foldl' (\ acc x -> Heap.take n (Heap.insert x acc) ) Heap.empty xs) 

main = do
  st <- create
  rxs :: [Int] <- Vec.toList `fmap` uniformVector st (10^7)

  args <- getArgs
  case args of
    ["LIST"] -> print (limitSortL 20 rxs)
    ["HEAP"] -> print (limitSortH 20 rxs)

  return ()

运行时:

Data.List模块:

./lazyTest LIST +RTS -s 
[-9223371438221280004,-9223369283422017686,-9223368296903201811,-9223365203042113783,-9223364809100004863,-9223363058932210878,-9223362160334234021,-9223359019266180408,-9223358851531436915,-9223345045262962114,-9223343191568060219,-9223342956514809662,-9223341125508040302,-9223340661319591967,-9223337771462470186,-9223336010230770808,-9223331570472117335,-9223329558935830150,-9223329536207787831,-9223328937489459283]
   2,059,921,192 bytes allocated in the heap
   2,248,105,704 bytes copied during GC
     552,350,688 bytes maximum residency (5 sample(s))
       3,390,456 bytes maximum slop
            1168 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0:  3772 collections,     0 parallel,  1.44s,  1.48s elapsed
  Generation 1:     5 collections,     0 parallel,  0.90s,  1.13s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    0.82s  (  0.84s elapsed)
  GC    time    2.34s  (  2.61s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time    3.16s  (  3.45s elapsed)

  %GC time      74.1%  (75.7% elapsed)

  Alloc rate    2,522,515,156 bytes per MUT second

  Productivity  25.9% of total user, 23.7% of total elapsed

Data.Heap:

./lazyTest HEAP +RTS -s 
[-9223371438221280004,-9223369283422017686,-9223368296903201811,-9223365203042113783,-9223364809100004863,-9223363058932210878,-9223362160334234021,-9223359019266180408,-9223358851531436915,-9223345045262962114,-9223343191568060219,-9223342956514809662,-9223341125508040302,-9223340661319591967,-9223337771462470186,-9223336010230770808,-9223331570472117335,-9223329558935830150,-9223329536207787831,-9223328937489459283]
 177,559,536,928 bytes allocated in the heap
     237,093,320 bytes copied during GC
      80,031,376 bytes maximum residency (2 sample(s))
         745,368 bytes maximum slop
              78 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0: 338539 collections,     0 parallel,  1.24s,  1.31s elapsed
  Generation 1:     2 collections,     0 parallel,  0.00s,  0.00s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time   35.24s  ( 35.46s elapsed)
  GC    time    1.24s  (  1.31s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time   36.48s  ( 36.77s elapsed)

  %GC time       3.4%  (3.6% elapsed)

  Alloc rate    5,038,907,812 bytes per MUT second

  Productivity  96.6% of total user, 95.8% of total elapsed

很明显,limitSortL要快得多,但它也非常耗费内存。在较大的列表上,它达到了RAM的大小。

是否有更快的算法来解决这个问题,而不是内存饥饿?

编辑:澄清:我从堆包中使用Data.Heap,我没有尝试堆包。

6 个答案:

答案 0 :(得分:4)

所以,我实际上设法解决了这个问题。想法是抛弃花哨的数据结构并手工工作;-) 基本上我们将输入列表分成块,对它们进行排序,并折叠[[Int]]列表,在每一步选择n个最小元素。 技巧部分是以正确的方式将累加器与已排序的块合并。我们必须使用seq或者懒惰会咬你,结果仍然需要大量的内存来计算。另外,我将合并与take n混合,只是为了进一步优化。以下是整个计划,以及之前的尝试:

{-# LANGUAGE ScopedTypeVariables, PackageImports #-}     
module Main where

import qualified Data.List as List
import qualified Data.List.Split as Split
import qualified "heaps" Data.Heap as Heap -- qualified import from "heaps" package

import System.Random.MWC
import qualified Data.Vector.Unboxed as Vec

import System.Environment

limitSortL n xs = take n (List.sort xs)
limitSortH n xs = List.unfoldr Heap.uncons (List.foldl' (\ acc x -> Heap.take n (Heap.insert x acc) ) Heap.empty xs)
takeSortMerge n inp = List.foldl' 
                        (\acc lst -> (merge n acc (List.sort lst))) 
                        [] (Split.splitEvery n inp)
    where
     merge 0 _ _ = []
     merge _ [] xs = xs
     merge _ ys [] = ys
     merge f (x:xs) (y:ys) | x < y = let tail = merge (f-1) xs (y:ys) in tail `seq` (x:tail) 
                           | otherwise = let tail = merge (f-1) (x:xs) ys in tail `seq` (y:tail)


main = do
  st <- create

  let n1 = 10^7
      n2 = 20

  rxs :: [Int] <- Vec.toList `fmap` uniformVector st (n1)

  args <- getArgs

  case args of
    ["LIST"] ->  print (limitSortL n2 rxs)
    ["HEAP"] ->  print (limitSortH n2 rxs)
    ["MERGE"] -> print (takeSortMerge n2 rxs)
    _ -> putStrLn "Nothing..."

  return ()

运行时性能,内存消耗,GC时间:

LIST       3.96s   1168 MB    75 %
HEAP       35.29s    78 MB    3.6 %
MERGE      1.00s     78 MB    3.0 %
just rxs   0.21s     78 MB    0.0 %  -- just evaluating the random vector

答案 1 :(得分:3)

有很多selection algorithms专门做这件事。基于分区的算法是“经典算法”,但就像Quicksort一样,它并不适合Haskell列表。维基百科与函数式编程没有多大关系,尽管我怀疑所描述的“锦标赛选择”与您当前的mergesort解决方案相同或没有太大差别。

如果您担心内存消耗,可以使用优先级队列 - 它总共使用O(K)内存和O(N * logK)时间:

queue := first k elements
for each element in the rest:
    add the element to the queue
    remove the largest element from the queue
convert the queue to a sorted list

答案 2 :(得分:2)

“Quicksort和第k个最小元素”,总是引人入胜的Heinrich Apfelmus: http://apfelmus.nfshost.com/articles/quicksearch.html

答案 3 :(得分:1)

如果我无法破译,请原谅

 Vec.toList `fmap` uniformVector st (10^7)

但这个清单会有多长时间?很清楚,无论多么懒惰的合并,它至少必须实现整个列表?

更新

  

我听说mergesort实现了   Data.List.sort是懒惰的,但它没有   产生的元素多于必要的元素。

在它开始传递列表的第一个元素之前,它没有告诉mergesorts空间消耗。在任何情况下,它都必须遍历(从而实现)整个列表,分配为合并的子列表等。 以下是来自http://www.inf.fh-flensburg.de/lang/algorithmen/sortieren/merge/mergen.htm

的评论
  

mergesort的一个缺点就是它   需要一个额外的Θ(n)空间   临时数组b。

     

有不同的可能性   实现功能合并。最多   有效的这些是变体b。它   只需要一半的额外费用   空间,它比另一个更快   变种,它是稳定的。

答案 4 :(得分:0)

你可能误解了这个问题。这可能是太多懒惰的情况,而不是太少。

也许你应该在ST monad中尝试更严格的数据结构或可变数组。

对于可变数组方法,您可以将每次插入的移动次数限制为n/2而不是n-1,方法是记录一个“指向”头部的索引h。队列存储在数组中,并允许队列在数组内“环绕”。

答案 5 :(得分:0)

记忆效率很少是哈克尔的力量。也就是说,生成一个比mergesort更完全懒惰的排序算法并不难。例如,这是一个简单的快速排序:

qsort [] = []
qsort (x:xs) = qcombine (qsort a) b (qsort c) where
    (a,b,c) = qpart x (x:xs) ([],[],[])
qpart _ [] ac = ac
qpart n (x:xs) (a,b,c)
    | x > n = qpart n xs (a,b,x:c)
    | x < n = qpart n xs (x:a,b,c)
    | otherwise = qpart n xs (a,x:b,c)
qcombine (a:as) b c = a:qcombine as b c
qcombine [] (b:bs) c = b:qcombine [] bs c
qcombine [] [] c = c

我使用显式递归来明确发生了什么。这里的每个部分都是真正的懒惰,这意味着qcombine除非需要,否则永远不会调用qsort c。如果您只想要前几个项目,这应该会降低您的内存使用率。

您可以为此特定任务构建更好的排序算法,该算法使用快速排序样式分区以未排序的顺序获取列表的前n项。然后,如果您需要它们,只需调用一个高效的排序算法。

该方法的一个例子:

qselect 0 _ = []
qselect n [] = error ("cant produce " ++ show n ++ " from empty list")
qselect n (x:xs)
    | al > n = qselect n a
    | al + bl > n = a ++ take (al - n) b
    | otherwise = a ++ b ++ (qselect (n - al - bl) c) where
        (a,al,b,bl,c,cl) = qpartl x (x:xs) ([],0,[],0,[],0)

qpartl _ [] ac = ac
qpartl n (x:xs) (a,al,b,bl,c,cl)
    | x > n = qpartl n xs (a,al,b,bl,x:c,cl+1)
    | x < n = qpartl n xs (x:a,al+1,b,bl,c,cl+1)
    | otherwise = qpartl n xs (a,al,x:b,bl+1,c,cl)

同样,这段代码并不是最干净的,但我想说清楚它在做什么。

对于您想要拍摄非常低数字的情况,选择排序是最佳选择。例如,如果您只想要列表中的最高元素,则可以在给出大小的列表大小时迭代它。

另一方面,如果您想要几乎所有列表,但不关心按顺序排列,您可以重复“删除”列表中的最低元素。

这两种方法和上面的快速排序都是O(n ^ 2),但你想要的是经常使用大O(k * n)的策略,并且往往不使用大量的空间。

另一种选择是使用就地排序算法来控制内存使用。我不知道任何懒惰的原位排序,但如果它们存在那将是完美的。