在Reducers的许多资源(如the canonical blog post by Rich Hickey)中,声称reducer比常规的收集函数((map ... (filter ...))
等)要快,因为开销较少。为什么Clojure的core.reducers比懒惰的收集函数更快
什么是避免的额外开销? IIUC甚至懒惰的收集功能最终只走一次原始序列。计算中间结果的细节有何不同?
指针到Clojure中的实现,有助于理解上的差异将是最有帮助的太
在Reducers的许多资源(如the canonical blog post by Rich Hickey)中,声称reducer比常规的收集函数((map ... (filter ...))
等)要快,因为开销较少。为什么Clojure的core.reducers比懒惰的收集函数更快
什么是避免的额外开销? IIUC甚至懒惰的收集功能最终只走一次原始序列。计算中间结果的细节有何不同?
指针到Clojure中的实现,有助于理解上的差异将是最有帮助的太
我认为一个重要观点是,从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游戏中,没有每步分配开销,因此速度更快。当你需要时,懒惰是伟大的,但是当你不需要时,你不应该为此付出代价。
实现懒惰序列的带有一个(线性)分配成本:每从懒惰SEQ另一元件被实现,带seq的其余被存储在新的thunk时间,以及表示这样的'thunk'是a new clojure.lang.LazySeq
object。
我相信这些LazySeq
对象是引用中引用的分配开销。对于减速器,没有逐步实现惰性seq元素,因此根本没有实例化LazySeq
thunk。
相关的地方“注意,有没有产生中间集合。”
这是改进,因为垃圾收集器的压力比较小。
当你结合像map
,filter
这样的操作时,那些函数迭代一个集合并返回一个新的集合,然后传递给下一个函数。减速器情况并非如此。
因此,它们可以用作那些功能(也就是说,它们以相同的方式组成)并且可以应用于相同的集合。但是增加了性能。
具有和不减速的地图/地图/过滤操作的粗糙和完全不科学草图如下:
没有减速器
初始集合=>地图=>中介集合=>地图=> intermediary collection => filter => final collection
没有reducer(即clojure.core map/filter函数)int中间集合是懒惰生产的。 也就是说,只有在下一个处理步骤需要时才产生新的元素,一次一个元素或一个块。
注意:块是由32个元素组成的块(尽管这应该被视为实现细节)。这是出于效率的原因,以避免从一次计算跳到另一次计算。
查看map
函数return value例如:它返回lazy-seq
(transducers的情况除外)。
随着减速
初步收集=>地图/地图/过滤器减速=>最终收集
如果您需要处理整个集合快,那么懒惰也是在越来越表现方式。删除懒惰seqs的中间层提供了性能增益。 Reducers可以做到这一点并且可以快速处理收集,因此速度更快。您还可以保持相同的功能来缓解切换。
请仔细阅读下面的意见,尤其是来自@米哈尔Marczyk
我不相信这是真的。 Clojure中的集合是懒惰的,'map' et.al坚持懒惰 - 当叠加'map','reduce','filter'等时,只有一次通过输入集合,而不是多次。参见http:///clojure.org/reference/sequences – zlatanski
在最初的收集是。但是,他们创建了另一个集合,迭代(仅一次),然后传递给下一个函数,该函数遍历新集合(仅再次)...等等。换能器没有迭代的中间集合。我编辑了我的答案,希望它有帮助。 – nha
如果你说的是真的,那么'(以3(地图增量(甚至过滤?(范围999999999))))'将花费很长时间并使用大量内存。但它需要30个用户,并且几乎不消耗内存 – zlatanski
谢谢 - 这听起来比我的其他答案更正确。另一个答案意味着整个seq是在每一步之间实现的,这是不对的。这种分配成本是我理解的东西更有意义,而这是我们不想支付的成本,这是减速器更快的成本。 – zlatanski
我想你可以通过这种方式阅读第一个答案......我认为这可能是一个意外的阅读,但如果它强烈地向你表明,那么肯定的是,这是误导。惰性seq处理一次发生一个元素或一个块(最多32个元素的块),并在需要时实现新元素或块。我认为原始答案试图做出的正确观点是,当你将懒惰的seq转换进行堆栈时,每一层都需要分配它自己的懒惰seq对象,每个元素或块有一个“单元”(尽管它们当然会被懒惰地实现)。 –
@MichałMarczyk:第一个答案说:“当你结合像map这样的操作,过滤那些函数遍历一个集合,并返回一个新的集合,然后传递给下一个函数**”,这听起来毫不含糊。 nha还一直在评论中捍卫这一点,所以我放弃了争论。后来看来,他/她修补了更广泛的答案。 – zlatanski