我在CodeReview中发布了同样的问题但未能得到答案。所以我在这里试试运气。
这是我的一个程序,它使用memoization和array来提高性能和内存使用率。性能似乎令人满意,但内存使用是荒谬的,我无法弄清楚错误:
{-# 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
的索引数组,其中每个单元格包含通过应用Collatz公式从索引到1的链接长度。
但是这种方法的内存使用率如此之高。以下是分析器结果(ghc选项-prof -fprof-auto -rtsopts
,运行时选项+RTS -p
,n == 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代码中的时间和内存效率低下。具体来说,我不知道为什么这个带有尾递归和爆炸模式的代码会消耗这么多内存。
与-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兆。那还是太大了。
UPDATE2完整代码
{-# 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
答案 0 :(得分:2)
正如CodeReview提示的另一个答案,一个500000元素的盒装数组可以提供大约20MB的内存,不过它不仅仅是阵列而是很多东西:
虽然你把爆炸模式放在每个地方,但数组初始化本身就是一个懒惰的折叠器:
-- 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
的引用,因此genColtzArr
在完全评估llist
之前不会进行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
编译)