29

私は、ClojureのパフォーマンスがJavaと同等の立場にあることを証明しようとしています。私が見つけた重要なユースケースはクイックソートです。私は次のように実装を書きました:

(set! *unchecked-math* true)

(defn qsort [^longs a]
  (let [qs (fn qs [^long low, ^long high]
             (when (< low high)
               (let [pivot (aget a low)
                     [i j]
                     (loop [i low, j high]
                       (let [i (loop [i i] (if (< (aget a i) pivot)
                                             (recur (inc i)) i))
                             j (loop [j j] (if (> (aget a j) pivot)
                                             (recur (dec j)) j))
                             [i j] (if (<= i j)
                                     (let [tmp (aget a i)]
                                       (aset a i (aget a j)) (aset a j tmp)
                                       [(inc i) (dec j)])
                                     [i j])]
                         (if (< i j) (recur i j) [i j])))]
                 (when (< low j) (qs low j))
                 (when (< i high) (qs i high)))))]
    (qs 0 (dec (alength a))))
  a)

また、これはJavaクイックソートを呼び出すのに役立ちます。

(defn jqsort [^longs a] (java.util.Arrays/sort a) a))

さて、ベンチマークのために。

user> (def xs (let [rnd (java.util.Random.)] 
        (long-array (repeatedly 100000 #(.nextLong rnd)))))
#'user/xs
user> (def ys (long-array xs))
#'user/ys
user> (time (qsort ys))
"Elapsed time: 163.33 msecs"
#<long[] [J@3ae34094>
user> (def ys (long-array xs))
user> (time (jqsort ys))
"Elapsed time: 13.895 msecs"
#<long[] [J@1b2b2f7f>

パフォーマンスは世界的に異なります(桁違いに、そしていくつか)。

私が見逃しているものはありますか、私が使用した可能性のあるClojure機能はありますか?パフォーマンス低下の主な原因は、ループからいくつかの値を返す必要があり、そのためのベクトルを割り当てる必要がある場合だと思います。これは避けられますか?

ところで、Clojure1.4を実行しています。また、HotSpotをウォームアップするために、ベンチマークを複数回実行したことにも注意してください。これらは彼らが落ち着く時です。

アップデート

私のコードの最もひどい弱点は、ベクトルの割り当てだけでなく、それらがボクシングを強制し、プリミティブチェーンを壊すという事実です。もう1つの弱点は、結果を使用することです。これは、結果loopもチェーンを切断するためです。はい、Clojureでのパフォーマンスはまだ地雷原です。

4

4 に答える 4

45

このバージョンは @mikera のものに基づいており、同じくらい高速で、醜いマクロを使用する必要はありません。私のマシンでは、java.util.Arrays/sort の場合は ~12ms に対して ~9ms かかります:

(set! *unchecked-math* true)
(set! *warn-on-reflection* true)

(defn swap [^longs a ^long i ^long j]
  (let [t (aget a i)]
    (aset a i (aget a j))
    (aset a j t)))

(defn ^long apartition [^longs a ^long pivot ^long i ^long j]
  (loop [i i j j]
    (if (<= i j)
      (let [v (aget a i)]
        (if (< v pivot)
          (recur (inc i) j)
          (do 
            (when (< i j) 
              (aset a i (aget a j))
              (aset a j v))
            (recur i (dec j)))))
      i)))

(defn qsort 
  ([^longs a]
     (qsort a 0 (long (alength a))))
  ([^longs a ^long lo ^long hi]    
     (when
         (< (inc lo) hi)
       (let [pivot (aget a lo)
             split (dec (apartition a pivot (inc lo) (dec hi)))]
         (when (> split lo)
           (swap a lo split))
         (qsort a lo split)
         (qsort a (inc split) hi)))
     a))

(defn ^longs rand-long-array []
  (let [rnd (java.util.Random.)] 
    (long-array (repeatedly 100000 #(.nextLong rnd)))))

(comment
  (dotimes [_ 10]
    (let [as (rand-long-array)]
      (time
       (dotimes [_ 1] 
         (qsort as)))))
  )

Clojure 1.3 以降では、手動でインライン化する必要はほとんどありません。関数の引数にのみいくつかの型ヒントを指定すると、JVM がインライン展開を行います。配列操作のためにインデックス引数を int にキャストする必要はありません。Clojure がこれを行います。

注意すべきことの 1 つは、入れ子になった loop/recur が JVM のインライン化に問題を引き起こすことです。これは、loop/recur が (現時点では) 戻りプリミティブをサポートしていないためです。したがって、コードを別々の fn に分割する必要があります。ネストされたループ/再帰はClojureでとにかく非常に醜くなるので、これは最善です。

Java パフォーマンスを一貫して達成する方法 (実際に必要な場合) の詳細については、test.benchmarkを調べて理解してください。

于 2012-08-29T16:27:46.127 に答える
11

マクロのせいでこれは少し恐ろしいですが、このコードで Java の速度に匹敵すると思います (ベンチマークでは約 11ms です)。

(set! *unchecked-math* true)

(defmacro swap [a i j]
  `(let [a# ~a
         i# ~i
         j# ~j
         t# (aget a# i#)]
     (aset a# i# (aget a# j#))
     (aset a# j# t#)))

(defmacro apartition [a pivot i j]
  `(let [pivot# ~pivot]
     (loop [i# ~i
            j# ~j]
       (if (<= i# j#)
         (let [v# (aget ~a i#)]
           (if (< v# pivot#)
             (recur (inc i#) j#)
             (do 
               (when (< i# j#) 
                 (aset ~a i# (aget ~a j#))
                 (aset ~a j# v#))
               (recur i# (dec j#)))))
         i#))))

(defn qsort 
  ([^longs a]
    (qsort a 0 (alength a)))
  ([^longs a ^long lo ^long hi]    
    (let [lo (int lo)
          hi (int hi)]
      (when
        (< (inc lo) hi)
        (let [pivot (aget a lo)
              split (dec (apartition a pivot (inc lo) (dec hi)))]
          (when (> split lo) (swap a lo split))
          (qsort a lo split)
          (qsort a (inc split) hi)))
      a)))

主なトリックは次のとおりです。

  • すべてを原始演算で行う
  • 配列インデックスに int を使用します (これにより、不要なキャストが回避されます。大したことではありませんが、少しでも役立ちます....)
  • 関数ではなくマクロを使用してコードを分割します (関数呼び出しのオーバーヘッドとパラメーターのボックス化を回避します)。
  • 内側のループで最大速度を得るには loop/recur を使用します (つまり、部分配列を分割します)。
  • ヒープ上に新しいオブジェクトを作成しないでください (ベクトル、シーケンス、マップなどは避けてください)。
于 2012-08-29T13:48:55.003 に答える
10

The Joy of Clojureの Chapter 6.4 では、遅延クイックソート アルゴリズムについて説明しています。したがって、x << n の場合、このアルゴリズムは O(n) です。

(ns joy.q)

(defn sort-parts
  "Lazy, tail-recursive, incremental quicksort.  Works against
   and creates partitions based on the pivot, defined as 'work'."
  [work]
  (lazy-seq
   (loop [[part & parts] work]
     (if-let [[pivot & xs] (seq part)]
       (let [smaller? #(< % pivot)]
         (recur (list*
                 (filter smaller? xs)
                 pivot
                 (remove smaller? xs)
                 parts)))
       (when-let [[x & parts] parts]
         (cons x (sort-parts parts)))))))

(defn qsort [xs]
    (sort-parts (list xs))) 
于 2012-08-29T15:32:34.557 に答える
7

mikera の回答の主なポイントを調べると、慣用的な Java 実装にはおそらく存在しない (微調整ではなく) 慣用的な Clojure を使用することによって導入されるオーバーヘッドを排除することに主に焦点が当てられていることがわかります。

  • プリミティブ算術- Java では少し簡単で慣用的です。sintよりもIntegersを使用する可能性が高くなります。
  • 配列インデックスの ints - 同じ
  • 関数ではなくマクロを使用してコードを分割します (関数呼び出しのオーバーヘッドとボックス化を回避します) - 言語の使用によって発生した問題を修正します。Clojure は関数型スタイルを推奨しているため、関数呼び出しのオーバーヘッド (およびボクシング) が発生します。
  • 内側のループで最大速度を得るには loop/recur を使用します。Java では慣用的に通常のループを使用します (これは、私の知る限り、とにかく loop/recur がコンパイルするものです)。

そうは言っても、実際には別の簡単な解決策があります。クイック ソートの効率的な Java 実装を記述 (または検索) し、次のような署名で何かを言います。

Sort.quickSort(long[] elems)

そして、Clojure から呼び出します。

(Sort/quickSort elems)

チェックリスト:

  • Java と同じくらい効率的 -はい

  • Clojure の慣用句 -間違いなくそうです。Java 相互運用性は Clojure のコア機能の 1 つです。

  • 再利用可能 -はい、既に書かれた非常に効率的な Java 実装を簡単に見つけることができる可能性は十分にあります。

私はトロールしようとしているわけではありません。これらの実験であなたが見つけようとしていることを理解しています。完全を期すためにこの回答を追加しているだけです。当たり前のことを見逃さないようにしましょう!:)

于 2012-08-29T14:13:57.510 に答える