为什么这个Haskell代码没有耗尽堆?

时间:2016-11-12 11:39:58

标签: haskell memory-leaks

此代码对在顶级指定的两个(相互取消)无限列表的内容进行求和:

{-# language BangPatterns #-}
module Main where

unending1 :: [Int]
unending1 = cycle [1]

unending2 :: [Int]
unending2 = cycle [negate 1]

main :: IO ()
main = do
    let summator :: Int -> [Int] -> [Int] -> Int
        summator !acc (i1 : rest1) (i2 : rest2) =
            if acc > 100
            then acc -- never happens
            else summator (acc+i1+i2) rest1 rest2
    print (summator 0 unending1 unending2)

我编译此代码时没有优化和低堆大小,如下所示:

ghc -O0 -with-rtsopts="-M10m" Main.hs

我的直觉是这段代码会导致内存泄漏,因为求和函数会尝试“实现”两个列表,并且它们的头部都处于顶层,所以它们不会被丢弃

然而,当我执行程序时,它似乎无限期地运行而没有问题。

我错在哪里?

编辑。使用-ddump-simpl检查生成的Core,列表似乎仍保持在最高级别。例如:

Result size of Tidy Core = {terms: 77, types: 52, coercions: 0}

-- RHS size: {terms: 5, types: 3, coercions: 0}
unending1 :: [Int]
[GblId, Str=DmdType]
unending1 =
  cycle
    @ Int (GHC.Types.: @ Int (GHC.Types.I# 1#) (GHC.Types.[] @ Int))

2 个答案:

答案 0 :(得分:4)

正如Chi回答的那样,由于cycle的定义,您的代码在恒定的空间中运行。

但是,即使使用ghc -O0,它也会在cycle xs = xs ++ cycle xs的恒定空间中运行,因为顶级thunk(常量应用形式,CAF-s)可以进行垃圾回收。闭包的信息表有“静态引用表”,它列出了静态闭包,以便

  • 闭包的代码提到了他们
  • 他们或者是顶级的thunk,或者他们的代码是传递的顶级thunks

文档here。如果无法从GC根(包括线程状态对象的堆栈,也就是在我们的情况下执行中的main的闭包)到达顶级thunks,则它们指向的堆对象将被丢弃。

答案 1 :(得分:3)

来自cycle :: [a] -> [a] cycle [] = errorEmptyList "cycle" cycle xs = xs' where xs' = xs ++ xs'

xs'

请注意,递归不涉及函数调用,而是列表值cycle

当完全强制时,它应该在内存中表示为循环链表,带有向后指针。然后只需要有限的内存。

尝试例如定义自己的cycle' xs = xs ++ cycle' xs

> let list1 :: [Int] ; list1 = cycle [1,2,3]
> list1 !! (4*10^9)
2

因为GHC不进行自动记忆,所以这应该在内存中生成一个无界限列表。

实际上,即使在GHCi(未经优化)中,我的机器上仍然保持在70M以下

> let list2 :: [Int] ; list2 = cycle' [1,2,3]
> list2 !! (4*10^7)
2

虽然爆炸(> 1GB):

sig Person {father: Person}

fact {
    no p: Person | p in p.^father
}

pred Show {}

run Show