Подтвердить что ты не робот

Быстрая сортировка в Clojure

Я пытаюсь доказать, что производительность Clojure может быть на равной основе с Java. Важным вариантом использования, который я нашел, является Quicksort. Я написал реализацию следующим образом:

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

Кроме того, это помогает вызвать quicksort 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[] [[email protected]>
user> (def ys (long-array xs))
user> (time (jqsort ys))
"Elapsed time: 13.895 msecs"
#<long[] [[email protected]>

Производительность - это миры (на порядок, а затем и некоторые).

Есть ли что-нибудь, что мне не хватает, любая функция Clojure, которую я, возможно, использовал? Я думаю, что основным источником снижения производительности является то, что мне нужно вернуть несколько значений из цикла и выделить для этого вектор. Можно ли этого избежать?

BTW running Clojure 1.4. Также обратите внимание, что я несколько раз запускал тест, чтобы разогреть HotSpot. Это время, когда они оседают.

Update

Самая страшная слабость в моем коде - это не просто распределение векторов, а то, что они заставляют бокс и разрушают примитивную цепочку. Другая слабость заключается в использовании результатов loop, поскольку они также разрушают цепочку. Да, производительность в Clojure по-прежнему остается минным полем.

4b9b3361

Ответ 1

Эта версия основана на @mikera's, так же быстро и не требует использования уродливых макросов. На моей машине это занимает ~ 12 мс против ~ 9 мс для java.util.Arrays/sort:

(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 делает это для вас.

Одна вещь, о которой следует помнить, заключается в том, что вложенный цикл /recur действительно представляет проблемы для JVM-inline, поскольку loop/recur не поддерживает (в это время) поддержку возвращаемых примитивов. Таким образом, вы должны разбить свой код на отдельные fns. Это наилучшим образом, так как вложенные loop/recurs становятся все более уродливыми в Clojure.

Для более детального изучения того, как последовательно достичь производительности Java (когда вам это действительно нужно), изучите и поймите test.benchmark.

Ответ 2

Это немного ужасно из-за макросов, но с этим кодом я думаю, что вы можете сопоставить скорость Java (я получаю около 11 мс для теста):

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

Основные трюки:

  • Делать все с помощью примитивной арифметики
  • Использовать ints для индексов массива (это позволяет избежать некоторых ненужных бросков, а не большое дело, но каждый помогает...)
  • Используйте макросы, а не функции для разрыва кода (избегайте служебных вызовов функций и бокса параметров)
  • Использовать loop/recur для максимальной скорости во внутреннем цикле (т.е. разбиение подмассива)
  • Избегайте создания каких-либо новых объектов в куче (избегайте векторов, последовательностей, карт и т.д.).

Ответ 3

Радость Clojure, глава 6.4 описывает ленивый алгоритм быстрой сортировки. Красота ленивой сортировки заключается в том, что она будет делать столько же работайте по мере необходимости, чтобы найти первые значения x. Поэтому, если 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))) 

Ответ 4

Изучив основные моменты ответа mikera, вы можете видеть, что они в основном сосредоточены на устранении накладных расходов, введенных с помощью идиоматического (в отличие от измененного) Clojure, который, вероятно, не будет существовать в идиоматической реализации Java:

  • примитивная арифметика - немного проще и более идиоматично в Java, вы с большей вероятностью будете использовать int, чем Integer s
  • ints для индексов массива - тот же
  • Используйте макросы, а не функции для разложения кода (избегайте функциональных накладных расходов и бокса) - исправляет проблему, возникающую при использовании языка. Clojure поощряет функциональный стиль, следовательно, служебный вызов функции (и бокс).
  • Использовать loop/recur для максимальной скорости во внутреннем цикле - в Java вы бы идиоматически использовали обычный цикл (который, как я знаю, компилирует цикл /recurn, насколько мне известно)

Это, как говорится, фактически есть другое тривиальное решение. Напишите (или найдите) эффективную реализацию Java Quick Sort, произнесите что-то с такой подписью:

Sort.quickSort(long[] elems)

И затем назовите его из Clojure:

(Sort/quickSort elems)

Контрольный список:

  • столь же эффективен, как в Java - да

  • idiomatic в Clojure - возможно да, я бы сказал, что Java-interop является одним из основных функций Clojure.

  • reusable - yes, есть хорошая возможность, что вы можете легко найти очень эффективную реализацию Java, уже написанную.

Я не пытаюсь троллировать, я понимаю, что вы пытаетесь выяснить с помощью этих экспериментов. Я просто добавляю этот ответ ради полноты. Пусть не упускает очевидного!:)Суб >