具有惰性评估的差异列表的好处

时间:2014-02-01 23:21:42

标签: performance haskell

我很难强调为什么++被认为是O(n),而differential lists被认为是“O(1)”。

如果是++,我们假设它被定义为:

(++) :: [a] -> [a] -> [a]
(a:as) ++ b = a:(as ++ b)
[]     ++ b = b

现在,如果我们需要在a ++ b中获取访问第一个元素,我们可以在O(1)中执行它(假设{1}}可以在1步中成为HNF),类似于第二个等。它会随着将多个列表设置为Ω(1)/ O(m)而更改,其中m是未评估的附加数。访问最后一个元素可以用Θ(n + m)来完成,其中n是列表的长度,除非我遗漏了一些东西。如果我们有差分列表,我们也可以访问Θ(m)中的第一个元素,而最后一个元素是Θ(n + m)。

我想念什么?

3 个答案:

答案 0 :(得分:7)

理论上的表现

O(1)指的是DLists的附加只是(.),其中一个减少,而(++)是O(n)。

最坏情况

++具有二次性能,当您使用它重复添加到现有字符串的末尾时,因为每次添加另一个列表时,您都会遍历现有列表,所以

"Existing long ...... answer" ++ "newbit"
每次添加新位时,

遍历"Existing long ....... answer"

另一方面,

("Existing long ..... answer" ++ ) . ("newbit"++)
当函数链应用于"Existing long ...... answer"以转换为列表时,

只会实际遍历[] 一次

经验说

多年前,当我还是一个年轻的Haskeller时,我编写了一个程序,正在寻找一个猜想的反例,所以一直在向磁盘输出数据,直到我停止它,除了一旦我取下测试制动器,它输出没有什么因为我的左关联尾部递归构建一个字符串,我意识到我的程序不够懒惰 - 它不能输出任何东西,直到它附加了最后的字符串,但没有最后的字符串!我推出了自己的DList(这是在编写DList库的那个之前的千禧年),并且我的程序运行得非常漂亮,愉快地在服务器上制作了大量的非反例,直到我们放弃了该项目。

如果你搞得足够大的例子,你可以看到性能差异,但对于小的有限输出并不重要。它当然教会了我懒惰的好处。

玩具示例

愚蠢的例子来证明我的观点:

plenty f = f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f
alot f = plenty f.plenty f.plenty f

让我们做两种追加,首先是DList方式

compose f = f . ("..and some more.."++)
append xs = xs ++ "..and some more.."

insufficiently_lazy = alot append []
sufficiently_lazy = alot compose id []

给出:

ghci> head $ sufficiently_lazy
'.'
(0.02 secs, 0 bytes)
ghci> head $ insufficiently_lazy
'.'
(0.02 secs, 518652 bytes)

ghci> insufficiently_lazy
    -- (much output skipped)
..and some more....and some more....and some more.."
(0.73 secs, 61171508 bytes)

ghci> sufficiently_lazy
    -- (much output skipped) 
..and some more....and some more....and some more.."
(0.31 secs, 4673640 bytes).    
    -- less than a tenth the space and half the time

所以它在实践和理论上都更快。

答案 1 :(得分:2)

如果您反复追加列表片段,DL主题通常最有用。好的,

foldl1 (++) [a,b,c,d,e] == (((a ++ b) ++ c) ++ d) ++ e

非常糟糕

foldr1 (++) [a,b,c,d,e] == a ++ (b ++ (c ++ (d ++ e)))

距离n位置仍然n步。不幸的是,您经常通过遍历结构并附加到累积字符串的末尾来构建字符串,因此左折叠方案并不罕见。因此,DList在您重复构建字符串(如Blaze / ByteString Builder库)的情况下非常有用。

答案 2 :(得分:2)

[经过进一步的思考和阅读其他答案后,我相信我知道出了什么问题 - 但我认为没有完全解释,所以我加入了自己的答案。]

假设您有列表a1:a2:[] b1:b2:[]c1:c2:[]。现在你追加他们(a ++ b) ++ c。这给了:

(a1:a2:[] ++ b1:b2:[]) ++ c1:c2:[]

现在要抬头,你需要采取O(m)步,其中m是附加数。这给了thunk如下:

 a1:((a2:[] ++ b1:b2:[]) ++ c1:c2:[])

要给出下一个元素,你需要执行m或m-1步骤(我认为它在我的推理中是免费的)。因此,在2米或2米-1步之后,视图如下:

 a1:a2(([] ++ b1:b2:[]) ++ c1:c2:[])

等等。在最坏的情况下,当遍历thunks 每个时间时,它会给m * n时间遍历列表。

编辑 - 看起来the answer to duplicate的照片效果会更好。