19

私は Clojure でいくつかの基本的な複素数演算を実装していましたが、型ヒントを使用しても、ほぼ同等の Java コードよりも約 10 倍遅いことに気付きました。

比較:

(defn plus [[^double x1 ^double y1] [^double x2 ^double y2]]
    [(+ x1 x2) (+ y1 y2)])

(defn times [[^double x1 ^double y1] [^double x2 ^double y2]]
    [(- (* x1 x2) (* y1 y2)) (+ (* x1 y2) (* y1 x2))])

(time (dorun (repeatedly 100000 #(plus [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(times [1 0] [0 1])))) 

出力:

"Elapsed time: 69.429796 msecs"
"Elapsed time: 72.232479 msecs"

と:

public static void main( String[] args ) {
  double[] z1 = new double[] { 1, 0 };
  double[] z2 = new double[] { 0, 1 };
  double[] z3 = null;

  long l_StartTimeMillis = System.currentTimeMillis();
  for ( int i = 0; i < 100000; i++ ) {
    z3 = plus( z1, z2 ); // assign result to dummy var to stop compiler from optimising the loop away
  }
  long l_EndTimeMillis = System.currentTimeMillis();
  long l_TimeTakenMillis = l_EndTimeMillis - l_StartTimeMillis;
  System.out.format( "Time taken: %d millis\n", l_TimeTakenMillis );


  l_StartTimeMillis = System.currentTimeMillis();
  for ( int i = 0; i < 100000; i++ ) {
    z3 = times( z1, z2 );
  }
  l_EndTimeMillis = System.currentTimeMillis();
  l_TimeTakenMillis = l_EndTimeMillis - l_StartTimeMillis;
  System.out.format( "Time taken: %d millis\n", l_TimeTakenMillis );

  doNothing( z3 );
}

private static void doNothing( double[] z ) {

}

public static double[] plus (double[] z1, double[] z2) {
  return new double[] { z1[0] + z2[0], z1[1] + z2[1] };
}

public static double[] times (double[] z1, double[] z2) {
  return new double[] { z1[0]*z2[0] - z1[1]*z2[1], z1[0]*z2[1] + z1[1]*z2[0] };
}

出力:

Time taken: 6 millis
Time taken: 6 millis

実際、型ヒントは違いを生むようには見えません。それらを削除すると、ほぼ同じ結果が得られます。本当に奇妙なのは、REPLなしで Clojure スクリプトを実行すると、結果が遅くなることです。

"Elapsed time: 137.337782 msecs"
"Elapsed time: 214.213993 msecs"

私の質問は次のとおりです。どうすれば Java コードのパフォーマンスに近づくことができますか? また、REPL なしで clojure を実行すると、一体なぜ式の評価に時間がかかるのでしょうか?

更新 ==============

deftypeと で型ヒントを使用しdeftype、ではなくdefn使用すると、Java バージョンと同等またはそれ以上のパフォーマンスが得られます。お二人のおかげです。dotimesrepeatedly

(deftype complex [^double real ^double imag])

(defn plus [^complex z1 ^complex z2]
  (let [x1 (double (.real z1))
        y1 (double (.imag z1))
        x2 (double (.real z2))
        y2 (double (.imag z2))]
    (complex. (+ x1 x2) (+ y1 y2))))

(defn times [^complex z1 ^complex z2]
  (let [x1 (double (.real z1))
        y1 (double (.imag z1))
        x2 (double (.real z2))
        y2 (double (.imag z2))]
    (complex. (- (* x1 x2) (* y1 y2)) (+ (* x1 y2) (* y1 x2)))))

(println "Warm up")
(time (dorun (repeatedly 100000 #(plus (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(times (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(plus (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(times (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(plus (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(times (complex. 1 0) (complex. 0 1)))))

(println "Try with dorun")
(time (dorun (repeatedly 100000 #(plus (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(times (complex. 1 0) (complex. 0 1)))))

(println "Try with dotimes")
(time (dotimes [_ 100000]
        (plus (complex. 1 0) (complex. 0 1))))

(time (dotimes [_ 100000]
        (times (complex. 1 0) (complex. 0 1))))

出力:

Warm up
"Elapsed time: 92.805664 msecs"
"Elapsed time: 164.929421 msecs"
"Elapsed time: 23.799012 msecs"
"Elapsed time: 32.841624 msecs"
"Elapsed time: 20.886101 msecs"
"Elapsed time: 18.872783 msecs"
Try with dorun
"Elapsed time: 19.238403 msecs"
"Elapsed time: 17.856938 msecs"
Try with dotimes
"Elapsed time: 5.165658 msecs"
"Elapsed time: 5.209027 msecs"
4

2 に答える 2

25

パフォーマンスが低下する理由として考えられるのは、次のとおりです。

  • Clojure のベクトルは、本質的に Java の double[] 配列よりも重いデータ構造です。したがって、ベクトルの作成と読み取りにはかなりの余分なオーバーヘッドがあります。
  • 関数への引数として、またそれらがベクトルに入れられたときに、double をボックス化しています。この種の低レベルの数値コードでは、ボックス化/ボックス化解除は比較的高価です。
  • 型ヒント ( ^double) は役に立ちません。通常の Clojure 関数ではプリミティブ型ヒントを使用できますが、ベクターでは機能しません。

詳細については、原始演算の高速化に関するこのブログ投稿を参照してください。

Clojure で本当に高速な複素数が必要な場合は、おそらくdeftype次のようなを使用して実装する必要があります。

(deftype Complex [^double real ^double imag])

次に、この型を使用してすべての複雑な関数を定義します。これにより、基本的な算術演算を全体で使用できるようになり、適切に作成された Java コードのパフォーマンスとほぼ同等になるはずです。

于 2012-08-06T08:49:40.113 に答える
4
  • ベンチマークテストについてはよくわかりませんが、テストを開始するときに jvm をウォームアップする必要があるようです。したがって、REPL で実行すると、すでにウォームアップされています。スクリプトとして実行するときはまだです。

  • Java では、1 つのメソッド内ですべてのループを実行します。plusand以外のメソッドtimesは呼び出されません。clojure では、匿名関数を作成し、それを呼び出すために繰り返し呼び出します。時間がかかります。で置き換えることができますdotimes

私の試み:

(println "Warm up")
(time (dorun (repeatedly 100000 #(plus [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(times [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(plus [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(times [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(plus [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(times [1 0] [0 1]))))

(println "Try with dorun")
(time (dorun (repeatedly 100000 #(plus [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(times [1 0] [0 1]))))

(println "Try with dotimes")
(time (dotimes [_ 100000]
        (plus [1 0] [0 1])))

(time (dotimes [_ 100000]
        (times [1 0] [0 1])))

結果:

Warm up
"Elapsed time: 367.569195 msecs"
"Elapsed time: 493.547628 msecs"
"Elapsed time: 116.832979 msecs"
"Elapsed time: 46.862176 msecs"
"Elapsed time: 27.805174 msecs"
"Elapsed time: 28.584179 msecs"
Try with dorun
"Elapsed time: 26.540489 msecs"
"Elapsed time: 27.64626 msecs"
Try with dotimes
"Elapsed time: 7.3792 msecs"
"Elapsed time: 5.940705 msecs"
于 2012-08-06T08:56:11.793 に答える