[英]Clojure performance really bad on simple loop versus Java
Spoiler警报,这是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;
}
}
一些性能问题:
(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)
并消除您发现的所有警告) 我无法再现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
我知道这是一个老问题,但我一直在遇到类似的问题。 看起来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就可以完成很多循环。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.