Clojure地图功能的高效副作用模拟

Mar*_*ars 3 clojure

如果mapdoseq生了一个孩子?我正在尝试编写像Common Lisp这样的函数或宏mapc,但是在Clojure中.这基本上做了什么map,但用于副作用,因此它不需要生成一系列结果,也不会是懒惰的.我知道可以使用迭代迭代单个序列doseq,但map可以迭代多个序列,将函数应用于所有序列的每个元素.我也知道,一个可以包装mapdorun.(注意:经过许多评论和非常彻底的回答后,这个问题已被广泛编辑.原始问题集中在宏,但那些宏观问题被证明是外围的.)

这很快(根据标准):

(defn domap2
  [f coll]
  (dotimes [i (count coll)]
    (f (nth coll i))))
Run Code Online (Sandbox Code Playgroud)

但它只接受一个集合.这接受任意集合:

(defn domap3
  [f & colls]
  (dotimes [i (apply min (map count colls))]
    (apply f (map #(nth % i) colls))))
Run Code Online (Sandbox Code Playgroud)

但相比之下,这是非常缓慢的.我也可以写这样的一个第一版本,但使用不同的参数情况[f c1 c2],[f c1 c2 c3]等等,但最终,我需要一个处理藏品的任意数字,像最后一个例子,这是无论如何简单的情况下.我也尝试了很多其他解决方案.

由于第二个例子非常像第一个例子,除了使用applymap循环内部之外,我怀疑摆脱它们会加速很多事情.我试图通过将domap2编写为宏来实现这一点,但是&处理后的catch-all变量的方式让我一直绊倒,如上所示.

其他的例子(满分为15个或20个不同的版本),基准代码和时间在MacBook Pro上,这是一个几十岁(完整源在这里):

(defn domap1
  [f coll]
  (doseq [e coll] 
    (f e)))

(defn domap7
  [f coll]
  (dorun (map f coll)))

(defn domap18
  [f & colls]
  (dorun (apply map f colls)))

(defn domap15
  [f coll] 
  (when (seq coll)
    (f (first coll))
    (recur f (rest coll))))

(defn domap17
  [f & colls]
  (let [argvecs (apply (partial map vector) colls)] ; seq of ntuples of interleaved vals
    (doseq [args argvecs]
      (apply f args))))
Run Code Online (Sandbox Code Playgroud)

我正在开发一个使用core.matrix矩阵和向量的应用程序,但可以随意替换下面的副作用函数.

(ns tst
  (:use criterium.core
        [clojure.core.matrix :as mx]))

(def howmany 1000)
(def a-coll (vec (range howmany)))
(def maskvec (zero-vector :vectorz howmany))

(defn unmaskit!
  [idx]
  (mx/mset! maskvec idx 1.0)) ; sets element idx of maskvec to 1.0

(defn runbench
  [domapfn label]
  (print (str "\n" label ":\n"))
  (bench (def _ (domapfn unmaskit! a-coll))))
Run Code Online (Sandbox Code Playgroud)

根据Criterium的平均执行时间,以微秒为单位:

domap1:12.317551 [doseq]
domap2:19.065317 [dotimes]
domap3:265.983779 [dotimes with apply,map]
domap7:53.263230 [map with dorun]
domap18:54.456801 [map with dorun,multiple collections]
domap15:32.034993 [recur]
domap17:95.259984 [doseq,使用地图交错的多个集合]

编辑:可能是dorun+ map是实现domap多个大型惰性序列参数的最佳方式,但doseq在单个懒惰序列方面仍然是王道.执行与unmask!上面相同的操作,但运行索引(mod idx 1000)并迭代(range 100000000),doseq大约是我的测试中的dorun+的两倍map(即(def domap25 (comp dorun map))).

noi*_*ith 5

你不需要一个宏,我不明白为什么宏在这里会有所帮助.

user> (defn do-map [f & lists] (apply mapv f lists) nil)
#'user/do-map
user> (do-map (comp println +) (range 2 6) (range 8 11) (range 22 40))
32
35
38
nil
Run Code Online (Sandbox Code Playgroud)

注意do-map这里很渴望(感谢mapv)并且只对副作用执行

宏可以使用varargs列表,因为do-map的(无用的!)宏版本演示了:

user> (defmacro do-map-macro [f & lists] `(do (mapv ~f ~@lists) nil))
#'user/do-map-macro
user> (do-map-macro (comp println +) (range 2 6) (range 8 11) (range 22 40))
32
35
38
nil
user> (macroexpand-1 '(do-map-macro (comp println +) (range 2 6) (range 8 11) (range 22 40)))
(do (clojure.core/mapv (comp println +) (range 2 6) (range 8 11) (range 22 40)) nil)
Run Code Online (Sandbox Code Playgroud)

附录: 解决效率/垃圾创建方面的问题:
请注意,由于简洁原因,我在下面截断了标准工作台功能的输出:

(defn do-map-loop
  [f & lists]
  (loop [heads lists]
    (when (every? seq heads)
      (apply f (map first heads))
      (recur (map rest heads)))))


user> (crit/bench (with-out-str (do-map-loop (comp println +) (range 2 6) (range 8 11) (range 22 40))))
...
            Execution time mean : 11.367804 µs
...
Run Code Online (Sandbox Code Playgroud)

这看起来很有希望,因为它不会创建我们不使用的数据结构(与上面的mapv不同).但事实证明它比前一个慢(可能是因为两个地图调用?).

user> (crit/bench (with-out-str (do-map-macro (comp println +) (range 2 6) (range 8 11) (range 22 40))))
...
             Execution time mean : 7.427182 µs
...
user> (crit/bench (with-out-str (do-map (comp println +) (range 2 6) (range 8 11) (range 22 40))))
...
             Execution time mean : 8.355587 µs
...
Run Code Online (Sandbox Code Playgroud)

由于循环仍然没有更快,让我们尝试一个专门研究arity的版本,这样我们就不需要在每次迭代时调用map两次:

(defn do-map-loop-3
  [f a b c]
  (loop [[a & as] a
         [b & bs] b
         [c & cs] c]
    (when (and a b c)
      (f a b c)
      (recur as bs cs))))
Run Code Online (Sandbox Code Playgroud)

值得注意的是,尽管速度更快,但它仍然比使用mapv的版本慢:

user> (crit/bench (with-out-str (do-map-loop-3 (comp println +) (range 2 6) (range 8 11) (range 22 40))))
...
             Execution time mean : 9.450108 µs
...
Run Code Online (Sandbox Code Playgroud)

接下来我想知道输入的大小是否是一个因素.输入量更大......

user> (def test-input (repeatedly 3 #(range (rand-int 100) (rand-int 1000))))
#'user/test-input
user> (map count test-input)
(475 531 511)
user> (crit/bench (with-out-str (apply do-map-loop-3 (comp println +) test-input)))
...
            Execution time mean : 1.005073 ms
...
user> (crit/bench (with-out-str (apply do-map (comp println +) test-input)))
...
             Execution time mean : 756.955238 µs
...
Run Code Online (Sandbox Code Playgroud)

最后,为了完整性,do-map-loop的时间(正如预期的那样稍微慢于do-map-loop-3)

user> (crit/bench (with-out-str (apply do-map-loop (comp println +) test-input)))
...
             Execution time mean : 1.553932 ms
Run Code Online (Sandbox Code Playgroud)

如我们所见,即使输入尺寸较大,mapv也会更快.

(我应该注意这里的完整性,map比mapv略快,但不是很大).