以下代码块中的mapAndSum
函数将map
和sum
组合在一起(不要紧,在主函数中应用另一个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."
答案 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_r2fb
和lvl17_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}}可能会被动态分配,因此有资格获得特殊处理。但是,无论如何,代码看起来可能会非常不同。