我玩弄定义以更好地理解评估模型,并为列表的长度写了两个。
天真的定义:
len :: [a] -> Int
len [] = 0
len (_:xs) = 1 + len xs
strict(和tail-recursive)定义:
slen :: [a] -> Int -> Int
slen [] n = n
slen (_:xs) !n = slen xs (n+1)
len [1..10000000]
大约需要5-6秒才能完成
slen [1..10000000] 0
需要大约3-4秒才能完成。
我很好奇为什么。在我检查表演之前,我很肯定他们会表现相同,因为len
最多应该只有一个thunk来评估。出于演示目的:
len [a,b,c,d]
= 1 + len [b,c,d]
= 1 + 1 + len [c,d]
= 1 + 1 + 1 + len [d]
= 1 + 1 + 1 + 1 + len []
= 1 + 1 + 1 + 1 + 0
= 4
和
slen [a,b,c,d] 0
= slen [b,c,d] 1
= slen [c,d] 2
= slen [d] 3
= slen [] 4
= 4
是什么让slen
显着加快?
P.S。我还写了一个尾递归懒惰函数(就像slen
但是懒惰)作为尝试接近原因 - 也许是因为它是尾递归 - 但是它表现得很好和天真的定义一样。
答案 0 :(得分:9)
len
的最后一步不是O(1)。将n个数加在一起是O(n)。 len
也使用O(n)内存,而slen
使用O(1)内存。
它使用O(n)内存的原因是每个thunk都占用了一些内存。所以当你有这样的事情时:
1 + 1 + 1 + 1 + len []
有五个未评估的thunk(包括len []
)
在GHCi中,我们可以使用:sprint
命令更轻松地检查这种thunk行为。 :sprint
命令打印给定值而不强制评估任何thunk(您可以从:help
了解更多信息)。我将使用conses((:)
),因为我们可以更容易地一次评估每个thunk,但原理是相同的。
λ> let ys = map id $ 1 : 2 : 3 : [] :: [Int] -- map id prevents GHCi from being too eager here
λ> :sprint ys
ys = _
λ> take 1 ys
[1]
λ> :sprint ys
ys = 1 : _
λ> take 2 ys
[1,2]
λ> :sprint ys
ys = 1 : 2 : _
λ> take 3 ys
[1,2,3]
λ> :sprint ys
ys = 1 : 2 : 3 : _
λ> take 4 ys
[1,2,3]
λ> :sprint ys
ys = [1,2,3]
未评估的thunk由_
表示,您可以看到原始ys
中有4
个thunk堆叠在一起,每个部分对应一个列表(包括{ {1}})。
我知道在[]
中看到这个并不是一个很好的方法,因为它的评估更全或没有,但它仍然以相同的方式构建嵌套的thunk。如果您能够看到这样的话,它的评估将如下所示:
Int
答案 1 :(得分:4)
David Young的回答给出了评估顺序差异的正确解释。您应该按照他概述的方式考虑Haskell评估。
让我告诉你如何看到核心的差异。我认为通过优化实际上它更加明显,因为评估最终是一个明确的case
语句。如果您之前从未玩过Core,请参阅有关该主题的规范SO问题:Reading GHC Core。
使用ghc -O2 -ddump-simpl -dsuppress-all -ddump-to-file SO27392665.hs
生成核心输出。您会看到GHC将len
和slen
分成一个递归的"工作者"函数,$wlen
或$wslen
,以及非递归的"包装器"功能。因为绝大部分时间都花在递归的工人身上,"专注于他们:
Rec {
$wlen
$wlen =
\ @ a_arZ w_sOR ->
case w_sOR of _ {
[] -> 0;
: ds_dNU xs_as0 ->
case $wlen xs_as0 of ww_sOU { __DEFAULT -> +# 1 ww_sOU }
}
end Rec }
len
len =
\ @ a_arZ w_sOR ->
case $wlen w_sOR of ww_sOU { __DEFAULT -> I# ww_sOU }
Rec {
$wslen
$wslen =
\ @ a_arR w_sOW ww_sP0 ->
case w_sOW of _ {
[] -> ww_sP0;
: ds_dNS xs_asW -> $wslen xs_asW (+# ww_sP0 1)
}
end Rec }
slen
slen =
\ @ a_arR w_sOW w1_sOX ->
case w1_sOX of _ { I# ww1_sP0 ->
case $wslen w_sOW ww1_sP0 of ww2_sP4 { __DEFAULT -> I# ww2_sP4 }
}
您可以看到$wslen
只有一个case
,而$wlen
有两个$wlen
。如果你看看大卫的答案,你可以追踪[]
中发生的事情:它在最外面的列表构造函数(:
/ $wlen xs_as0
)上进行案例分析,然后制作递归调用len xs
(即case
),它也$wslen
,即强制累积的thunk。
另一方面,在case
中,只有一个(+# ww_sP0 1)
语句。在递归分支中,只有一个未装箱的加法-O
,它不会创建一个thunk。
(注意:此答案的先前版本已声明,$wslen
GHC可以专门使用$wlen
但不Int#
来使用未装箱的{{1}}。'}事实并非如此。)