查找对于内存来说太大的列表的大小?

时间:2017-09-20 21:47:47

标签: haskell functional-programming lazy-evaluation

这里有全新的Haskell程序员。刚刚完成“了解你一个Haskell”......我对一个有多大特定属性的集合感兴趣。我有一些小参数值的代码,但我想知道如何处理更大的结构。我知道Haskell可以做“无限的数据结构”,但我只是没有看到如何从我所在的位置到达那里并且了解一个Haskell / Google并没有让我了解这一点。

以下是eSet给出的“小”参数rt

的工作代码
import Control.Monad
import System.Environment
import System.Exit

myPred :: [Int] -> Bool
myPred a = myPred' [] a
    where
        myPred' [] []         = False
        myPred' [] [0]        = True
        myPred' _  []         = True
        myPred' acc (0:aTail) = myPred' acc aTail
        myPred' acc (a:aTail)
             | a `elem` acc   = False
             | otherwise      = myPred' (a:acc) aTail

superSet :: Int -> Int -> [[Int]]
superSet r t = replicateM r [0..t]

eSet :: Int -> Int -> [[Int]]
eSet r t = filter myPred $ superSet r t

main :: IO ()
main = do
    args <- getArgs
    case args of
        [rArg, tArg] ->
            print $ length $ eSet (read rArg) (read tArg)
        [rArg, tArg, "set"] ->
            print $          eSet (read rArg) (read tArg)
        _ ->
            die "Usage: eSet r r set <set optional for printing set itself otherwise just print the size

编译/运行时我得到了

$ ghc eSet.hs -rtsopts
[1 of 1] Compiling Main             ( eSet.hs, eSet.o )
Linking eSet ...
$ # Here's is a tiny eSet to illustrate.  It is the set of lists of r integers from zero to t with no repeated nonzero list entries
$ ./eSet 4 2 set
[[0,0,0,0],[0,0,0,1],[0,0,0,2],[0,0,1,0],[0,0,1,2],[0,0,2,0],[0,0,2,1],[0,1,0,0],[0,1,0,2],[0,1,2,0],[0,2,0,0],[0,2,0,1],[0,2,1,0],[1,0,0,0],[1,0,0,2],[1,0,2,0],[1,2,0,0],[2,0,0,0],[2,0,0,1],[2,0,1,0],[2,1,0,0]]
$ ./eSet 8 4 +RTS -sstderr
3393
     174,406,136 bytes allocated in the heap
      29,061,152 bytes copied during GC
       4,382,568 bytes maximum residency (7 sample(s))
         148,664 bytes maximum slop
              14 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0       328 colls,     0 par    0.047s   0.047s     0.0001s    0.0009s
  Gen  1         7 colls,     0 par    0.055s   0.055s     0.0079s    0.0147s

  INIT    time    0.000s  (  0.000s elapsed)
  MUT     time    0.298s  (  0.301s elapsed)
  GC      time    0.102s  (  0.102s elapsed)
  EXIT    time    0.001s  (  0.001s elapsed)
  Total   time    0.406s  (  0.405s elapsed)

  %GC     time      25.1%  (25.2% elapsed)

  Alloc rate    585,308,888 bytes per MUT second

  Productivity  74.8% of total user, 75.0% of total elapsed

$ ./eSet 10 5 +RTS -sstderr
63591
  27,478,010,744 bytes allocated in the heap
   4,638,903,384 bytes copied during GC
     532,163,096 bytes maximum residency (15 sample(s))
      16,500,072 bytes maximum slop
            1556 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0     52656 colls,     0 par    6.865s   6.864s     0.0001s    0.0055s
  Gen  1        15 colls,     0 par    8.853s   8.997s     0.5998s    1.8617s

  INIT    time    0.000s  (  0.000s elapsed)
  MUT     time   52.652s  ( 52.796s elapsed)
  GC      time   15.717s  ( 15.861s elapsed)
  EXIT    time    0.193s  (  0.211s elapsed)
  Total   time   68.564s  ( 68.868s elapsed)

  %GC     time      22.9%  (23.0% elapsed)

  Alloc rate    521,883,277 bytes per MUT second

  Productivity  77.1% of total user, 76.7% of total elapsed

我看到我的内存使用率非常高,并且有很多垃圾收集。运行eSet 12 6时出现分段错误。

我觉得filter myPred $ superSet r t让我不能懒惰地让子集一次成为一部分因此我可以处理更大(但有限)的集合,但我不知道如何改变另一种方法会这样做的。我认为这是我问题的根源。

另外,由于这是我的第一个Haskell程序,因此非常感谢对样式以及如何实现Haskell类似“pythonic”的观点!

3 个答案:

答案 0 :(得分:5)

我怀疑这里的罪魁祸首是LineEnd(),其中有this implementation

replicateM

问题行是replicateM cnt0 f = loop cnt0 where loop cnt | cnt <= 0 = pure [] | otherwise = liftA2 (:) f (loop (cnt - 1)) ;可能liftA2 (:) f (loop (cnt - 1))loop (cnt - 1)部分应用于(:)元素的所有调用之间共享,因此f必须保留在内存中。不幸的是loop (cnt - 1)是一个很长的列表......

说服GHC 分享某些东西可能有点繁琐。以下对loop (cnt - 1)的重新定义给了我一个很好的平坦内存使用情况;当然,在适合内存的示例中,它可能会慢一些。关键的想法是让它看起来像未经训练的眼睛(即GHC),就像递归的monadic动作取决于之前做出的选择,即使它没有。

superSet

如果你不介意避免优化,那么更自然的定义也可以起作用:

superSet :: Int -> Int -> [[Int]]
superSet r t = go r 0 where
    go 0 ignored = if ignored == 0 then [[]] else [[]]
    go r ignored = do
        x <- [0..t]
        xs <- go (r-1) (ignored+x)
        return (x:xs)

...但superSet 0 t = [[]] superSet r t = do x <- [0..t] xs <- superSet (r-1) t return (x:xs) GHC过于聪明,并注意到它可以共享递归调用。

答案 1 :(得分:4)

完全替代方法是进行一些组合分析。这是eSet r t所做的过程,尽我所能:

  1. 最多选择r个元素,而无需从一组尺寸t替换。
  2. 通过交错标记值将序列填充到r的长度。
  3. 因此,让我们只计算执行这些步骤的方法,而不是实际执行它们。我们将引入一个新参数s,它是步骤(1)生成的序列的长度(因此我们知道它具有s <= rs <= t)。在没有替换大小s的情况下绘制元素时,有多少个大小为t的序列?好吧,第一个元素有t个选项,第二个元素有t-1个选项,第三个元素有t-2个选项,......

    -- sequencesWithoutReplacement is a very long name!
    seqWORepSize :: Integer -> Integer -> Integer
    seqWORepSize s t = product [t-s+1 .. t]
    

    然后我们想要将序列填充到r的长度。我们将从我们的序列中选择s - 长序列中的r个位置,其余的将是哨兵。有多少种方法可以做到这一点?这是一个着名的组合运算符choose

    choose :: Integer -> Integer -> Integer
    choose r s = product [r-s+1 .. r] `div` product [2 .. s]
    

    生成给定长度的填充序列的方法数量只是这两个数字的乘积,因为“插入什么值”和“插入值的位置”的选择可以完全独立。

    paddedSeqSize :: Integer -> Integer -> Integer -> Integer
    paddedSeqSize r s t = seqWORepSize s t * (r `choose` s)
    

    现在我们已经完成了很多工作。只需迭代所有可能的序列长度并加起来paddedSeqSize

    eSetSize :: Integer -> Integer -> Integer
    eSetSize r t = sum $ map (\s -> paddedSeqSize r s t) [0..r]
    

    我们可以在ghci中尝试:

    > :set +s
    > map length $ [eSet 1 1, eSet 4 4, eSet 6 4, eSet 4 6]
    [2,209,1045,1045]
    (0.13 secs, 26,924,264 bytes)
    > [eSetSize 1 1, eSetSize 4 4, eSetSize 6 4, eSetSize 4 6]
    [2,209,1045,1045]
    (0.01 secs, 120,272 bytes)
    

    这种方式明显更快,并且对内存更友好。实际上,我们可以进行查询并获得有关eSet的答案,这些答案我们永远无法逐个计算,例如。

    > length . show $ eSetSize 1000 1000
    2594
    (0.26 secs, 909,746,448 bytes)
    
    祝你好运一次只计算10 ^ 2594。 = P

    修改
    这个想法也可以适用于生成填充序列本身,而不仅仅计算有多少。首先,我发现自己定义了一个方便的实用程序,用于挑选列表中的各个元素并报告剩余的内容:

    zippers :: [a] -> [([a], a, [a])]
    zippers = go [] where
        go ls [] = []
        go ls (h:rs) = (ls, h, rs) : go (h:ls) rs
    

    现在,无需替换的序列可以通过从剩余部分重复选择一个元素来完成。

    seqWORep :: Int -> [a] -> [[a]]
    seqWORep 0 _  = [[]]
    seqWORep n xs = do
        (ls, y, rs) <- zippers xs
        ys <- seqWORep (n-1) (ls++rs)
        return (y:ys)
    

    一旦我们得到一个序列,我们可以通过产生sentinel值的所有交错来填充它到所需的大小,如下所示:

    interleavings :: Int -> a -> [a] -> [[a]]
    interleavings 0 _ xs = [xs]
    interleavings n z [] = [replicate n z]
    interleavings n z xs@(x:xt) =  map (z:) (interleavings (n-1) z xs)
                                ++ map (x:) (interleavings  n    z xt)
    

    最后,顶级函数只委托给seqWORepinterleavings

    eSet :: Int -> Int -> [[Int]]
    eSet r t = do
        s <- [0..r]
        xs <- seqWORep s [1..t]
        interleavings (r-s) 0 xs
    

    在我的测试中,这个修改过的eSet在使用和不使用优化的情况下具有良好的平坦内存使用率;不会产生任何需要稍后filter输出的虚假元素,因此比原始提案更快;与依赖于欺骗GHC的答案相比,对我来说看起来很自然。一系列不错的房产!

答案 2 :(得分:1)

重新阅读LYaH的部分并考虑@ daniel-wagners回答monadically组成听起来像是值得再试一次。新代码总内存是平坦的,可以使用和不使用-O2优化。

来源:

import Control.Monad
import System.Environment
import System.Exit

allowed :: [Int] -> Bool
allowed a = allowed' [] a
    where
        allowed' [ ] [ ]       = False
        allowed' [ ] [0]       = True
        allowed'  _  [ ]       = True
        allowed' acc (0:aTail) = allowed' acc aTail
        allowed' acc (a:aTail)
             | a `elem` acc    = False
             | otherwise       = allowed' (a:acc) aTail

branch :: Int -> [Int] -> [[Int]]
branch t x  = filter allowed [n:x | n <- [0..t]]

eSet :: Int -> Int -> [[Int]]
eSet r t = return [] >>= foldr (<=<) return (replicate r (branch t))

main :: IO ()
main = do
    args <- getArgs
    case args of
        [rArg, tArg] ->
            print $ length $ eSet (read rArg) (read tArg)
        [rArg, tArg, "set"] ->
            print $          eSet (read rArg) (read tArg)
        _ -> die "Usage: eSet r r set <set optional>"

具有monadic函数组合的版本测试速度更快,没有内存问题......

$ ./eSetMonad 10 5 +RTS -sstderr
63591
 289,726,000 bytes allocated in the heap
     997,968 bytes copied during GC
      63,360 bytes maximum residency (2 sample(s))
      24,704 bytes maximum slop
           2 MB total memory in use (0 MB lost due to fragmentation)

                                 Tot time (elapsed)  Avg pause  Max pause
Gen  0       553 colls,     0 par    0.008s   0.008s     0.0000s    0.0002s
Gen  1         2 colls,     0 par    0.000s   0.000s     0.0002s    0.0003s

INIT    time    0.000s  (  0.000s elapsed)
MUT     time    0.426s  (  0.429s elapsed)
GC      time    0.009s  (  0.009s elapsed)
EXIT    time    0.000s  (  0.000s elapsed)
Total   time    0.439s  (  0.438s elapsed)

%GC     time       2.0%  (2.0% elapsed)

Alloc rate    680,079,893 bytes per MUT second

Productivity  98.0% of total user, 98.3% of total elapsed