【问题标题】:Performance gain implementing concatMap with foldl' for finite list?使用 foldl' 为有限列表实现 concatMap 的性能增益?
【发布时间】:2017-03-26 18:50:49
【问题描述】:

我从Foldr Foldl Foldl' 了解到,foldl' 对于长有限列表更有效,因为它具有严格性属性。我知道它不适合无限列表。

因此,我只限制长有限列表的比较。


concatMap

concatMap 是使用foldr 实现的,这使它变得懒惰。但是,根据文章,将它与长的有限列表一起使用会建立一个长的未归约链。

concatMap :: Foldable t => (a -> [b]) -> t a -> [b]
concatMap f xs = build (\c n -> foldr (\x b -> foldr c b (f x)) n xs)

因此,我使用foldl' 提出了以下实现。

concatMap' :: Foldable t => (a -> [b]) -> t a -> [b]
concatMap' f = reverse . foldl' (\acc x -> f x ++ acc) []

测试一下

我已经构建了以下两个函数来测试性能。

lastA = last . concatMap (: []) $ [1..10000]
lastB = last . concatMap' (: []) $ [1..10000]

但是,我对结果感到震惊。

lastA:
(0.23 secs, 184,071,944 bytes)
(0.24 secs, 184,074,376 bytes)
(0.24 secs, 184,071,048 bytes)
(0.24 secs, 184,074,376 bytes)
(0.25 secs, 184,075,216 bytes)

lastB:   
(0.81 secs, 224,075,080 bytes)
(0.76 secs, 224,074,504 bytes)
(0.78 secs, 224,072,888 bytes)
(0.84 secs, 224,073,736 bytes)
(0.79 secs, 224,074,064 bytes)

后续问题

concatMap 在时间和记忆力上都胜过我的concatMap'。我想知道我在实现 concatMap' 时犯了哪些错误。

因此,我怀疑那些陈述foldl'的好文章。

  1. concatMap 中是否有什么黑魔法让它如此高效?

  2. foldl' 对于长有限列表是否更有效?

  3. 是否将foldr 与长有限列表一起使用会建立一个长的未归约链并影响性能?

【问题讨论】:

  • 好吧,你使用++,它在 O(n) 中运行,其中 n 是左列表的大小......所以你通常想要避免这种情况(因为你要在一个 concat 上定义一个 concat ......)
  • @Willem 感谢您的及时回复。对于concatMap,我怎样才能避免++?此外,f x 对于测试用例的大小为 1。
  • foldl' 很好,如果您要使用严格的函数从种子构建一个很好的严格数据类型的东西来更新 - 比如以 Int 种子开始使用 (+)Int举个最简单的例子。如果你是从列表中构造列表或其他惰性结构,foldr 会更自然。
  • 注意你的函数也不正确——你应该反转f x(试试concatMap (\x -> [-x,x]) [1,2,3]concatMap' (\x -> [-x,x]) [1,2,3]

标签: performance haskell functional-programming fold


【解决方案1】:

concatMap 中是否有什么黑魔法让它如此高效?

不,不是。

foldl' 对于长有限列表是否更有效?

并非总是如此。这取决于折叠功能。

重点是,foldlfoldl' 在生成输出之前总是必须扫描整个输入列表。相反,foldr 并不总是必须这样做。

作为一个极端情况,考虑

foldr (\x xs -> x) 0 [10..10000000]

立即计算为10 - 仅计算列表的第一个元素。减少类似于

foldr (\x xs -> x) 0 [10..10000000]
= foldr (\x xs -> x) 0 (10 : [11..10000000])
= (\x xs -> x) 10 (foldr (\x xs -> x) 0 [11..10000000])
= (\xs -> 10) (foldr (\x xs -> x) 0 [11..10000000])
= 10

由于惰性,递归调用没有被评估。

一般来说,在计算foldr f a xs 时,检查f y ys 是否能够在评估ys 之前构造部分输出很重要。比如

foldr f [] xs
where f y ys = (2*y) : ys

在评估2*yys 之前生成一个列表单元_ : _。这使它成为foldr 的绝佳候选者。

再次,我们可以定义

map f xs = foldr (\y ys -> f y : ys) [] xs

它运行得很好。它消耗来自xs 的一个元素并输出第一个输出单元。然后它消耗下一个元素,输出下一个元素,依此类推。使用foldl' 在处理整个列表之前不会输出任何内容,从而使代码效率非常低。

相反,如果我们写了

sum xs = foldr (\y ys -> y+ys) 0 xs

那么在xs 的第一个元素被消耗后我们不会输出任何东西。 我们构建了一长串 thunk,浪费了大量内存。 在这里,foldl' 将改为在恒定空间中工作。

foldr 与长的有限列表一起使用是否会建立一个长的未归约链并影响性能?

并非总是如此。这在很大程度上取决于调用者如何使用输出。

作为一个经验法则,如果输出是“原子的”,这意味着输出使用者不能只观察到它的一部分(例如Bool, Int, ...),那么最好使用foldl'。如果输出由许多独立值(列表、树等)“组成”,foldr 可能是更好的选择,如果f 可以以“流”方式逐步生成其输出。

【讨论】:

  • 感谢您的解释。我还有一个后续问题。 foldr惰性结构时如何避免堆栈溢出?
  • @Gavin foldr 在函数不严格的情况下效果很好,否则会导致创建大量 thunk。因此, foldr (\x y -> K e1 e2 .. en)` 是可以的,其中 K 是一个构造函数。在上面的map 示例中,K 是列表构造函数(:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-02-25
  • 2015-03-07
  • 2023-04-09
  • 1970-01-01
  • 2011-11-09
  • 2016-09-04
  • 1970-01-01
相关资源
最近更新 更多