Быстрая векторная математика в Clojure / Incanter - PullRequest
22 голосов
/ 28 сентября 2010

В настоящее время я рассматриваю Clojure и Incanter как альтернативу R. (Не то чтобы мне не нравился R, но просто интересно пробовать новые языки.) Мне нравится Incanter и я считаю, что синтаксис привлекателен, но векторизованные операции довольномедленный по сравнению, например, с R или Python.

В качестве примера я хотел получить разность первого порядка вектора, используя векторные операции Incanter, карту Clojure и R.Ниже приведен код и сроки для всех версий.Как видите, R явно быстрее.

Incanter и Clojure:

(use '(incanter core stats)) 
(def x (doall (sample-normal 1e7))) 
(time (def y (doall (minus (rest x) (butlast x))))) 
"Elapsed time: 16481.337 msecs" 
(time (def y (doall (map - (rest x) (butlast x))))) 
"Elapsed time: 16457.850 msecs"

R:

rdiff <- function(x){ 
   n = length(x) 
   x[2:n] - x[1:(n-1)]} 
x = rnorm(1e7) 
system.time(rdiff(x)) 
   user  system elapsed 
  1.504   0.900   2.561

Итак, мне было интересно, есть ли способ ускорить векторные операции в Incanter / Clojure?Также приветствуются решения, включающие использование циклов, Java-массивов и / или библиотек из Clojure.

Я также отправил этот вопрос в группу Google Incanter без ответов.

ОБНОВЛЕНИЕ: Я отметил ответ Джоуни как принятый, см. Ниже мой собственный ответ, где я немного очистил его код и добавил некоторые тесты.

Ответы [ 6 ]

20 голосов
/ 29 сентября 2010

Мои окончательные решения

После всех испытаний я нашел два слегка отличающихся способа выполнить расчет с достаточной скоростью.

Сначала я использовал функцию diff с различными типами возвращаемых значений, ниже приведен код, возвращающий вектор, но я также рассчитал версию, возвращающую двойной массив (заменить (vec y) на y) и Incanter.matrix (заменить (vec y) на матрицу y). Эта функция основана только на массивах Java. Это основано на коде Jouni с некоторыми дополнительными подсказками типа.

Другой подход заключается в выполнении вычислений с массивами Java и сохранении значений в переходном векторе. Как видно из времени, это немного быстрее, чем подход 1, если вы не хотите возвращать функцию и массив. Это реализовано в функции difft.

Так что выбор действительно зависит от того, что вы не хотите делать с данными. Я думаю, что хорошим вариантом было бы перегрузить функцию, чтобы она возвращала тот же тип, который использовался в вызове. На самом деле передача Java-массива в diff вместо вектора делает ~ 1с быстрее.

Времена для различных функций:

Вектор возврата различий:

(time (def y (diff x)))
"Elapsed time: 4733.259 msecs"

diff возвращает Incanter.matrix:

(time (def y (diff x)))
"Elapsed time: 2599.728 msecs"

diff возвращает двойной массив:

(time (def y (diff x)))
"Elapsed time: 1638.548 msecs"

difft:

(time (def y (difft x)))
"Elapsed time: 3683.237 msecs"

Функции

(use 'incanter.stats)
(def x (vec (sample-normal 1e7)))

(defn diff [x]
  (let [y (double-array (dec (count x)))
        x (double-array x)] 
   (dotimes [i (dec (count x))]
     (aset y i
       (- (aget x (inc i))
                   (aget x i))))
   (vec y)))


(defn difft [x]
  (let [y (vector (range n))
        y (transient y)
        x (double-array x)]
   (dotimes [i (dec (count x))]
     (assoc! y i
       (- (aget x (inc i))
                   (aget x i))))
   (persistent! y))) 
13 голосов
/ 29 сентября 2010

Вот реализация Java-массивов, которая в моей системе работает быстрее, чем ваш код R (YMMV).Обратите внимание на включение предупреждений об отражении, что важно при оптимизации производительности, и повторяющуюся подсказку типа на y (та, что в def, похоже, не помогла aset) и приведение всего к примитивным двойным значениям (dotimes гарантирует, чтоя примитивный int).

(set! *warn-on-reflection* true)
(use 'incanter.stats)
(def ^"[D" x (double-array (sample-normal 1e7)))

(time
 (do
   (def ^"[D" y (double-array (dec (count x))))
   (dotimes [i (dec (count x))]
     (aset ^"[D" y
       i
       (double (- (double (aget x (inc i)))
                  (double (aget x i))))))))
3 голосов
/ 30 сентября 2010

Не относится к вашему примеру кода, но, поскольку это превратилось в обсуждение производительности Clojure, вы можете воспользоваться этой ссылкой: Clojure быстр

2 голосов
/ 28 сентября 2010

В блоге Брэдфорда Кросса есть куча сообщений об этом (он использует этот материал для запуска, над которым он работает текст ссылки . В общем, используя переходные процессы во внутренних циклах, введите подсказку(через *warn-on-reflection*) и т. д. все это хорошо для увеличения скорости. В Joy of Clojure есть отличный раздел по настройке производительности, который вы должны прочитать.

1 голос
/ 29 сентября 2010

Вот решение с переходными процессами - привлекательное, но медленное.

(use 'incanter.stats)
(set! *warn-on-reflection* true)
(def x (doall (sample-normal 1e7)))

(time
 (def y
      (loop [xs x
             xs+ (rest x)
             result (transient [])]
        (if (empty? xs+)
          (persistent! result)
          (recur (rest xs) (rest xs+)
                 (conj! result (- (double (first xs+))
                                  (double (first xs)))))))))
0 голосов
/ 28 сентября 2010

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

@ Ссылка Шейна настолько полна устаревшей информации, что вряд ли стоит на нее смотреть. Также комментарий Шейна о том, что код медленнее, чем в 10 раз, просто неточен (и не поддерживается http://shootout.alioth.debian.org/u32q/compare.php?lang=clojure,, и эти тесты не учитывают виды оптимизации, возможные в 1.2.0 или 1.3.0-alpha1). Приложив немного усилий, обычно легко получить код Clojure с 4X-5X. Помимо этого обычно требуется более глубокое знание быстрых путей Clojure - что-то не так широко распространено, так как Clojure - довольно молодой язык.

Clojure довольно быстро. Но изучение того, как сделать это быстро, потребует немного работы / исследований, поскольку Clojure препятствует изменчивым операциям и изменяемым структурам данных.

...