【问题标题】:Memoized Collatz sequence记忆的 Collat​​z 序列
【发布时间】:2016-03-09 13:20:08
【问题描述】:

我在 CodeReview 中发布了同样的问题,但没有得到答案。所以我在这里碰运气。

这是我利用记忆和数组来提高性能和内存使用率的程序之一。性能似乎令人满意,但内存使用情况很荒谬,我不知道出了什么问题:

{-# LANGUAGE BangPatterns #-}
import Data.Functor
import Data.Array (Array)
import qualified Data.Array as Arr
import Control.DeepSeq

genColtzArr n = collatzArr
    where collatzArr = Arr.array (1, n) $ take n $ map (\v -> (v, collatz v 0)) [1..] 
          collatz 1 !acc  = 1 + acc
          collatz !m !acc
              | even m    = go (m `div` 2) acc
              | otherwise = go (3 * m + 1) acc
              where go !l !acc
                      | l <= n    = let !v = collatzArr Arr.! l in 1 + acc + v
                      | otherwise = collatz l $ 1 + acc

collatz 这里的意思是this guy。这个函数应该接收一个数字n,然后返回一个从1到n的数组索引,其中每个单元格包含应用Collat​​z公式从索引到1的链接长度。

但是这种方法的内存使用率很高。这是分析器结果(ghc 选项-prof -fprof-auto -rtsopts,运行时选项+RTS -pn == 500000):

total alloc = 730,636,136 bytes  (excludes profiling overheads)

COST CENTRE              MODULE  %time %alloc

genColtzArr.collatz      Main     40.4   34.7
genColtzArr.collatz.go   Main     25.5   14.4


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

      genColtzArr                Main                      105           1    0.0    0.0    74.7   72.1
       genColtzArr.collatzArr    Main                      106           1    8.0   20.8    74.7   72.1
        genColtzArr.collatzArr.\ Main                      107      500000    0.9    2.2    66.8   51.3
         genColtzArr.collatz     Main                      109     1182582   40.4   34.7    65.9   49.1
          genColtzArr.collatz.go Main                      110     1182581   25.5   14.4    25.5   14.4

请注意,-O2 不是所需的答案。我想弄清楚这个程序有什么问题,一般来说,我应该如何发现 Haskell 代码中的时间和内存效率低下。具体来说,我不知道为什么这段带有尾递归和 bang 模式的代码会消耗这么多内存。

更新1:

-s 相同的代码会产生这样的结果:

   1,347,869,264 bytes allocated in the heap
     595,901,528 bytes copied during GC
     172,105,056 bytes maximum residency (7 sample(s))
         897,704 bytes maximum slop
             315 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0      2408 colls,     0 par    0.412s   0.427s     0.0002s    0.0075s
  Gen  1         7 colls,     0 par    0.440s   0.531s     0.0759s    0.1835s

  INIT    time    0.000s  (  0.000s elapsed)
  MUT     time    0.828s  (  0.816s elapsed)
  GC      time    0.852s  (  0.958s elapsed)
  RP      time    0.000s  (  0.000s elapsed)
  PROF    time    0.000s  (  0.000s elapsed)
  EXIT    time    0.004s  (  0.017s elapsed)
  Total   time    1.684s  (  1.791s elapsed)

  %GC     time      50.6%  (53.5% elapsed)

  Alloc rate    1,627,861,429 bytes per MUT second

  Productivity  49.4% of total user, 46.4% of total elapsed

所以它需要 300 兆。那还是太大了。

更新2

完整代码

{-# LANGUAGE BangPatterns #-}
import Data.Functor
import Data.Array (Array)
import qualified Data.Array as Arr
import Control.DeepSeq

genColtzArr n = collatzArr
    where collatzArr = Arr.array (1, n) $ take n $ map (\v -> (v, collatz v 0)) [1..] 
          collatz 1 !acc  = 1 + acc
          collatz !m !acc
              | even m    = go (m `div` 2) acc
              | otherwise = go (3 * m + 1) acc
              where go !l !acc
                      | l <= n    = let !v = collatzArr Arr.! l in 1 + acc + v
                      | otherwise = collatz l $ 1 + acc


genLongestArr n = Arr.array (1, n) llist
    where colatz = genColtzArr n
          llist  = (1, 1):zipWith (\(n1, a1) l2 -> 
                                    let l1 = colatz Arr.! a1
                                     in (n1 + 1, if l2 < l1 then a1 else n1 + 1)) 
                                  llist (tail $ Arr.elems colatz)


main :: IO ()
main = getLine >> do
    ns <- map read <$> lines <$> getContents
    let m          = maximum ns
    let lar        = genLongestArr m
    let iter []    = return ()
        iter (h:t) = (putStrLn $ show $ lar Arr.! h) >> iter t
    iter ns

【问题讨论】:

  • “请注意,-O2 不是所需的答案。” - 你是说你没有编译优化?如果您希望您的 Haskell 代码接近快速,您必须进行优化编译。任何时候-O0 代码快速运行基本上都是侥幸。接下来您可以查看optimized core
  • @user2407038 我已经展示了我的 ghc 参数。我需要了解我的程序正在发生什么。它也会对我的其他程序有所帮​​助。我并不期望每次我尝试改进我的程序时,我都必须来这里提问。所以对于haskell,人们很容易说try -O2,不,我想专注于改进程序本身,而不是依赖一些优化。
  • 我对 -O0 Haskell 程序的操作语义一无所知 - 编译器会随心所欲地执行任何操作,因此无法预测会触发哪个核心传递。您唯一的办法是查看-v3 的输出和最终生成的核心。如果代码看起来愚蠢可笑地糟糕..可能是因为您确实必须将-O2 与 GHC 一起使用。没有阴谋,-O2 不是以某种方式“作弊” - 您的程序可能有 no 问题并且编译器会生成一个错误的程序,因为-O0 的意思是“做这个快”,而不是“做这口井”。
  • 你的程序中存在各种空间泄漏:collatzArrcollatz 之间的相互递归,一个不能被即时 GC 的大而长的惰性数组等等,但是你的main?评估了哪些 IO 操作?
  • 只要您不提供main,就不可能以有助于他人的方式重现您的行为。例如,在不更改代码的情况下,我的工作效率提高了 70%,而如果我使用 main = print $ maximum $ genColtzArr 500000 和 128 MB 的总内存使用量,你的工作效率不到 50%。

标签: haskell memoization collatz


【解决方案1】:

正如 CodeReview 上的另一个答案所暗示的那样,一个 500000 个元素的盒装数组占用约 20MB 内存是可以的,但它不仅是数组,而且还有很多东西:

虽然你在每个地方都放置了 bang 模式,但数组初始化本身就是一个惰性折叠器:

-- from GHC.Arr
array (l,u) ies
    = let n = safeRangeSize (l,u)
      in unsafeArray' (l,u) n
                      [(safeIndex (l,u) n i, e) | (i, e) <- ies]

unsafeArray' :: Ix i => (i,i) -> Int -> [(Int, e)] -> Array i e
unsafeArray' (l,u) n@(I# n#) ies = runST (ST $ \s1# ->
    case newArray# n# arrEleBottom s1# of
        (# s2#, marr# #) ->
            foldr (fill marr#) (done l u n marr#) ies s2#)

因此,除非您评估数组的最后一位,否则它会引用初始化中使用的列表。通常可以在评估数组时对列表进行 GC,但在您的情况下,相互引用和自引用扰乱了常见的 GC 模式。

  • llist 是自引用来生成每个元素的,所以在你评估它的最后一个元素之前它不会被 GC 处理
  • 它还包含对 genColtzArr 的引用,因此在完全评估 llist 之前,genColtzArr 不会被 GC 处理
  • 您可能认为 collatz 是尾递归的,但事实并非如此,它与 collatzArr 相互递归,所以在完全评估之前它们都不会被 GC 处理

所有的东西结合起来,你的程序将在内存中保留三个 500000 元素的类似列表的结构,并产生约 80MB 的峰值堆大小。


解决方案

显而易见的解决方案是在将每个数组/列表用于另一个之前将其强制为正常形式,这样您就不会在内存中保留相同数据的多个副本。

genLongestArr :: Int -> Array Int Int
genLongestArr n =
  let collatz = genColtzArr n
  -- deepseq genColtzArr before mapping over it
  -- this is equivalent to your recursive definition
  in collatz `deepseq` (Arr.listArray (1,n) $ fmap fst $ scanl' (maxWith snd) (0, 0) $ Arr.assocs collatz)

maxWith :: Ord a => (b -> a) -> b -> b -> b
maxWith f b b' = case compare (f b) (f b') of
  LT -> b'
  _  -> b

main:

-- deepseq lar before mapping over it
-- this is equivalent to your iter loop
lar `deepseq` mapM_ (print . (lar Arr.!)) ns

genColtzArr 什么都做不了,它使用自己来记忆,所以相互递归是必要的。

现在堆图的峰值约为 20MB:

(免责声明:此答案中的所有程序均使用 -O0 编译)

【讨论】:

    猜你喜欢
    • 2013-03-07
    • 2016-01-29
    • 2016-05-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-19
    • 2014-12-14
    相关资源
    最近更新 更多