我正在努力学习Java,Scala和& Clojure的。
我正在研究三种语言的项目Euler问题。下面列出了问题#5(http://projecteuler.net/problem=5)的代码以及前五个问题的运行时间(以秒为单位)。令我惊讶的是Java和Clojure版本比问题#5的Scala慢得多。它们运行在同一台机器上,相同的jvm,结果在几次试验中是一致的。我能做些什么来加速这两个(特别是Clojure版本)?为什么Scala版本更快?
|---------|--------|--------|----------|
| problem | Java | Scala | Clojure |
|=========|========|========|==========|
| 1 | .0010 | .1570 | .0116 |
| 2 | .0120 | .0030 | .0003 |
| 3 | .0530 | .0200 | .1511 |
| 4 | .2120 | .2600 | .8387 |
| 5 | 3.9680 | .3020 | 33.8574 |
public class Problem005 {
private static ArrayList<Integer> divisors;
private static void initializeDivisors(int ceiling) {
divisors = new ArrayList<Integer>();
for (Integer i = 1; i <= ceiling; i++)
divisors.add(i);
}
private static boolean isDivisibleByAll(int n) {
for (int divisor : divisors)
if (n % divisor != 0)
return false;
return true;
}
public static int findSmallestMultiple (int ceiling) {
initializeDivisors(ceiling);
int number = 1;
while (!isDivisibleByAll(number))
number++;
return number;
}
}
object Problem005 {
private def isDivisibleByAll(n: Int, top: Int): Boolean =
(1 to top).forall(n % _ == 0)
def findSmallestMultiple(ceiling: Int): Int = {
def iter(n: Int): Int = if (isDivisibleByAll(n, ceiling)) n else iter(n+1)
iter(1)
}
}
(defn smallest-multiple-of-1-to-n
[n]
(loop [divisors (range 2 (inc n))
i n]
(if (every? #(= 0 (mod i %)) divisors)
i
(recur divisors (inc i)))))
有人建议我将各种答案汇编成我自己的答案。但是,我想在信用到期时给予信任(我自己真的没有回答这个问题)。
关于第一个问题,可以通过使用更好的算法来加速所有三个版本。具体来说,创建一个最大公因子列表1-20(2 ^ 4,3 ^ 2,5 ^ 1,7 ^ 1,11 ^ 1,11 ^ 1,17 ^ 1,19 ^ 1)和将它们相乘。
更有趣的方面是使用基本相同的算法来理解三种语言之间的差异。有些情况下,像这样的强力算法可能会有所帮助。那么,为什么性能差异呢?
对于Java,一个建议是将ArrayList更改为一个原始的int数组。这确实减少了运行时间,减少了大约0.5-1秒的时间(我今天早上跑了它,它将运行时间从4.386秒减少到3.577秒。这减少了一点,但没有人能够想出一个将它带到半秒以下的方式(类似于Scala版本)。考虑到所有三个都编译成java字节码,这是令人惊讶的。@ didierc建议使用不可变迭代器;我测试了这个建议,它将运行时间增加到超过5秒。
对于Clojure,@ mikera和@Webb提出了一些加快建议的建议。他们建议使用loop / recur进行快速迭代,使用两个循环变量,unchecked-math用于稍微更快的数学运算(因为我们知道这里没有溢出的危险),使用原始long而不是盒装数字,并避免更高阶函数,如每?
运行@mikera的代码,我最终的运行时间为2.453秒,不如scala代码好,但比我的原始版本好得多,比Java版本更好:
(set! *unchecked-math* true)
(defn euler5
[]
(loop [n 1
d 2]
(if (== 0 (unchecked-remainder-int n d))
(if (>= d 20) n (recur n (inc d)))
(recur (inc n) 2))))
(defn is-divisible-by-all?
[number divisors]
(= 0 (reduce + (map #(mod 2 %) divisors))))
对于Scala,@ didierc声明范围对象1到20实际上不是对象列表而是一个对象。很酷。因此,Scala中的性能差异在于我们迭代单个对象而不是整数1-20的列表/数组。
实际上,如果我将scala方法中的辅助函数从范围对象更改为列表(参见下文),那么scala版本的运行时间将从0.302秒增加到226.59秒。
private def isDivisibleByAll2(n: Int, top: Int): Boolean = {
def divisors: List[Int] = List(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20)
divisors.forall(n % _ == 0)
}
因此,似乎@didierc已正确识别scala在此实例中的优势。知道如何在java和clojure中实现这种类型的对象会很有趣。
@didierc建议通过创建一个ImmutableRange类来改进代码,如下所示:
import java.util.Iterator;
import java.lang.Iterable;
public class ImmutableRange implements Iterable<Integer> {
class ImmutableRangeIterator implements Iterator<Integer> {
private int counter, end, step;
public ImmutableRangeIterator(int start_, int end_, int step_) {
end = end_;
step = step_;
counter = start_;
}
public boolean hasNext(){
if (step>0) return counter <= end;
else return counter >= end;
}
public Integer next(){
int r = counter;
counter+=step;
return r;
}
public void remove(){
throw new UnsupportedOperationException();
}
}
private int start, end, step;
public ImmutableRange(int start_, int end_, int step_){
// fix-me: properly check for parameters consistency
start = start_;
end = end_;
step = step_;
}
public Iterator<Integer> iterator(){
return new ImmutableRangeIterator(start,end,step);
}
}
没有改善运行时间。 java版本在我的机器上以5.097秒运行。因此,最后,我们得到了一个令人满意的答案,为什么Scala版本表现更好,我们理解如何提高Clojure版本的性能,但缺少的是理解如何在Java中实现Scala的不可变范围对象
正如一些人所评论的那样,改善此代码运行时间的最有效方法是使用更好的算法。例如,以下java代码使用Sieve of Eratosthenes和Trial Division在不到1毫秒的时间内计算答案:
/**
* Smallest Multiple
*
* 2520 is the smallest number that can be divided by each of the numbers
* from 1 to 10 without any remainder. What is the smallest positive number
* that is evenly divisible by all of the numbers from 1 to 20?
*
* User: Alexandros Bantis
* Date: 1/29/13
* Time: 7:06 PM
*/
public class Problem005 {
final private static int CROSSED_OUT = 0;
final private static int NOT_CROSSED_OUT = 1;
private static int intPow(int base, int exponent) {
int value = 1;
for (int i = 0; i < exponent; i++)
value *= base;
return value;
}
/**
* primesTo computes all primes numbers up to n using trial by
* division algorithm
*
* @param n designates primes should be in the range 2 ... n
* @return int[] a sieve of all prime factors
* (0=CROSSED_OUT, 1=NOT_CROSSED_OUT)
*/
private static int[] primesTo(int n) {
int ceiling = (int) Math.sqrt(n * 1.0) + 1;
int[] sieve = new int[n+1];
// set default values
for (int i = 2; i <= n; i++)
sieve[i] = NOT_CROSSED_OUT;
// cross out sieve values
for (int i = 2; i <= ceiling; i++)
for (int j = 2; i*j <= n; j++)
sieve[i*j] = CROSSED_OUT;
return sieve;
}
/**
* getPrimeExp computes a prime factorization of n
*
* @param n the number subject to prime factorization
* @return int[] an array of exponents for prime factors of n
* thus 8 => (0^0, 1^0, 2^3, 3^0, 4^0, 5^0, 6^0, 7^0, 8^0)
*/
public static int[] getPrimeExp(int n) {
int[] factor = primesTo(n);
int[] primePowAll = new int[n+1];
// set prime_factor_exponent for all factor/exponent pairs
for (int i = 2; i <= n; i++) {
if (factor[i] != CROSSED_OUT) {
while (true) {
if (n % i == 0) {
n /= i;
primePowAll[i] += 1;
} else {
break;
}
}
}
}
return primePowAll;
}
/**
* findSmallestMultiple computes the smallest number evenly divisible
* by all numbers 1 to n
*
* @param n the top of the range
* @return int evenly divisible by all numbers 1 to n
*/
public static int findSmallestMultiple(int n) {
int[] gcfAll = new int[n+1];
// populate greatest common factor arrays
int[] gcfThis = null;
for (int i = 2; i <= n; i++) {
gcfThis = getPrimeExp(i);
for (int j = 2; j <= i; j++) {
if (gcfThis[j] > 0 && gcfThis[j] > gcfAll[j]) {
gcfAll[j] = gcfThis[j];
}
}
}
// multiply out gcf arrays
int value = 1;
for (int i = 2; i <= n; i++) {
if (gcfAll[i] > 0)
value *= intPow(i, gcfAll[i]);
}
return value;
}
}
答案 0 :(得分:5)
Scala更快,因为其他解决方案无缘无故地创建了显式集合。在Scala中,1 to top
创建一个对象,该对象表示从1
到top
的数字,但不会在任何地方明确列出它们。在Java中,你明确地创建了列表 - 并且每次迭代创建一个对象要比20个数组(实际上是21个对象,因为ArrayList
也是一个对象)快得多。
(请注意,没有一个版本实际上接近最佳状态。请参阅“最小公倍数”,这是Eastsun在没有提及的情况下所做的事情。)
答案 1 :(得分:5)
这是Clojure中更快的版本:
(set! *unchecked-math* true)
(defn euler5 []
(loop [n 1
d 2)]
(if (== 0 (unchecked-remainder-int n d))
(if (>= d 20) n (recur n (inc d)))
(recur (inc n) 2))))
(time (euler5))
=> "Elapsed time: 2438.761237 msecs"
即。它与Java版本的速度大致相同。
关键技巧是:
loop/recur
进行快速迭代,使用两个循环变量unchecked-math
进行稍微快速的数学运算(因为我们知道这里没有溢出的危险)every?
这样的高阶函数 - 它们比低级操作具有更高的开销显然,如果你真的关心速度,你会选择一个更好的算法: - )
答案 2 :(得分:4)
我注意到的第一件事可能会对Java版本的速度产生一些影响,因为您创建的是ArrayList<Integer>
而不是int[]
。
Java从版本5开始有一个功能,可以自动在Integer
和int
之间进行转换 - 您在此列表中进行迭代,在比较和数学中将它们视为int
类型计算,迫使Java花费大量的周期在两种类型之间进行转换。用ArrayList<Integer>
替换int[]
可能会对性能产生一些影响。
我在查看你的时间时的第一直觉是验证所有人都给出了正确的结果。我假设您已对所有三个进行了正确测试,以确保更快的Scala版本确实能为您提供正确的结果。
它似乎与解决它的算法选择无关,因为策略在所有三个中都看起来相同(我不熟悉Clojure或Scala,所以我可能会遗漏一些微妙的差异)。也许Scala能够在内部优化这个特定的循环/算法,产生更快的结果?
答案 3 :(得分:4)
在我痛苦的慢速计算机上,Clojure代码需要将近10分钟,所以我在这里的老忠实者的运行速度要慢20倍。
user=> (time (smallest-multiple-of-1-to-n 20))
"Elapsed time: 561420.259 msecs"
232792560
你可以通过避免懒惰,使用类型提示/原语/未经检查的操作等使这个相同的算法与其他算法更具可比性.Clojure代码是匿名函数的装箱原语,并创建/实现一个懒惰序列range
的每次迭代loop
。这种开销通常可以忽略不计,但在这里它被循环了数亿次。以下非惯用代码可提供3倍的加速。
(defn smallest-multiple-of-1-to-n [n]
(loop [c (int n)]
(if
(loop [x (int 2)]
(cond (pos? (unchecked-remainder-int c x)) false
(>= x n) true
:else (recur (inc x))))
c (recur (inc c)))))
user=> (time (smallest-multiple-of-1-to-n 20))
"Elapsed time: 171921.80347 msecs"
232792560
你可以继续修补这个并且可能更接近,但是最好通过算法来考虑,并且比从20到2亿的迭代做得更好。
(defn gcd [a b]
(if (zero? b) a (recur b (mod a b))))
(defn lcm
([a b] (* b (quot a (gcd a b))))
([a b & r] (reduce lcm (lcm a b) r)))
user=> (time (apply lcm (range 2 21)))
"Elapsed time: 0.268749 msecs"
232792560
因此,即使在我的古老机器上,这比你的快速机器上的任何算法实现快1000倍。我注意到为Scala发布了gcd / lcm折叠解决方案。因此,比较这些类似算法的速度会很有趣。
答案 4 :(得分:2)
按照你的算法,clojure比java版本慢大约10倍。
对于clojure版本来说快一点: 46555ms =&gt; 23846ms
(defn smallest-multiple-of-1-to-n
[n]
(let [divisors (range 2 (inc n))]
(loop [i n]
(if (loop [d 2]
(cond (> d n) true
(not= 0 (mod i d)) false
:else (recur (inc d))))
i
(recur (inc i))))))
Java版本快一点:3248ms =&gt; 2757ms
private static int[] divisors;
private static void initializeDivisors(int ceiling) {
divisors = new int[ceiling];
for (Integer i = 1; i <= ceiling; i++)
divisors[i - 1] = i;
}
答案 5 :(得分:1)
首先,如果一个数字可以被例如4整除,它也可以被2整除(4个因子中的一个)。
因此,从1-20开始,您只需要检查一些数字,而不是所有数字。
其次,如果您可以对数字进行素数分析,那么这只是要求您提供最低的公共乘数(这是解决此问题的另一种方法)。事实上,你可以用笔和纸来做,因为它只有1-20。
您正在使用的算法非常幼稚 - 它并没有充分利用问题为您提供的信息。
答案 6 :(得分:1)
这是scala中更有效的解决方案:
def smallestMultipe(n: Int): Int = {
@scala.annotation.tailrec
def gcd(x: Int, y: Int): Int = if(x == 0) y else gcd(y%x, x)
(1 to n).foldLeft(1){ (x,y) => x/gcd(x,y)*y }
}
我怀疑为什么你的问题1的scala版本如此无效。 以下是Scala中问题1的两种可能解决方案:
简短的一句:
(1 until 1000) filter (n => n%3 == 0 || n%5 == 0) sum
效率更高:
(1 until 1000).foldLeft(0){ (r,n) => if(n%3==0||n%5==0) r+n else r }
答案 7 :(得分:1)
问题不在于拳击,懒惰,列表,向量等。问题在于算法。当然,解决方案是“蛮力”,但它是关于“强力”中“粗暴”的比例。
首先,在欧拉问题5中,我们没有被要求通过 1到n 检查可分性:只有一到二十。那说:第二,解决方案必须是38的倍数。第三,必须首先检查素数,并且必须按降序检查所有除数,尽快失败。第四,一些除数也确保其他除数,即如果一个数可以被18整除,它也可以被9,6和3整除。最后,所有数字都可以被1整除。
在Clojure中的这个解决方案在MacBook Pro i7上的运行时间可以忽略不计410毫秒:
;Euler 5 helper
(defn divisible-by-all [n]
(let [divisors [19 17 13 11 20 18 16 15 14 12]
maxidx (dec (count divisors))]
(loop [idx 0]
(let [result (zero? (mod n (nth divisors idx)))]
(cond
(and (= idx maxidx) (true? result)) true
(false? result) false
:else (recur (inc idx)))))))
;Euler 5 solution
(defn min-divisible-by-one-to-twenty []
(loop[ x 38 ] ;this one can be set MUCH MUCH higher...
(let [result (divisible-by-all x)]
(if (true? result) x (recur (+ x 38))))))
user=>(time (min-divisible-by-one-to-twenty))
"Elapsed time: 410.06 msecs"
答案 8 :(得分:1)
我相信这是您可以为该问题和天真算法编写的最快的纯Java代码。它比Scala更快。
public class Euler5 {
public static void main(String[] args) {
int test = 2520;
int i;
again: while (true) {
test++;
for (i = 20; i >1; i--) {
if (test % i != 0)
continue again;
}
break;
}
System.out.println(test);
}
}
一些小细节: