foldl是尾递归的,那么如何来foldr运行速度快于foldl?
-
26-09-2019 - |
题
我想测试foldl vs foldr.从我所看到的你应该使用foldl过foldr时,你永远可以由于尾reccursion优化。
这是有道理的。然而,在运行这个测试我很困惑:
foldr(需要0.057s当使用时命令):
a::a -> [a] -> [a]
a x = ([x] ++ )
main = putStrLn(show ( sum (foldr a [] [0.. 100000])))
foldl(需要0.089s当使用时命令):
b::[b] -> b -> [b]
b xs = ( ++ xs). (\y->[y])
main = putStrLn(show ( sum (foldl b [] [0.. 100000])))
很明显,这个例子是微不足道,但是我困惑为什么foldr是打foldl.不这是一个明确的情况下foldl赢了?
解决方案
欢迎到惰性计算的世界。
当你想想看,严格的评估,与foldl看起来“好”的条款和foldr相似容貌的“坏”,因为与foldl是尾递归,但foldr相似本来在堆栈中建立一个塔,因此它可以先处理的最后一个项目
然而,惰性计算转动表。举个例子来说,所述映射函数的定义:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
这不会是如果哈斯克尔使用严格的评价不太好,因为它必须先计算尾,然后前面加上项目(列表中的所有项目)。只有这样,才能做到这一点有效地将构建在反向元件,它似乎。
然而,由于Haskell的惰性计算,此映射功能实际上是有效的。在Haskell列表可以被认为是发电机,并且此映射函数通过将F到输入列表的第一项生成其第一项。当它需要第二个项目,它只是再次做同样的事情(不使用额外的空间)。
事实证明,map
可以foldr
来描述:
map f xs = foldr (\x ys -> f x : ys) [] xs
这是很难通过看它告诉,但懒惰的评价踢,因为foldr相似能给f
第一个参数的时候了:
foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
由于由f
限定的map
可以返回仅使用第一参数的结果列表的第一项,所述折叠可在懒惰地恒定空间中操作。
现在,惰性计算确实反咬。例如,尝试运行总和[1..1000000。它产生一个堆栈溢出。何必呢?它应该只是由左到右,右?
评估让我们看看哈斯克尔是如何评价它:
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
sum = foldl (+) 0
sum [1..1000000] = foldl (+) 0 [1..1000000]
= foldl (+) ((+) 0 1) [2..1000000]
= foldl (+) ((+) ((+) 0 1) 2) [3..1000000]
= foldl (+) ((+) ((+) ((+) 0 1) 2) 3) [4..1000000]
...
= (+) ((+) ((+) (...) 999999) 1000000)
Haskell是懒得,因为它去执行加法。相反,它必须被强制让一些未经评价的thunk的塔结束。该评价过程中发生堆栈溢出,因为它必须深深递归来评估所有的thunk。
幸运的是,在称为Data.List模块foldl'
一个特殊功能,严格操作。 foldl' (+) 0 [1..1000000]
不会堆栈溢出。 (注:我想在您的测试与foldl
更换foldl'
,但它实际上使它运行速度较慢。)
其他提示
编辑:在寻找这个问题再一次,我认为目前所有的解释稍有不足,所以我已经写了一个较长的解释。
所不同的是,在如何 foldl
和 foldr
运用他们的减少功能。看 foldr
种情况下,我们可以扩大它的作为
foldr (\x -> [x] ++ ) [] [0..10000]
[0] ++ foldr a [] [1..10000]
[0] ++ ([1] ++ foldr a [] [2..10000])
...
这个名单是理由 sum
, ,其消耗如下:
sum = foldl' (+) 0
foldl' (+) 0 ([0] ++ ([1] ++ ... ++ [10000]))
foldl' (+) 0 (0 : [1] ++ ... ++ [10000]) -- get head of list from '++' definition
foldl' (+) 0 ([1] ++ [2] ++ ... ++ [10000]) -- add accumulator and head of list
foldl' (+) 0 (1 : [2] ++ ... ++ [10000])
foldl' (+) 1 ([2] ++ ... ++ [10000])
...
我已经离开了详细的清单串联,但这是如何减少收益。重要的是,一切得到处理,以便尽量减少列遍历.的 foldr
只穿过一次,连接不需要连续列出遍历, sum
最后消耗中的列表一通过。重要的是,头的名单可从 foldr
立即 sum
, ,所以 sum
可以开始工作,立即和价值可以gc会因为他们产生的。与融合框架,例如 vector
, 即使中间名单可能会融合程。
对比这个的 foldl
功能:
b xs = ( ++xs) . (\y->[y])
foldl b [] [0..10000]
foldl b ( [0] ++ [] ) [1..10000]
foldl b ( [1] ++ ([0] ++ []) ) [2..10000]
foldl b ( [2] ++ ([1] ++ ([0] ++ [])) ) [3..10000]
...
请注意,现在列表的头是不是可直到 foldl
已经完成。这意味着整个清单必须构成的存储器之前 sum
可以开始工作。这是更有效的整体。运行两个版本 +RTS -s
表示悲惨的垃圾收集性能从foldl版本。
这也是一个情况下 foldl'
不会的帮助。加严格的 foldl'
不会改变的方式中列出创建。头部的名单仍然无法使用,直到foldl'已经完成,因此结果将仍然是比较慢 foldr
.
我用下列规则来确定最好的选择 fold
- 为褶皱的是一个 减少, 使用
foldl'
(例如这将是唯/最终遍历) - 否则使用
foldr
. - 不要用
foldl
.
在大多数情况下 foldr
是最好的折功能,因为穿越方向是最佳的懒惰的评价的清单。它也是唯一一个有能力处理无限的清单。额外的严格的 foldl'
可以使它更快地在一些情况,但这取决于你怎么会使用结构和多懒惰。
我不认为任何人居然说在这一个真正的答案,除非我失去了一些东西(这很可能是真实的,并与downvotes欢迎)。
我认为在这种情况下,最大的不同之处在于foldr
建立这样的列表:
[0] ++([1] ++([2] ++(++ ... [百万])))
尽管foldl
构建列表是这样的:
((([0] ++ [1])++ [2])++ ...)++ [999888])++ [999999])++ [百万]
在细微的差别,但请注意,在foldr
版本++
始终只有一个列表元素作为其左边的参数。与foldl
版本,有高达++
的左参数999999个元件(平均约500000),但是只有一个元素在右边的参数。
然而,++
需要时间正比于左参数的大小,因为它看起来虽然整个左参数列表的末尾,然后将该最后一个元素重新指向到右边的参数的第一个元素(在最好的,也许它实际上需要做的复印件)。正确的参数列表是不变的,所以它不会不管它是多么大了。
这就是为什么foldl
版本要慢得多。它什么都没有做懒惰在我看来。
问题是,尾递归化是一个存优化,不是一个执行时优化!
尾递归化可避免的需要记住值为每递归的呼吁。
因此,foldl是在事实上"好"的和foldr是"坏"。
例如,考虑到定义的foldr和foldl:
foldl f z [] = z
foldl f z (x:xs) = foldl f (z `f` x) xs
foldr f z [] = z
foldr f z (x:xs) = x `f` (foldr f z xs)
这是怎样表达"foldl(+)0[1、2、3的]"是评估:
foldl (+) 0 [1, 2, 3]
foldl (+) (0+1) [2, 3]
foldl (+) ((0+1)+2) [3]
foldl (+) (((0+1)+2)+3) [ ]
(((0+1)+2)+3)
((1+2)+3)
(3+3)
6
注意foldl不记得值0,1,2...,但是通过个整体的表达(((0+1)+2)+3) 作为论点,懒洋洋地以及不会评估它直到最后评价foldl,它达到基本情况和返回的价值,通过了作为第二个参数(z)至极不是进行评估。
另一方面,这就是如何foldr工作:
foldr (+) 0 [1, 2, 3]
1 + (foldr (+) 0 [2, 3])
1 + (2 + (foldr (+) 0 [3]))
1 + (2 + (3 + (foldr (+) 0 [])))
1 + (2 + (3 + 0)))
1 + (2 + 3)
1 + 5
6
重要的差别这里是哪里foldl评估整体的表达的最后一个电话,避免需要回到达记住值,foldr没有。foldr记住一整数的每个呼吁并执行一个外在每个呼吁。
重要的是要铭记,foldr和foldl并不总是等同物。例如,尝试计算这表现在拥抱:
foldr (&&) True (False:(repeat True))
foldl (&&) True (False:(repeat True))
foldr和foldl是相当只在某些条件的描述 在这里,
(对不起我英语不好)
有关一个,所述列表[0.. 100000]
需要扩大马上使得foldr相似可以与最后一个元素开始。然后,当它折叠东西放在一起,中间结果
[100000]
[99999, 100000]
[99998, 99999, 100000]
...
[0.. 100000] -- i.e., the original list
由于没有人被允许改变该列表的值(Haskell是纯功能性的语言),编译器可以自由地再次使用该值。中间值,象[99999, 100000]
甚至可以简单指针到膨胀[0.. 100000]
列表,而不是单独的列表。
有关B,看中间值:
[0]
[0, 1]
[0, 1, 2]
...
[0, 1, ..., 99999]
[0.. 100000]
每个那些中间的列表中不能再用,因为如果你改变了列表的末尾,那么你已经改变了任何其他值指向它。所以,你要创建一个一堆需要时间来建立在内存中额外的名单。因此,在这种情况下,你花更多的时间分配和在这些名单是中间值填充。
既然你只是把名单副本,一个运行速度更快,因为它开始通过扩展的完整列表,然后只是不断从列表中的回前方移动的指针。
无论foldl
也不foldr
是尾优化。它只是foldl'
。
但在使用++
与foldl'
你的情况是不是好主意,因为++
的连续评估将引起一次又一次穿越增长累加器。
好了,让我重写的方式你的函数的区别应该是显而易见的 -
a :: a -> [a] -> [a]
a = (:)
b :: [b] -> b -> [b]
b = flip (:)
您看到,b比更复杂。如果你想成为精确a
需要针对要计算的值减少一个步骤,但b
需要两个。这使得要测量的时间差,在第二个例子中的两倍减少必须执行的。
//编辑:但是时间复杂度是一样的,所以我不会理会太多