简体   繁体   English

对于简单的循环而言,Clojure的性能与Java相比非常糟糕

[英]Clojure performance really bad on simple loop versus Java

Spoiler alert, this is problem 5 of Project Euler. Spoiler警报,这是Project Euler的第5个问题。

I am attempting to learn Clojure and solved problem 5, but it is a couple orders of magnitude slower (1515 ms in Java versus 169932 ms in Clojure). 我正在尝试学习Clojure并解决了问题5,但它慢了几个数量级(Java中为1515 ms,而Clojure中为169932 ms)。 I even tried using type hinting, unchecked math operations, and inlining functions all for naught. 我甚至尝试使用类型提示,未经检查的数学运算和内联函数都是徒劳的。

Why is my Clojure code so much slower? 为什么我的Clojure代码这么慢?

Clojure code: Clojure代码:

(set! *unchecked-math* true)
(defn divides? [^long number ^long divisor] (zero? (mod number divisor)))

(defn has-all-divisors [divisors ^long num]
  (if (every? (fn [i] (divides? num i)) divisors) num false))

(time (prn (some (fn [^long i] (has-all-divisors (range 2 20) i)) (iterate inc 1))))

Java code: Java代码:

public class Problem5 {
  public static void main(String[] args) {
    long start = System.currentTimeMillis();
    int i = 1;
    while(!hasAllDivisors(i, 2, 20)) {
      i++;
    }
    long end = System.currentTimeMillis();
    System.out.println(i);
    System.out.println("Elapsed time " + (end - start));
  }

  public static boolean hasAllDivisors(int num, int startDivisor, int stopDivisor) {
    for(int divisor=startDivisor; divisor<=stopDivisor; divisor++) {
      if(!divides(num, divisor)) return false;
    }
    return true;
  }

  public static boolean divides(int num, int divisor) {
    return num % divisor == 0;
  }
}

Some performance problems: 一些性能问题:

  • The (range 2 20) call is creating a new lazy list of numbers for every increment of i . (range 2 20)调用正在为i每个增量创建一个新的惰性数字列表。 This is expensive, and is causing lots of unnecessary GC. 这很昂贵,并且导致许多不必要的GC。
  • You are doing a lot of boxing by passing through function calls. 你通过函数调用做了很多拳击。 Even the (iterate inc 1) is doing boxing / unboxing at every increment. 甚至(iterate inc 1)也在每次增量时进行装箱/拆箱。
  • You are traversing a sequence of divisors. 你正在遍历一系列除数。 This is slower than a straight iterative loop 这比直接迭代循环慢
  • mod is actually not a very well optimised function in Clojure at present. mod目前在Clojure中实际上不是一个非常优化的函数。 You are much better off using rem 你最好使用rem

You can solve the first problem by using a let statement to define the range just once: 您可以使用let语句仅定义一次范围来解决第一个问题:

(time (let [rng (range 2 20)]
  (prn (some (fn [^long i] (has-all-divisors rng i)) (iterate inc 1)))))
=> "Elapsed time: 48863.801522 msecs"

You can solve the second problem with loop/recur: 你可以用loop / recur解决第二个问题:

(time (let [rng (range 2 20)
           f (fn [^long i] (has-all-divisors rng i))]
       (prn (loop [i 1] 
              (if (f i)
                i
                (recur (inc i)))))))
=> "Elapsed time: 32757.594957 msecs"

You can solve the third problem by using an iterative loop over the possible divisors: 您可以通过对可能的除数使用迭代循环来解决第三个问题:

(defn has-all-divisors [^long num]
  (loop [d (long 2)]
    (if (zero? (mod num d))
      (if (>= d 20) true (recur (inc d)))
      false)))

 (time (prn (loop [i (long 1)] (if (has-all-divisors i) i (recur (inc i))))))
 => "Elapsed time: 13369.525651 msecs"

You can solve the final problem using rem 您可以使用rem解决最终问题

(defn has-all-divisors [^long num]
  (loop [d (long 2)]
    (if (== 0 (rem num d))
      (if (>= d 20) true (recur (inc d)))
      false)))

 (time (prn (loop [i (long 1)] (if (has-all-divisors i) i (recur (inc i))))))
=> "Elapsed time: 2423.195407 msecs"

As you can see, it is now competitive with the Java version. 如您所见,它现在与Java版本竞争。

In general, you can usually make Clojure almost as fast as Java with a bit of effort. 通常,您通常可以通过一些努力使Clojure几乎与Java一样快。 The main tricks are usually: 主要技巧通常是:

  • Avoid lazy functional features. 避免懒惰的功能。 They are nice, but add some overhead which can be problematic in low-level computation-intensive code. 它们很好,但增加了一些开销,这在低级计算密集型代码中可能会有问题。
  • Use primitive / unchecked maths 使用原始/未经检查的数学
  • Use loop/recur rather than sequences 使用循环/重复而不是序列
  • Ensure you are not doing any reflection on Java objects (ie (set! *warn-on-reflection* true) and eliminate all warnings you find) 确保您没有对Java对象进行任何反射(即(set! *warn-on-reflection* true)并消除您发现的所有警告)

I have not been able to reproduce the 1500 ms performance. 我无法再现1500毫秒的性能。 The Clojure code seems actually twice as fast as the Java version after compilation to uberjar. 在编译成uberjar之后,Clojure代码实际上似乎是Java版本的两倍。

Now timing Java version
    232792560
"Elapsed time: 4385.205 msecs"

Now timing Clojure version
    232792560
"Elapsed time: 2511.916 msecs"

I put the java class in resources/HasAllDivisors.java 我把java类放在resources / HasAllDivisors.java中

public class HasAllDivisors {

    public static long findMinimumWithAllDivisors() {
        long i = 1;
        while(!hasAllDivisors(i,2,20)) i++;
        return i;
    }

    public static boolean hasAllDivisors(long num, int startDivisor, int stopDivisor) {
        for(int divisor = startDivisor; divisor <= stopDivisor; divisor++) {
            if(num % divisor > 0) return false;
        }
        return true;
    }

    public static void main(String[] args){
        long start = System.currentTimeMillis();
        long i = findMinimumWithAllDivisors();
        long end = System.currentTimeMillis();
        System.out.println(i);
        System.out.println("Elapsed time " + (end - start));
    }

}

And in Clojure 在Clojure

(time (prn (HasAllDivisors/findMinimumWithAllDivisors)))

(println "Now timing Clojure version")
(time
    (prn
        (loop [i (long 1)]
            (if (has-all-divisors i)
                i
                (recur (inc i))))))

Even on the command line the java class is not reproducing the fast speed. 即使在命令行上,java类也没有再现快速的速度。

$ time java HasAllDivisors
  232792560
Elapsed time 4398

real   0m4.563s
user   0m4.597s
sys    0m0.029s

I know this is an old question, but I've been running into similar things. 我知道这是一个老问题,但我一直在遇到类似的问题。 It looks like the statement from the OP, that Clojure is much worse than Java on simple loops, is true. 看起来OP的声明,Clojure在简单循环上比Java差得多,是真的。 I went through the process in this thread, starting with OP's code and then adding the performance improvements. 我在这个帖子中完成了这个过程,从OP的代码开始,然后添加了性能改进。 At the end of it all, the java code runs in around 300 ms and the optimized Clojure code runs in 3000 ms. 最后,java代码运行大约300毫秒,优化的Clojure代码运行3000毫秒。 Creating an uberjar with lein gets the Clojure code down to 2500 ms. 使用lein创建一个uberjar可以将Clojure代码缩短到2500毫秒。

Since we know the answer that the given code spits out, I used that to have the Clojure code merely loop the number of times without doing the mod/rem calculations. 由于我们知道给定代码吐出的答案,我使用它来让Clojure代码仅循环次数而不进行mod / rem计算。 It simply goes through the loops. 它只是通过循环。

(def target 232792560)

(defn has-all-divisors [divisors ^long num]
    (loop [d (long 2)]
        (if (< d 20) (recur (inc d)))))

(time (let [rng (range 2 20)
            f (fn [^long i] (has-all-divisors (range 2 20) i)) ]
    (prn (loop [i 1] 
            (if (< i target)
                (do (f i) (recur (inc i))))))))

The resulting times are roughly the same as doing the calculations, ie 3000 ms. 结果时间与计算大致相同,即3000毫秒。 So, it's taking Clojure that long simply to walk through that many loops. 因此,只需要通过Clojure就可以完成很多循环。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM