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

Clojure Производительность для дорогостоящих алгоритмов

Я применил алгоритм вычисления самой длинной смежной общей подпоследовательности (не путать с самой длинной общей подпоследовательностью, хотя и неважно для этих вопросов). Мне нужно сжать максимальную производительность, потому что я буду называть это много. Я применил тот же алгоритм в Clojure и Java, чтобы сравнить производительность. Версия Java работает значительно быстрее. Мой вопрос: есть ли что-нибудь, что я могу сделать для версии Clojure, чтобы ускорить ее до уровня Java.

Здесь код Java:

public static int lcs(String[] a1, String[] a2) {
    if (a1 == null || a2 == null) {
        return 0;
    }

    int matchLen = 0;
    int maxLen = 0;

    int a1Len = a1.length;
    int a2Len = a2.length;
    int[] prev = new int[a2Len + 1]; // holds data from previous iteration of inner for loop
    int[] curr = new int[a2Len + 1]; // used for the 'current' iteration of inner for loop

    for (int i = 0; i < a1Len; ++i) {
        for (int j = 0; j < a2Len; ++j) {
            if (a1[i].equals(a2[j])) {
                matchLen = prev[j] + 1; // curr and prev are padded by 1 to allow for this assignment when j=0
            }
            else {
                matchLen = 0;
            }
            curr[j+1] = matchLen;

            if (matchLen > maxLen) {
                maxLen = matchLen;
            }
        }

        int[] swap = prev;
        prev = curr;
        curr = swap;
    }

    return maxLen;
}

Ниже приведена версия Clojure:

(defn lcs
  [#^"[Ljava.lang.String;" a1 #^"[Ljava.lang.String;" a2]
  (let [a1-len (alength a1)
        a2-len (alength a2)
        prev (int-array (inc a2-len))
        curr (int-array (inc a2-len))]
    (loop [i 0 max-len 0 prev prev curr curr]
      (if (< i a1-len)
        (recur (inc i)
               (loop [j 0 max-len max-len]
                 (if (< j a2-len)
                   (if (= (aget a1 i) (aget a2 j))
                     (let [match-len (inc (aget prev j))]
                       (do
                         (aset-int curr (inc j) match-len)
                         (recur (inc j) (max max-len match-len))))
                     (do
                       (aset-int curr (inc j) 0)
                       (recur (inc j) max-len)))
                   max-len))
               curr
               prev)
        max-len))))

Теперь протестируйте их на моей машине:

(def pool "ABC")
(defn get-random-id [n] (apply str (repeatedly n #(rand-nth pool))))
(def a1 (into-array (take 10000 (repeatedly #(get-random-id 5)))))
(def a2 (into-array (take 10000 (repeatedly #(get-random-id 5)))))

Java:

(time (Ratcliff/lcs a1 a2))
"Elapsed time: 1521.455 msecs"

Clojure:

(time (lcs a1 a2))
"Elapsed time: 19863.633 msecs"

Clojure работает быстро, но на порядок медленнее, чем Java. Я могу что-то сделать, чтобы закрыть этот пробел? Или я максимизировал его, и на один порядок величины это "минимальные служебные данные Clojure".

Как вы можете видеть, я уже использую конструкцию цикла "low level", я использую собственные массивы Java, и у меня есть тип-намек на параметры, чтобы избежать отражения.

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

4b9b3361

Ответ 1

РЕДАКТИРОВАТЬ: Добавлена ​​более быстрая уродливая версия ниже первой.

Вот мой прием:

(defn my-lcs [^objects a1 ^objects a2]
  (first
    (let [n (inc (alength a1))]
      (areduce a1 i 
        [max-len ^ints prev ^ints curr] [0 (int-array n) (int-array n)]
        [(areduce a2 j max-len (unchecked-long max-len)
           (let [match-len 
                 (if (.equals (aget a1 i) (aget a2 j))
                   (unchecked-inc (aget prev j))
                   0)]
             (aset curr (unchecked-inc j) match-len)
             (if (> match-len max-len)
               match-len
               max-len)))
         curr prev]))))

Основные различия с вашими: a[gs]et vs a[gs]et-int, использование unchecked- ops (неявно через areduce), использование вектора в качестве возвращаемого значения (и механизм "свопинга" ), а max-len - принудительно до примитивного перед внутренним контуром (примитивнозначные петли проблематичны, немного меньше, чем 1.5RC2, но поддержка еще не идеальна, однако *warn-on-reflection* не молчат).

И я переключился на .equals вместо =, чтобы избежать логики в Clojure equiv.

РЕДАКТИРОВАТЬ: пусть уродливо и восстанавливает трюк свопов массивов:

(deftype F [^:unsynchronized-mutable ^ints curr
            ^:unsynchronized-mutable ^ints prev]
  clojure.lang.IFn
  (invoke [_ a1 a2]
    (let [^objects a1 a1
          ^objects a2 a2]
      (areduce a1 i max-len 0
        (let [m (areduce a2 j max-len (unchecked-long max-len)
                  (let [match-len 
                        (if (.equals (aget a1 i) (aget a2 j))
                          (unchecked-inc (aget prev j))
                          0)]
                    (aset curr (unchecked-inc j) (unchecked-int match-len))
                    (if (> match-len max-len)
                      match-len
                      max-len)))
              bak curr]
          (set! curr prev)
          (set! prev bak)
          m)))))

(defn my-lcs2 [^objects a1 a2]
  (let [n (inc (alength a1))
        f (F. (int-array n) (int-array n))]
    (f a1 a2)))

В моем окне это на 30% быстрее.

Ответ 2

Вот несколько улучшений:

  • Отсутствие преимуществ для намека на тип намека, просто используйте ^ объекты
  • aset-int устарел, я верю - просто старая aget быстрее, примерно на 3 раза в целом кажется

Помимо этого (и длинного типа намека на повторение, упомянутого выше), я не вижу никаких очевидных путей дальнейшего улучшения.

(defn lcs
  [^objects a1 ^objects a2]
  (let [a1-len (alength a1)
        a2-len (alength a2)
        prev (int-array (inc a2-len))
        curr (int-array (inc a2-len))]
    (loop [i 0 max-len 0 prev prev curr curr]
      (if (< i a1-len)
        (recur (inc i)
               (long (loop [j 0 max-len max-len]
                 (if (< j a2-len)
                   (if (= (aget a1 i) (aget a2 j))
                     (let [match-len (inc (aget prev j))]
                       (do
                         (aset curr (inc j) match-len)
                         (recur (inc j) (max max-len match-len))))
                     (do
                       (aset curr (inc j) 0)
                       (recur (inc j) max-len)))
                   max-len)))
               curr
               prev)
        max-len))))
#'user/lcs
user> (time (lcs a1 a2))
"Elapsed time: 3862.211 msecs"