Spoiler alert,这是Project Euler的第5个问题。
我正在尝试学习Clojure并解决问题5,但它慢了几个数量级(Java中为1515 ms,而Clojure中为169932 ms)。我甚至尝试使用类型提示,未经检查的数学运算和内联函数都是徒劳的。
为什么我的Clojure代码这么慢?
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代码:
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;
}
}
答案 0 :(得分:53)
一些性能问题:
(range 2 20)
调用正在为i
的每个增量创建一个新的惰性数字列表。这很昂贵,导致大量不必要的GC。(iterate inc 1)
在每次增量都进行装箱/拆箱。mod
目前在Clojure中实际上并不是一个非常优化的函数。使用rem
您可以使用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"
你可以用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"
您可以通过对可能的除数使用迭代循环来解决第三个问题:
(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"
您可以使用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"
正如您所看到的,它现在与Java版本竞争。
通常,您通常可以通过一些努力使Clojure几乎与Java一样快。主要技巧通常是:
(set! *warn-on-reflection* true)
并消除您发现的所有警告)答案 1 :(得分:1)
我无法再现1500毫秒的性能。在编译成uberjar之后,Clojure代码实际上似乎是Java版本的两倍。
Now timing Java version
232792560
"Elapsed time: 4385.205 msecs"
Now timing Clojure version
232792560
"Elapsed time: 2511.916 msecs"
我将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));
}
}
在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))))))
即使在命令行上,java类也没有再现快速速度。
$ time java HasAllDivisors
232792560
Elapsed time 4398
real 0m4.563s
user 0m4.597s
sys 0m0.029s
答案 2 :(得分:0)
我知道这是一个老问题,但是我遇到了类似的问题。从OP看来,Clojure在简单循环上比Java更糟糕,这是事实。我在此线程中进行了整个过程,从OP的代码开始,然后添加了性能改进。最后,Java代码在300毫秒内运行,优化的Clojure代码在3000毫秒内运行。用lein创建一个uberjar可使Clojure代码降低到2500毫秒。
由于我们知道给定代码吐出的答案,因此我使用该方法使Clojure代码仅循环次数而不进行mod / rem计算。它只是遍历循环。
(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))))))))
结果时间与计算大致相同,即3000毫秒。因此,Clojure花费这么长时间只是为了走过这么多循环。