我玩弄了一些定义以更好地理解评估模型,并为列表的长度写了两个。
天真的定义:
len :: [a] -> Int
len [] = 0
len (_:xs) = 1 + len xs
严格的(和尾递归)定义:
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
一样,但是很懒),以尝试关闭原因-也许是因为它是尾递归的-但是它的执行效果与朴素的定义相同。 最佳答案
len
的最后一步不是O(1)。将n个数字相加为O(n)。 len
也使用O(n)内存,而slen
使用O(1)内存。
它使用O(n)内存的原因是每个thunk都会占用一些内存。所以当你有这样的事情:
1 + 1 + 1 + 1 + len []
有五个未评估的重击(包括
len []
)在GHCi中,我们可以使用
:sprint
命令更轻松地检查这种重击行为。 :sprint
命令将打印给定值,而不会强制评估任何重击(您可以从: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,列表的每个部分(包括[]
)一个。我不知道有什么好方法可以在
Int
中看到它,因为它的评估结果是全部或全部,但它仍然以相同的方式构建了一个嵌套的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
= 1 + 1 + 1 + 1 -- Here it stops building the thunks and starts evaluating them
= 1 + 1 + 2
= 1 + 3
= 4
关于performance - 为什么严格长度函数的执行速度明显更快?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/27392665/