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

时间:2017-06-13 20:05:58

标签: java performance clojure

我非常喜欢关于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秒~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)如何在这项任务上花费更少的时间?

2 个答案:

答案 0 :(得分:2)

您的问题是您无法有效地迭代iterate。这就是您在分析时看到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)))

答案 1 :(得分:1)

我提前为严肃挖掘道歉,但我有点担心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))))

过滤掉集合中的所有值,余数为零(过滤器...)并取第一个(取......)。

此解决方案的执行时间与reduce:

的执行时间相同
(defn first-factor-reduce 
  [^BigInteger n] 
  (reduce
    (fn [_ x]
      (when (identical? BigInteger/ZERO (.remainder ^BigInteger n x))
            (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