无可辩驳的模式在递归时不会泄漏内存,但为什么呢?

时间:2012-07-24 07:11:49

标签: haskell ghc lazy-evaluation accumulator space-leak

以下代码块中的mapAndSum函数将mapsum组合在一起(不要紧,在主函数中应用另一个sum,它只是服务使输出紧凑)。 map是懒惰计算的,而sum是使用累积参数计算的。我们的想法是,map的结果可以在没有内存中的完整列表的情况下使用,并且(仅)之后sum可以“免费”使用。 main函数表示在调用mapAndSum时我们遇到了无可辩驳模式的问题。让我解释一下这个问题。

根据Haskell标准,无可辩驳的模式示例let (xs, s) = mapAndSum ... in print xs >> print s被转换为

(\ v ->    print (case v of { (xs, s) -> xs })
        >> print (case v of { (xs, s) -> s }))
$ mapAndSum ...

因此,两个print调用都带有对整个对的引用,这导致整个列表保存在内存中。

我们(我的同事Toni Dietze和我)使用明确的case语句(比较“bad”vs“good2”)来解决这个问题。顺便说一句,发现这一点花了我们相当多的时间..!

现在,我们不明白的是双重的:

  • 为什么mapAndSum首先起作用?它还使用无可辩驳的模式,因此它也应该将整个列表保留在内存中,但显然不会。将let转换为case会使函数完全不受欢迎(到堆栈溢出的程度;没有双关语)。

    我们查看了GHC生成的“核心”代码,但就我们所能解释的而言,它实际上包含与上述let相同的翻译。所以在这里没有任何线索,反而更加困惑。

  • 为什么“不好?”在关闭优化时工作,但在打开优化时不工作?

关于我们实际应用的一句话:我们希望实现大型文本文件的流处理(格式转换),同时累积一些值然后写入单独的文件。如上所述,我们取得了成功,但仍然存在两个问题,答案可能会提高我们对即将到来的任务和问题的GHC的理解。

谢谢!

{-# LANGUAGE BangPatterns #-}

-- Tested with The Glorious Glasgow Haskell Compilation System, version 7.4.2.

module Main where


import Control.Arrow (first)
import Data.List (foldl')
import System.Environment (getArgs)


mapAndSum :: Num a => (a -> b) -> [a] -> ([b], a)
mapAndSum f = go 0
  where go !s (x : xs) = let (xs', s') = go (s + x) xs in (f x : xs', s')
                       -- ^ I have no idea why it works here. (TD)
        go !s []       = ([], s)


main :: IO ()
main = do
  let foo  = mapAndSum (^ (2 :: Integer)) [1 .. 1000000 :: Integer]
  let sum' = foldl' (+) 0
  args <- getArgs
  case args of
    ["bad" ]  -> let (xs, s) = foo in print (sum xs) >> print s
    ["bad?"]  -> print $ first sum' $ foo
              -- ^ Without ghc's optimizer, this version is as memory
              -- efficient as the “good” versions
              -- With optimization “bad?” is as bad as “bad”. Why? (TD)
    ["good1"] -> print $ first' sum' $ foo
                   where first' g (x, y) = (g x, y)
    ["good2"] -> case foo of
                    (xs, s) -> print (sum' xs) >> print s
    ["good3"] -> (\ (xs, s) -> print (sum' xs) >> print s) $ foo
    _ -> error "Sorry, I do not understand."

1 个答案:

答案 0 :(得分:18)

让我首先回答为什么mapAndSome可以正常工作:你看到的(很可能)是Philip Wadler在“Fixing some space leaks with a garbage collector”中描述的优化效果。简短摘要:如果垃圾收集器看到fst x形式的thunk并且x已经被评估给元组构造函数,例如(y,z),它会fst x取代y,如果在其他地方没有引用z,可能会释放s'

在您的代码中,go一旦foo的结果被评估为元组并且在一轮GCing之后,将不保留对元组的引用但将被累积替换参数。

现在让我们通过调查核心看看其他模式。 foo_r2eT :: ([Type.Integer], Type.Integer) foo_r2eT = case $wgo_r2eP mapAndSum1 lvl2_r2eS of _ { (# ww1_s2d7, ww2_s2d8 #) -> (ww1_s2d7, ww2_s2d8) } 绑定编译为:

"bad"

以下是lvl18_r2fd案例中的代码(case eqString ds_dyA lvl18_r2fd of _ { False -> $wa_s2da new_s_a14o; True -> case ds1_dyB of _ { [] -> case Handle.Text.hPutStr2 Handle.FD.stdout lvl17_r2fc True new_s_a14o of _ { (# new_s1_X15h, _ #) -> Handle.Text.hPutStr2 Handle.FD.stdout lvl16_r2fb True new_s1_X15h }; : ipv_sIs ipv1_sIt -> $wa_s2da new_s_a14o } 是“不好”):

lvl17_r2fc

我们可以看到模块级别的两个值,lvl16_r2fblvl17_r2fc :: String [GblId] lvl17_r2fc = case foo_r2eT of _ { (xs_Xqp, s_Xq9) -> $w$cshowsPrec 0 (Data.List.sum_sum' xs_Xqp Data.List.genericDrop2) ([] @ Char) } lvl16_r2fb :: String [GblId] lvl16_r2fb = case foo_r2eT of _ { (xs_apS, s_Xqp) -> $w$cshowsPrec 0 s_Xqp ([] @ Char) } ,这是他们的代码:

lvl17_r2fc

为什么它们在模块级别绑定,而不是在表达式中?这是延迟提升的影响,这是另一种增加共享的优化,因此有时会对空间性能产生负面影响。有关此效果的另一种情况,请参见GHC ticket 719

所以会发生的是foo的评估会导致lvl16_r2fb被评估,而左边的条目会被懒惰地打印出来。不幸的是,"good1"仍然存在并且保留了完整的元组。而且因为垃圾收集器(似乎)没有看到这是一个选择器thunk,Wadler的优化并没有开始。

相比之下,以下是lvl8_r2f1 a.k.a. case eqString ds_dyA lvl8_r2f1 of _ { False -> $wa2_s2dI w3_s2cF; True -> case ds1_dyB of _ { [] -> Handle.Text.hPutStr2 Handle.FD.stdout lvl7_r2f0 True w3_s2cF; : ipv_sHg ipv1_sHh -> $wa2_s2dI w3_s2cF } } } in 的代码:

lvl7_r2f0 :: String
[GblId]
lvl7_r2f0 =
  case foo_r2eT of _ { (x_af6, y_af7) ->
  show_tuple
    (:
       @ ShowS
       (let {
          w2_a2bY [Dmd=Just L] :: Type.Integer

          w2_a2bY = lgo_r2eU mapAndSum1 x_af6 } in
        \ (w3_a2bZ :: String) ->
          $w$cshowsPrec 0 w2_a2bY w3_a2bZ)
       (:
          @ ShowS
          (\ (w2_a2bZ :: String) ->
             $w$cshowsPrec 0 y_af7 w2_a2bZ)
          ([] @ ShowS)))
    ([] @ Char)
  }

其中打印值为此字符串:

"good2"

正如您所看到的,元组仅被拆开一次,并且使用了两个值。所以没有什么是整个元组,它可以被垃圾收集。类似于"good3""bad?"

现在到 case eqString ds_dyA (unpackCString# "bad?") of _ { False -> fail2_dyN realWorld#; True -> case ds1_dyB of _ { [] -> $ @ (Type.Integer, Type.Integer) @ (IO ()) (System.IO.print @ (Type.Integer, Type.Integer) $dShow_rzk) ($ @ ([Type.Integer], Type.Integer) @ (Type.Integer, Type.Integer) (Control.Arrow.first @ (->) Control.Arrow.$fArrow(->) @ [Type.Integer] @ Type.Integer @ Type.Integer sum'_rzm) foo_rzl); : ipv_szd ipv1_sze -> fail2_dyN realWorld# } } } in :在未经优化的情况下,我们得到这段代码:

first

*** w_r2f2 :: Type.Integer w_r2f2 = case foo_r2eT of _ { (x_aI1, y_aI2) -> lgo_r2eU mapAndSum1 x_aI1 } lvl9_r2f3 :: String -> String [GblId, Arity=1] lvl9_r2f3 = \ (w2_a2bZ :: String) -> $w$cshowsPrec 0 w_r2f2 w2_a2bZ w1_r2f4 :: Type.Integer w1_r2f4 = case foo_r2eT of _ { (x_aI6, y_aI7) -> y_aI7 } lvl10_r2f5 :: String -> String [GblId, Arity=1] lvl10_r2f5 = \ (w2_a2bZ :: String) -> $w$cshowsPrec 0 w1_r2f4 w2_a2bZ lvl11_r2f6 :: [ShowS] [GblId] lvl11_r2f6 = : @ ShowS lvl10_r2f5 ([] @ ShowS) lvl12_r2f7 :: [ShowS] [GblId] lvl12_r2f7 = : @ ShowS lvl9_r2f3 lvl11_r2f6 lvl13_r2f8 :: ShowS [GblId] lvl13_r2f8 = show_tuple lvl12_r2f7 lvl14_r2f9 :: String [GblId] lvl14_r2f9 = lvl13_r2f8 ([] @ Char) 通过first使用可反射模式,因此生成了垃圾收集器处理得好的选择器类型。

在优化的情况下,事情有点分散,但无论如何这里是相关代码(最后一个值是正在打印的值):

case foo_r2eT

内联使用了w1_rf24。我们看到两次调用foo,因此尽管w1_rf24看起来像一个选择器,但这很容易发生空间泄漏(所以我希望运行时应用Wadler的优化)。也许它不适用于静态的thunk?实际上implementation,如果是最新的,只讨论动态分配的选择器thunk。因此,如果您的{{1}}不是模块级别的值(或者说是懒惰的一个),而是依赖于某些输入,{{1}}可能会被动态分配,因此有资格获得特殊处理。但是,无论如何,代码看起来可能会非常不同。