【问题标题】:Why are Clojure's core.reducers faster than lazy collection functions为什么 Clojure 的 core.reducers 比惰性收集函数快
【发布时间】:2016-06-30 22:02:55
【问题描述】:

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

避免的额外开销是什么? IIUC 甚至懒惰的收集函数最终都会遍历原始序列一次。中间结果计算的细节有区别吗?

指向 Clojure 实现中相关位置的指针有助于理解差异也将非常有帮助

【问题讨论】:

    标签: clojure functional-programming reducers


    【解决方案1】:

    我认为original blog post 的以下段落中有一个关键见解:

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

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

    惰性序列的实现带有(线性)分配成本:每次实现惰性序列中的另一个元素时,序列的 rest 都会存储在一个新的 thunk 中,并且这种“thunk”的表示是a new clojure.lang.LazySeq object

    我相信那些LazySeq 对象是引用中提到的分配开销。使用 reducer 不会逐渐实现惰性 seq 元素,因此根本不会实例化 LazySeq thunk。

    【讨论】:

    • 谢谢 - 这听起来比我的其他答案更正确。另一个答案意味着整个序列是在每一步之间实现的,这不可能是真的。我理解这个分配成本更有意义,这是我们不想支付的成本,而减速器更快。
    • 我想你可以这样阅读第一个答案......我认为这可能是一个无意的阅读,但如果它强烈地向你暗示,那么当然,那是一种误导。延迟序列处理一次发生一个元素或一个块(= 最多 32 个元素的块),并在需要时实现新元素或块。我认为原始答案试图做出的正确观点是,当您堆叠惰性 seq 转换时,每一层都需要分配自己的惰性 seq 对象,每个元素或块有一个“单元”(当然它们会被延迟实现)。
    • @MichałMarczyk:第一个答案是“当你组合像 map、filter 这样的操作时,这些函数会遍历一个集合并返回一个新集合,然后将其传递给下一个函数”这听起来显然是错误的。 nha 在 cmets 中也一直在捍卫这一点,所以我放弃了争论。后来似乎他/她将答案修补得更笼统。
    • 它们实际上确实每个都返回一个新集合,只是这些集合是惰性 seq 对象,因此它们以未实现的形式出现,并且只有在从中提取元素时“从外向内”实现“最外层”的惰性序列对象。
    • 诚然,您引用的句子似乎暗示 map & Co. 迭代他们的输入并 然后 返回新的集合,这是不正确的 - 他们立即返回新的集合,在其中嵌入迭代逻辑(在请求实际元素时调用)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-24
    • 1970-01-01
    相关资源
    最近更新 更多