在GHC解释器中冗余使用seq时出现空间泄漏

时间:2012-05-29 05:12:06

标签: haskell ghc space-leak

我将此代码键入解释器并快速占用内存:

last [1..10^7] `seq` ()

我不明白为什么这需要超过O(1)空间。如果我这样做(这应该是相同的,因为Show强制弱头正常形式,所以seq是多余的?):

last [1..10^7]

......工作正常。

我无法在翻译之外重现这种情况。

这里发生了什么?


以下是一些测试用例: http://hpaste.org/69234

注意事项:

  • 通过在解释器中运行,我加载wtf.hs而不编译它,并在ghci中键入wtf<n>
  • 通过编译,我做ghc --make wtf.hs && ./wtf
  • last可以替换带有严格累加器的sum或者在列表中找到max元素的函数,并且仍然会发生空间泄漏
  • 使用$!代替seq时,我没有看到此行为。
  • 我尝试添加一个虚拟()参数,因为我认为这可能是CAF问题。没有任何改变。
  • Enum上的函数可能不是问题,因为我可以使用wtf5及更高版本重现行为,而根本不使用Enum
  • NumIntInteger可能不是问题,因为我可以在wtf14wtf16中重现没有它们的行为。< / LI>

我尝试使用Peano算法重现问题,将列表和整数排除在等式之外(在10 ^ 9结束时取出),但遇到其他共享/空间泄漏问题我不明白实施*

2 个答案:

答案 0 :(得分:15)

我们需要查看整数enumFromTo的实例,最后:

last []                 =  errorEmptyList "last"
last (x:xs)             =  last' x xs
  where last' y []     = y
        last' _ (y:ys) = last' y ys

在GHC.Enum中定义为:

enumFrom x             = enumDeltaInteger  x 1
enumFromThen x y       = enumDeltaInteger  x (y-x)
enumFromTo x lim       = enumDeltaToInteger x 1 lim

,其中

enumDeltaInteger :: Integer -> Integer -> [Integer]
enumDeltaInteger x d = x `seq` (x : enumDeltaInteger (x+d) d)
-- strict accumulator, so
--     head (drop 1000000 [1 .. ]
-- works

enumDeltaToInteger :: Integer -> Integer -> Integer -> [Integer]
enumDeltaToInteger x delta lim
  | delta >= 0 = up_list x delta lim
  | otherwise  = dn_list x delta lim

up_list :: Integer -> Integer -> Integer -> [Integer]
up_list x0 delta lim = go (x0 :: Integer)
                where
                    go x | x > lim   = []
                         | otherwise = x : go (x+delta)
正如预期的那样,

last完全是懒惰的。

对于Integer Enum类,我们有enumFrom的严格累加器(显式)。在有界的情况下(例如[1..n]),它会调用enumDeltaToInteger然后调用up_list,它会使用工作人员展开列表,直到达到其限制。

up_list帮助x中的go严格(请参阅与lim的比较)。

当在GHCi中运行时,没有一个被优化,在返回()之前,对enumFromTo进行了天真的调用。

let
  it_ax6 :: ()      
  it_ax6 =
    case last
           @ GHC.Integer.Type.Integer
           (GHC.Enum.enumFromTo
              @ GHC.Integer.Type.Integer
              GHC.Num.$fEnumInteger
              (GHC.Integer.smallInteger 1)
              (GHC.Real.^
                 @ GHC.Integer.Type.Integer
                 @ GHC.Integer.Type.Integer
                 GHC.Num.$fNumInteger
                 GHC.Real.$fIntegralInteger
                 (GHC.Integer.smallInteger 10)
                 (GHC.Integer.smallInteger 7)))
    of _ -> GHC.Unit.()
in
  GHC.Base.thenIO
    @ ()
    @ [()]
    (System.IO.print @ () GHC.Show.$fShow() it_ax6)
    (GHC.Base.returnIO
       @ [()] (GHC.Types.: @ () it_ax6 (GHC.Types.[] @ ())))

那么,为什么我们保留seq案例中的列表,而不是常规案例?常规案例在constrant空间中很好地运行,依赖于enumFromToInteger的{​​{1}}的懒惰。该案例的GHCi核心如下:

last

所以这几乎是相同的,区别在于:

    let { it_aKj :: GHC.Integer.Type.Integer [LclId, Unf=Unf{Src=<vanilla>, TopLvl=False, Arity=0, Value=False, ConLike=False, Cheap=False, Expandable=False, Guidance=IF_ARGS [] 170 0}] it_aKj = GHC.List.last @ GHC.Integer.Type.Integer (GHC.Enum.enumFromTo @ GHC.Integer.Type.Integer GHC.Num.$fEnumInteger (GHC.Integer.smallInteger 1) (GHC.Real.^ @ GHC.Integer.Type.Integer @ GHC.Integer.Type.Integer GHC.Num.$fNumInteger GHC.Real.$fIntegralInteger (GHC.Integer.smallInteger 10) (GHC.Integer.smallInteger 7))) } in GHC.Base.thenIO @ () @ [()] (System.IO.print @ GHC.Integer.Type.Integer GHC.Num.$fShowInteger it_aKj) (GHC.Base.returnIO @ [()] (GHC.Types.: @ () (it_aKj `cast` (UnsafeCo GHC.Integer.Type.Integer () :: GHC.Integer.Type.Integer ~ ())) (GHC.Types.[] @ ()))) 版本中
  • seq被迫last (enumFromTo ..)
  • 在常规版本中,它是一个懒惰的case
  • let版本中,计算该值然后丢弃,产生seq - 没有看到结果
  • 在常规情况下,会进行检查和显示。

奇怪的是,没有什么神奇之处:

()

使其保留值。

正如我们所见,let x = case last (enumFromTo 1 n) of _ -> () 的实现在其累加器中是严格的(因为它与up_list进行比较,并且列表是懒散展开的 - 因此lim应该能够消耗它在恒定的空间)。手写表达确认了这一点。

执行ghci执行的堆配置文件显示保留整个列表:

enter image description here

它告诉我们至少它不是一串thunk,而是整个列表正在严格建立并保持不变,直到被丢弃。

所以神秘之处在于:什么是ghci中last的列表参数,而不是ghc?

我怀疑ghci的一些内部(或微妙)细节 - 我认为这是值得的ghci票。

答案 1 :(得分:1)

我认为@n.m是对的。 没有什么东西强迫列表中的,所以1 + 1 + 1 + 1 + ... thunk最终会杀死空间。

我将进行快速测试。