22

Java、Scala、Clojure を学ぼうとしています。

私は 3 つの言語で Project Euler の問題に取り組んでいます。以下に、問題 #5 ( http://projecteuler.net/problem=5 ) のコードと、最初の 5 つの問題のこれまでの実行時間 (秒単位) を示します。問題 5 で、Java と Clojure のバージョンが Scala のバージョンよりもはるかに遅いことは、私にとって驚くべきことです。それらは同じマシン、同じ jvm で実行されており、結果はいくつかの試行で一貫しています。2 つ (特に 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  |

問題 5 の Java バージョン

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;
  }

}

問題#5のScalaバージョン

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)
  }

}

問題 5 の Clojure バージョン

(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)))))

編集

さまざまな回答を自分の回答にまとめることが提案されました。ただし、クレジットが必要な場合はクレジットを提供したいと思います(実際には、この質問には自分で答えていません)。

最初の質問に関しては、より優れたアルゴリズムを使用することで、3 つのバージョンすべてを高速化できます。具体的には、1 ~ 20 の数字の最大公約数 (2^4、3^2、5^1、7^1、11^1、13^1、17^1、19^1) のリストを作成し、それらを乗算します。

はるかに興味深い側面は、基本的に同じアルゴリズムを使用して 3 つの言語の違いを理解することです。このようなブルー​​ト フォース アルゴリズムが役立つ場合があります。では、なぜパフォーマンスの違いが生じるのでしょうか?

Java の場合、ArrayList を int のプリミティブ配列に変更することをお勧めします。これにより実行時間が短縮され、約 0.5 ~ 1 秒短縮されます (今朝実行したところ、実行時間が 4.386 秒から 3.577 秒に短縮されました。少し短縮されましたが、誰も(Scala バージョンと同様) 0.5 秒未満にする方法. 3 つすべてが Java バイトコードにコンパイルされることを考えると、これは驚くべきことです. @didirc によって、不変イテレータを使用するという提案がありました. 私はこの提案をテストしました.実行時間が 5 秒強に増加しました。

Clojure については、@mikera と @Webb がスピードアップのためのいくつかの提案をしています。彼らは、2 つのループ変数を使用した高速反復には 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 の範囲は実際にはオブジェクトのリストではなく、1 つのオブジェクトであると述べています。とてもかっこいい。したがって、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 でどのように実装されるかを知ることは興味深いでしょう。

次のように、ImmutableRange クラスを作成してコードを改善するという @didierc の提案:

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 コードは、エラトステネスの篩試行分割を使用して、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;
  }
}
4

9 に答える 9

5

他のソリューションは理由もなく明示的なコレクションを作成するため、Scala の方が高速です。Scala では、 ~ から~ まで1 to topの数値を表すオブジェクトを作成しますが、それらをどこにも明示的にリストしません。Java では、リストを明示的に作成します。反復ごとに 20 個の配列 ( もオブジェクトであるため、実際には 21 個のオブジェクト) を作成するよりも、1 つのオブジェクトを作成する方がはるかに高速です。1topArrayList

(どのバージョンも実際には最適に近いものではないことに注意してください。「最小公倍数」を参照してください。これは、Eastsun が言及せずに行っていることです。)

于 2013-02-03T19:04:50.820 に答える
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/recur2 つのループ変数を使用した高速反復に使用
  • わずかに高速な数学演算に使用unchecked-mathします (ここではオーバーフローの危険がないことがわかっているため)
  • ボックス化された数値ではなくプリミティブ long を使用する
  • 次のような高次関数を避けるevery?- 低レベル操作よりもオーバーヘッドが高い

明らかに、速度を本当に気にするなら、より良いアルゴリズムを選ぶでしょう:-)

于 2013-02-04T04:11:34.703 に答える
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

これをいじり続けて、おそらくさらに近づくこともできますが、2,000 万から 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 倍以上高速です。gcd/lcm の折りたたみソリューションが Scala にも投稿されていることに気付きました。したがって、これらの同様のアルゴリズムの速度を比較することは興味深いでしょう。

于 2013-02-03T05:23:09.457 に答える
4

Java バージョンの速度におそらく何らかの影響を与えることに最初に気付いたのは、 のArrayList<Integer>代わりに を作成していることですint[]

Java にはバージョン 5 以降、 と の間で自動的に変換する機能がIntegerありintます。このリストを反復処理しint、比較と数学計算でそれらを型として扱います。これにより、Java は 2 つの型の間で多くのサイクルを変換する必要があります。ArrayList<Integer>yourを anに置き換えると、int[]パフォーマンスに影響が出る可能性があります。

あなたのタイミングを見るときの私の最初の本能は、すべてが正しい結果を出していることを確認することです. 3 つすべてを適切にテストして、より高速な Scala バージョンが実際に正しい結果をもたらすことを確認したと思います。

戦略は3つすべてで同じに見えるため、それを解決するためのアルゴリズムの選択とは関係がないようです(私はClojureまたはScalaに精通していないため、微妙な違いを見落としている可能性があります)。おそらく、Scala はこの特定のループ/アルゴリズムを内部的に最適化し、はるかに高速な結果をもたらすことができるでしょうか?

于 2013-02-03T02:08:38.963 に答える
2

アルゴリズムに従ってください。clojure は Java バージョンよりも約 10 倍遅くなります。

clojure バージョンの方が少し速い: 46555ms => 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 => 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;
}
于 2013-02-03T04:11:25.793 に答える
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);
  }
}

いくつかの詳細:

  1. 質問で値として言及されているため、2520 でテストを開始できます:)
  2. 範囲の上限では、下限よりも早く失敗するように思えました。
  3. continue ステートメントにラベルを使用しました。これは基本的に、for ループをリセットし、テスト ケースをインクリメントするための安価で合成的な方法です。
于 2013-02-05T02:40:38.260 に答える
1

問題はボクシング、怠惰、リスト、ベクトルなどではありません。問題はアルゴリズムに関するものです。もちろん解決策は「力ずく」ですが、「力」に占める「力ずく」の割合くらいです。

まず、オイラー問題 5 では、 1 から nで割り切れるかどうかを確認するよう求められません。1 から 20 だけです。つまり、2 番目に、解は 38 の倍数でなければなりません。3 番目に、素数を最初にチェックし、すべての除数を降順でチェックして、できるだけ早く失敗するようにする必要があります。第 4 に、一部の約数は他の約数も保証します。つまり、ある数が 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"
于 2013-02-04T02:44:06.203 に答える
1

まず、例えば 4 で割り切れる数は、2 (4 の因数の 1 つ) でも割り切れます。

したがって、1 から 20 までのすべての数字ではなく、一部の数字のみを確認する必要があります。

次に、数値を素因数分解できる場合、これは単純に最小公倍数を求めているだけです (これは、この問題にアプローチする別の方法です)。実際、1-20しかないので、おそらくペンと紙でそれを行うことができます.

あなたが取り組んでいるアルゴリズムはかなり単純です。問題が提供する情報を最大限に活用していません。

于 2013-02-03T00:47:14.480 に答える
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 の 2 つの可能な解決策を次に示します。

短いもの:

(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 }
于 2013-02-03T02:08:24.347 に答える