2013-01-02 9 views
29

Spoiler警告です。これはProject Eulerの問題5です。Clojureの性能は、単純なループとJavaの比較では非常に悪いです。

Clojureを学習しようとしていますが、問題5を解決しようとしていますが、ClojureではJavaでは1515 ms対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; 
    } 
} 
+1

Javaコードは2〜18になりますが、Clojureコードは2〜20になります。 – Ankur

+0

申し訳ありませんが、私はそれを修正しました。私は誤ったバージョンのコードを間違って貼り付けていましたが、両方のタイミングが正確に20になるようになっていました。 – gleenn

+0

System.currentTimeMillis()をベンチマークとしますか?これは深刻ではない。 http://shipilev.net/talks/devoxx-Nov2013-benchmarking.pdf – Puh

答えて

52

いくつかのパフォーマンスの問題:

  • (range 2 20)コールがiのすべての増分のための番号の新しい怠惰なリストを作成しています。これは高価であり、不必要なGCをたくさん引き起こしています。
  • あなたは関数呼び出しを通すことによって多くのボクシングをしています。 (iterate inc 1)でもすべての増分でボクシング/アンボクシングを行っています。
  • あなたは除数のシーケンスをトラバースしています。これは、ストレート反復ループよりも遅いです。
  • modは現在、Clojureでは実際には非常に最適化された関数ではありません。

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

    あなたはループ/ RECURの第2の問題を解決することができます:あなたは一度だけの範囲を定義するためにletステートメントを使用して最初の問題を解決することができますrem

を使用してオフにはるかに優れている

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

あなたは、可能な除数の上に反復ループを使用して第三の問題を解決することができます:

あなたが見ることができるように210
(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とほぼ同じくらい高速に動作します。主なテクニックは通常次のとおりです。

  • 遅延機能フィーチャを避けてください。彼らはいいですが、いくつかのオーバーヘッドを追加します。これは、低レベルの計算集約的なコードで問題になる可能性があります。私は持っていない
  • 利用プリミティブ/未確認の数学は
  • 使用ループ/再発
  • ではなく、シーケンス
  • を使用すると、Javaオブジェクト上の任意の反射を行っていないことを確認し(すなわち(set! *warn-on-reflection* true)とあなたが見つけるすべての警告をなくす)
+1

私はこれが少し悲しいと言う必要があります。パフォーマンスのために機能的な機能を犠牲にしなければならないことを示唆しているようです。パフォーマンスを得るためにCスタイルのコードを書く必要がある場合、コンパイラは作業を必要としますか? – Hendekagon

+9

それはちょっと悲しいかもしれませんが、それはまた人生の事実です。高級言語の機能には、しばしば費用/オーバーヘッドが必要です。私はコンパイラでより賢明な作業ができると確信していますが、遅延データ構造には、同等の非遅延オブジェクトよりもオーバーヘッドが多いという事実を変更することはできません。 – mikera

+8

もう1つのことは、Clojureには選択肢があります。パフォーマンスに問題がなければ、レイジーシーケンスと好きなすべてのハイレベル機能を使ってコードを書くことができます。おそらくはるかにコンパクトです。しかし、パフォーマンスが必要な場合は、いつも他の方法でも良い結果を得ることができます。 –

1

1500ミリ秒のパフォーマンスを再現できました。Clojureコードは、uberjarへのコンパイル後に実際にはJavaバージョンの2倍の速さです。

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

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

私もJavaのクラスは速いスピードを再現されていないコマンドライン上のリソースで/ 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 
+0

私はjarredコードをまったく使用していませんでした。おそらくあなたはperfがより良くなったところで新しいバージョンのclojureを使用していますか?物事を聞くには常に良い方向に向かっています。 – gleenn

+0

私は別のデータポイントを追加します。スタンドアロンのJARファイル(Clojureなし)でJavaコードを実行すると、マシン上でほぼ即座に(372ms)返されますが、mikeraのポストで最適化されたClojure uberjar(1.8または1.9)コードは約2300msかかります。 mikeraの最初の修正(一度定義された範囲)で元の機能コードを開くと、はるかに遅い24390msになります。 2017 MBPからのすべての結果。 – nogridbag

関連する問題