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

Why are Clojure's core.reducers faster than lazy collection functions

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

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

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

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

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

That should look familiar - it's the same named functions, applied in the same order, with the same arguments, producing the same result as the Clojure's seq-based fns. The difference is that, reduce being eager, and these reducers fns being out of the seq game, there's no per-step allocation overhead, so it's faster. Laziness is great when you need it, but when you don't you shouldn't have to pay for it.

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

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