【问题标题】:Why does Clojure spend so much time on clojure.lang.Iterate.first?为什么 Clojure 在 clojure.lang.Iterate.first 上花费这么多时间?
【发布时间】:2017-11-15 18:25:05
【问题描述】:

我很喜欢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 对~2 秒)。 这引出了我的问题:Java(和 Kotlin)如何在这项任务上花费如此少的时间?

【问题讨论】:

  • 什么是expt
  • Clojure 的 numeric tower 中的求幂函数。

标签: java performance clojure


【解决方案1】:

您的问题是您对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)))

【讨论】:

    【解决方案2】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-11-12
      • 2016-08-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-06-01
      相关资源
      最近更新 更多