为什么Clojure的core.reducers比懒惰的集合函数更快

时间:2016-03-15 12:44:30

标签: clojure functional-programming reducers

在Reducers的许多资源中(如the canonical blog post by Rich Hickey),它声称reducers比常规集合函数((map ... (filter ...))等)更快,因为开销更少。

避免了多少额外开销? IIUC即使是懒惰的集合函数最终只能执行原始序列 。计算中间结果的细节有何不同?

指示Clojure实施中有助于理解差异的相关位置也是最有帮助的

2 个答案:

答案 0 :(得分:5)

  

“请注意,没有生成中间集合。”

这是改进,因为例如对垃圾收集器的压力较小。

当您组合mapfilter等操作时,这些函数会迭代一个集合并返回一个新集合,然后将其传递给下一个函数。减速器不是这种情况。

因此它们可以用作那些函数(也就是说,它们以相同的方式组成)并且可以应用于相同的集合。但随着性能的提升。

有或没有减速器的地图/地图/过滤器操作的粗略且完全不科学的草图如下:

没有减速机

  

初始收集=> map =>中介收集=> map =>   中介收集=> filter =>最终收藏

没有reducer(即clojure.core map / filter函数),懒惰地生成中间集合。 也就是说,新元素仅在下一个处理步骤需要时生成,一次一个元素或一个块。

注意:块是一个包含32个元素的块(尽管这应该被视为实现细节)。这是出于效率的原因,以避免从一个计算“跳跃”到另一个计算。

例如,请参阅map函数return value:它返回lazy-seqtransducers除外)。

使用缩减器

  

初始收集=> map / map / filter reducer =>最终收藏

如果你需要快速处理整个系列,那么懒惰就会影响性能。删除惰性seq的中间层可提供性能提升。 Reducers执行此操作并热切地处理集合,因此更快。您还可以使用相同的功能来简化切换。

请同时阅读以下评论,特别是来自@MichałMarczyk的评论

答案 1 :(得分:3)

我认为一个关键的见解来自original blog post

的以下段落
(require '[clojure.core.reducers :as r])
(reduce + (r/filter even? (r/map inc [1 1 1 2])))
;=> 6
     

这应该看起来很熟悉 - 它是相同的命名函数,以相同的顺序应用,具有相同的参数,产生与Clojure的基于seq的fns相同的结果。不同之处在于,减少急切,并且这些减速器fns不在seq游戏中,没有每步分配开销,因此速度更快。懒惰在你需要的时候很棒,但是当你没有时,你不应该为此付出代价。

延迟序列的实现伴随着(线性)分配成本:每次实现lazy seq中的另一个元素时,seq的 rest 存储在一个新的thunk中,并且这种'thunk'的表示是a new clojure.lang.LazySeq object

我相信那些LazySeq个对象是引用中引用的分配开销。对于reducers,没有逐渐实现lazy seq元素,因此根本没有LazySeq thunk的实例化。