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

Быстрая векторная математика в Clojure/Incanter

В настоящее время я рассматриваю Clojure и Incanter как альтернативу Р. (Не то, чтобы мне не нравилось 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.

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

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

4b9b3361

Ответ 1

Здесь реализация 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))))))))

Ответ 2

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

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

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

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

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

Сроки для разных функций:

diff возвращающий вектор:

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

diff return 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))) 

Ответ 3

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

Ответ 4

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

Ответ 5

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

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

Ответ 6

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

Ссылка @Shane настолько заполнена устаревшей информацией, что ее вряд ли стоит посмотреть. Также @Shane комментирует, что код медленнее, чем в 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 обескураживает изменяемые операции и изменяемые структуры данных.