【问题标题】:Why does memoized function consume so much memory in Haskell?为什么在 Haskell 中 memoized 函数会消耗这么多内存?
【发布时间】:2019-05-14 13:46:56
【问题描述】:

我写了一段 Haskell 代码来计算 Collatz chain 的长度。给定一个数 n,如果 n 是偶数,则序列中的下一个数是 n/2;如果 n 是奇数,则为 3*n+1。序列在收敛到 1 时结束。我想从某个输入数字以下的任何数字开始计算最长链的长度。

我尝试使用记忆函数来实现长度计算,因为我预计需要从一些数字开始的链长度。因此,从 726 开始的链的长度将只是 1 + 从 363 开始的链的长度,这已经计算过了。我的代码如下所示。

collatz :: Int -> Int
collatz n
    | even n = n `div` 2
    | otherwise = 3 * n + 1

collatzLength :: Int -> Int
collatzLength = (fmap len [0 ..] !!)
    where len 0 = 0
          len 1 = 1
          len n = 1 + (collatzLength . collatz $ n)

maxLengthBelow :: Int -> Int
maxLengthBelow = foldl1 max . fmap collatzLength . enumFromTo 1

main :: IO()
main = print $ maxLengthBelow 10000

此代码有效,但占用大量内存。在分析它时,输入 10000 运行 mainlen 只被调用了 21664 次,正如预期的那样,但程序需要 16 秒和 4.5Gb 的内存!是什么占用了所有的内存?我本来希望 memoized 函数能够产生快速、低内存的解决方案。

【问题讨论】:

    标签: haskell


    【解决方案1】:

    让 Collat​​z 序列如此有趣的原因之一是,有一些小的起始种子会带你一路,在通往 1 的途中进入大气层。特别是,9663 一直到 27114424在它崩溃之前——这是一个很长的记忆列表!

    而且,无论如何,我希望你的记忆列表每个元素使用三个机器字:一个用于Int 上的I# 构造函数,一个用于包含的数字,一个用于(:) 构造函数.让我们问一下存储27114424个元素需要多少空间,然后:

    > 27114424 * (64*3) / 1024 {-Kb-} / 1024 {-Mb-} / 1024 {-Gb-}
    4.8484368324279785
    

    所以 4.5Gb 听起来不错,甚至可能有点低。

    【讨论】:

    • 奇怪的是:在我的机器上,这个程序只占用 2Gb(2.7Gb 没有优化)。我想知道为什么! (绝对是64位的机器,我可以确认Int确实可以忠实地存储2^63-1。)
    • 但是惰性求值不应该只需要在需要时计算 collat​​zLength 元素的值吗?最大的数字可能是 27114424,但永远不应该构造 27114423。
    • @mentoc3000 当然可以。但是您仍然需要构建列表的主干——尽管您可能不需要同时存储 I# 和包含的元素的编号,但您必须存储一个说明如何计算的 thunk如果以后需要这个数字,我怀疑 thunk 会比 Int 本身占用更少的空间。
    • @mentoc3000 如果您使用trie 而不是列表作为您的记忆结构,您应该会看到巨大的改进。
    • @luqui 当然。但它在我的机器上使用的空间小于,它比存储所有Ints 所需的空间少,而且没有(可能更大)thunk!
    猜你喜欢
    • 2019-09-30
    • 2018-10-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-03
    • 1970-01-01
    • 2010-11-09
    相关资源
    最近更新 更多