为什么Clojure在clojure.lang.Iterate.first上花了这么多时间?

我很喜欢关于弗兰克·纳尔逊·科尔 ( Frank Nelson Cole)的故事,他在1903年在一个着名的“没有文字的讲座”中展示了2 ^ 67 – 1的主因式分解。 这些天使用下面的朴素算法可以很容易地找到分解:

(def mersenne67 (dec (expt 2 67))) (->> (iterate inc 2) (filter #(zero? (rem mersenne67 %))) (first)) 

但是我注意到这个Clojure代码大约需要等效的Java或Kotlin代码的两倍。 (在我的机器上〜40 vs〜20秒)
我把这个Java比较为:

  public static BigInteger mersenne() { BigInteger mersenne67 = BigInteger.valueOf(2).pow(67).subtract(BigInteger.ONE); return Stream.iterate(BigInteger.valueOf(2), (x -> x.add(BigInteger.ONE))) .filter(x -> mersenne67.remainder(x).equals(BigInteger.ZERO)) .findFirst() .get(); } 

重写低级Clojure代码没有任何区别:

 (def mersenne67 (-> (BigInteger/valueOf 2) (.pow (BigInteger/valueOf 67)) (.subtract BigInteger/ONE))) (->> (iterate #(.add ^BigInteger % BigInteger/ONE) (BigInteger/valueOf 2)) (filter #(= BigInteger/ZERO (.remainder ^BigInteger mersenne67 %))) (first)) 

在用VisualVM分析代码之后,主要的嫌疑人似乎是clojure.lang.Iterate.first() ,这几乎正好说明了这些函数运行多长时间的差异。 Java的等效java.util.stream.ReferencePipeline.findFirst()只运行一小部分(〜22 vs〜2秒)。 这导致了我的问题:Java(和Kotlin)如何在这个任务上花费这么少的时间来逃避?

我提前道歉挖掘,但我有点关心ClojureMostly的答案。 它确实及时地解决了这个问题,但是对我来说这看起来像一个肮脏的黑客攻击:通过匿名还原功能,忽略当前结果(_)并在找到(减少)第一个因子后立即结束。

如何使用传感器和传感功能:

 (defn first-factor-tr [^BigInteger n] (transduce (comp (filter #(identical? BigInteger/ZERO (.remainder ^BigInteger n %))) (take 1)) conj nil (iterate #(.add ^BigInteger % BigInteger/ONE) (BigInteger/valueOf 2)))) 

用剩下的零(filter …)过滤掉集合中的所有值,并取第一个(取…)。

该解决方案的执行时间与减少的执行时间相当:

 (defn first-factor-reduce [^BigInteger n] (reduce (fn [_ x] (when (identical? BigInteger/ZERO (.remainder ^BigInteger nx)) (reduced x))) nil (iterate #(.add ^BigInteger % BigInteger/ONE) (BigInteger/valueOf 2)))) => (time (first-factor-tr mersenne67)) "Elapsed time: 20896.594178 msecs" (193707721) => (time (first-factor-reduce mersenne67)) "Elapsed time: 20424.323894 msecs" 193707721 

你的问题是你迭代迭代效率低下。 这就是为什么你在分析中first看到的。 这当然是clojure的所有核心功能都有很多不同的数据结构的结果。

避免这种情况的最好方法是使用reduce ,这使对象本身可以在循环中调用函数。

所以这个速度大概是2倍:

 (reduce (fn [_ x] (when (identical? BigInteger/ZERO (.remainder ^BigInteger mersenne67 x)) (reduced x))) nil (iterate #(.add ^BigInteger % BigInteger/ONE) (BigInteger/valueOf 2)))